Performance Optimization
Best practices for optimizing performance in Fireact applications.
Overview
This guide covers performance optimization strategies for Fireact applications, focusing on frontend React performance, Firebase optimization, and Cloud Functions efficiency.
React Performance
Code Splitting
Split your application into smaller bundles for faster initial load:
// src/App.tsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Subscription = lazy(() => import('./pages/Subscription'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/subscription" element={<Subscription />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Memoization
Use React.memo, useMemo, and useCallback to prevent unnecessary re-renders:
// ✅ Good: Memoized component
export const UserCard = React.memo<{ user: User }>(({ user }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
// ✅ Good: Memoized expensive calculation
function Dashboard() {
const { subscriptions } = useSubscriptions();
const totalRevenue = useMemo(() => {
return subscriptions.reduce((sum, sub) => sum + sub.amount, 0);
}, [subscriptions]);
return <div>Total: ${totalRevenue}</div>;
}
// ✅ Good: Memoized callback
function UserList() {
const handleUserClick = useCallback((userId: string) => {
console.log('User clicked:', userId);
}, []);
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} onClick={handleUserClick} />
))}
</div>
);
}
Virtual Scrolling
Implement virtual scrolling for large lists:
// Using react-window
import { FixedSizeList } from 'react-window';
interface Item {
id: string;
name: string;
}
const Row: React.FC<{ index: number; style: React.CSSProperties; data: Item[] }> = ({
index,
style,
data,
}) => {
const item = data[index];
return (
<div style={style} className="flex items-center p-4 border-b">
{item.name}
</div>
);
};
export const VirtualList: React.FC<{ items: Item[] }> = ({ items }) => {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={60}
width="100%"
itemData={items}
>
{Row}
</FixedSizeList>
);
};
Debouncing and Throttling
Optimize event handlers with debounce and throttle:
import { useState, useCallback } from 'react';
import { debounce } from 'lodash';
export const SearchInput: React.FC = () => {
const [query, setQuery] = useState('');
// Debounce search to avoid excessive API calls
const debouncedSearch = useCallback(
debounce(async (searchQuery: string) => {
if (searchQuery.length < 3) return;
const results = await searchAPI(searchQuery);
setSearchResults(results);
}, 500),
[]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
};
// Throttle scroll events
import { throttle } from 'lodash';
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
Firebase Performance
Firestore Query Optimization
Use Composite Indexes
// ❌ Bad: Multiple separate queries
const getActiveUserSubscriptions = async (userId: string) => {
const allSubscriptions = await getDocs(
query(collection(db, 'subscriptions'), where('userId', '==', userId))
);
return allSubscriptions.docs.filter(doc => doc.data().status === 'active');
};
// ✅ Good: Single optimized query with composite index
const getActiveUserSubscriptions = async (userId: string) => {
const q = query(
collection(db, 'subscriptions'),
where('userId', '==', userId),
where('status', '==', 'active')
);
return await getDocs(q);
};
// Create composite index in firebase.json:
// "indexes": [
// {
// "collectionGroup": "subscriptions",
// "queryScope": "COLLECTION",
// "fields": [
// { "fieldPath": "userId", "order": "ASCENDING" },
// { "fieldPath": "status", "order": "ASCENDING" }
// ]
// }
// ]
Limit Query Results
// ❌ Bad: Fetching all documents
const getAllUsers = async () => {
return await getDocs(collection(db, 'users'));
};
// ✅ Good: Limit results with pagination
const getUsersPage = async (pageSize: number = 20, lastDoc?: DocumentSnapshot) => {
let q = query(
collection(db, 'users'),
orderBy('createdAt', 'desc'),
limit(pageSize)
);
if (lastDoc) {
q = query(q, startAfter(lastDoc));
}
const snapshot = await getDocs(q);
return {
users: snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })),
lastDoc: snapshot.docs[snapshot.docs.length - 1],
};
};
Use Real-time Listeners Wisely
// ❌ Bad: Creating new listener on every render
function SubscriptionDetails({ subscriptionId }: { subscriptionId: string }) {
const [subscription, setSubscription] = useState<any>(null);
// This creates a new listener on every render!
onSnapshot(doc(db, 'subscriptions', subscriptionId), (doc) => {
setSubscription(doc.data());
});
return <div>{subscription?.name}</div>;
}
// ✅ Good: Listener in useEffect with cleanup
function SubscriptionDetails({ subscriptionId }: { subscriptionId: string }) {
const [subscription, setSubscription] = useState<any>(null);
useEffect(() => {
const unsubscribe = onSnapshot(
doc(db, 'subscriptions', subscriptionId),
(doc) => {
setSubscription(doc.data());
}
);
return () => unsubscribe(); // Cleanup listener
}, [subscriptionId]);
return <div>{subscription?.name}</div>;
}
Batch Operations
// ❌ Bad: Multiple individual writes
const updateMultipleUsers = async (userIds: string[], updates: any) => {
for (const userId of userIds) {
await updateDoc(doc(db, 'users', userId), updates);
}
};
// ✅ Good: Batch write
const updateMultipleUsers = async (userIds: string[], updates: any) => {
const batch = writeBatch(db);
userIds.forEach((userId) => {
const userRef = doc(db, 'users', userId);
batch.update(userRef, updates);
});
await batch.commit(); // Single network call
};
Firestore Cache
// Enable offline persistence for faster reads
import { enableIndexedDbPersistence } from 'firebase/firestore';
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === 'failed-precondition') {
console.error('Multiple tabs open');
} else if (err.code === 'unimplemented') {
console.error('Browser doesn\'t support persistence');
}
});
// Use cache-first strategy
const getUserFromCache = async (userId: string) => {
const docRef = doc(db, 'users', userId);
const docSnap = await getDocFromCache(docRef);
if (docSnap.exists()) {
return docSnap.data();
}
// Fallback to server
const serverSnap = await getDocFromServer(docRef);
return serverSnap.data();
};
Cloud Functions Performance
Cold Start Optimization
// ❌ Bad: Initialize inside function
export const slowFunction = functions.https.onCall(async (data, context) => {
const stripe = new Stripe(functions.config().stripe.secret_key, {
apiVersion: '2023-10-16',
});
return await stripe.customers.list();
});
// ✅ Good: Initialize outside function (reused across invocations)
const stripe = new Stripe(functions.config().stripe.secret_key, {
apiVersion: '2023-10-16',
});
export const fastFunction = functions.https.onCall(async (data, context) => {
return await stripe.customers.list();
});
Function Region Selection
// Deploy functions to region closest to users
import * as functions from 'firebase-functions';
// US users
export const usFunction = functions
.region('us-central1')
.https.onCall(async (data, context) => {
// Function logic
});
// European users
export const euFunction = functions
.region('europe-west1')
.https.onCall(async (data, context) => {
// Function logic
});
Memory and Timeout Configuration
// Configure memory and timeout based on function needs
export const heavyFunction = functions
.runWith({
memory: '2GB',
timeoutSeconds: 300,
})
.https.onCall(async (data, context) => {
// CPU/memory intensive operation
});
export const lightFunction = functions
.runWith({
memory: '256MB',
timeoutSeconds: 60,
})
.https.onCall(async (data, context) => {
// Simple operation
});
Parallel Processing
// ❌ Bad: Sequential processing
export const processUsers = functions.https.onCall(async (data, context) => {
const users = await getUsers();
for (const user of users) {
await processUser(user);
}
});
// ✅ Good: Parallel processing
export const processUsers = functions.https.onCall(async (data, context) => {
const users = await getUsers();
await Promise.all(users.map(user => processUser(user)));
});
// ✅ Better: Chunked parallel processing (avoid overwhelming resources)
export const processUsers = functions.https.onCall(async (data, context) => {
const users = await getUsers();
const chunkSize = 10;
for (let i = 0; i < users.length; i += chunkSize) {
const chunk = users.slice(i, i + chunkSize);
await Promise.all(chunk.map(user => processUser(user)));
}
});
Asset Optimization
Image Optimization
// Use next-gen formats and lazy loading
export const OptimizedImage: React.FC<{
src: string;
alt: string;
width: number;
height: number;
}> = ({ src, alt, width, height }) => {
return (
<picture>
<source srcSet={`${src}.webp`} type="image/webp" />
<source srcSet={`${src}.jpg`} type="image/jpeg" />
<img
src={`${src}.jpg`}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
</picture>
);
};
// Resize images before upload
import imageCompression from 'browser-image-compression';
const compressImage = async (file: File) => {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
};
return await imageCompression(file, options);
};
Bundle Size Optimization
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'firebase-vendor': ['firebase/app', 'firebase/auth', 'firebase/firestore'],
'ui-vendor': ['@headlessui/react', '@heroicons/react'],
},
},
},
},
});
Monitoring Performance
Performance Metrics
// src/services/performance/monitoring.ts
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals';
export const initializePerformanceMonitoring = () => {
onCLS(console.log); // Cumulative Layout Shift
onFID(console.log); // First Input Delay
onLCP(console.log); // Largest Contentful Paint
onFCP(console.log); // First Contentful Paint
onTTFB(console.log); // Time to First Byte
};
// Report to analytics
import { trackEvent } from '../analytics/ga4';
const reportWebVital = (metric: any) => {
trackEvent('Web Vitals', metric.name, metric.id, Math.round(metric.value));
};
onCLS(reportWebVital);
onFID(reportWebVital);
onLCP(reportWebVital);
Firebase Performance Monitoring
// Enable Firebase Performance Monitoring
import { getPerformance } from 'firebase/performance';
const perf = getPerformance(app);
// Custom traces
import { trace } from 'firebase/performance';
const fetchData = async () => {
const t = trace(perf, 'fetchUserData');
t.start();
try {
const data = await getData();
t.putAttribute('dataSize', String(data.length));
return data;
} finally {
t.stop();
}
};
Checklist
- Implement code splitting for routes
- Use React.memo for expensive components
- Add virtual scrolling for large lists
- Debounce search and input handlers
- Create Firestore composite indexes
- Implement pagination for queries
- Use batch operations for multiple writes
- Enable Firestore offline persistence
- Optimize Cloud Functions cold starts
- Configure appropriate memory/timeout
- Compress and lazy load images
- Analyze and optimize bundle size
- Monitor web vitals and performance metrics
See Also
- Code Organization Best Practices
- Error Handling Best Practices
- Data Management Examples
- Cloud Functions Examples
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.