Advanced techniques for optimizing React applications including memoization, code splitting, and bundle optimization.
React applications can become slow if not optimized properly. This comprehensive guide covers advanced techniques to make your React apps blazingly fast, from component optimization to bundle splitting strategies.
Before optimizing, it's crucial to understand how React works:
Use React DevTools Profiler to identify performance bottlenecks:
// Enable profiler in development
import { Profiler } from "react";
function onRenderCallback(id, phase, actualDuration) {
console.log("Component:", id, "Phase:", phase, "Duration:", actualDuration);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}
// Before optimization - re-renders on every parent update
const ExpensiveComponent = ({ data, theme }) => {
const processedData = expensiveCalculation(data);
return (
<div className={theme}>
{processedData.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
// After optimization - only re-renders when props change
const OptimizedComponent = React.memo(({ data, theme }) => {
const processedData = expensiveCalculation(data);
return (
<div className={theme}>
{processedData.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});
// Custom comparison function for complex props
const OptimizedComponentWithComparison = React.memo(
({ data, theme }) => {
// Component implementation
},
(prevProps, nextProps) => {
return (
prevProps.theme === nextProps.theme &&
JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data)
);
}
);
import { useMemo, useState } from "react";
function ProductList({ products, filters }) {
const [sortBy, setSortBy] = useState("name");
// Expensive calculation memoized
const filteredAndSortedProducts = useMemo(() => {
console.log("Recalculating filtered products...");
return products
.filter((product) => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return product[key].toLowerCase().includes(value.toLowerCase());
});
})
.sort((a, b) => {
if (sortBy === "price") return a.price - b.price;
if (sortBy === "name") return a.name.localeCompare(b.name);
return 0;
});
}, [products, filters, sortBy]);
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
{filteredAndSortedProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
import { useCallback, useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
// Without useCallback - new function on every render
const addTodo = (text) => {
setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
};
// With useCallback - stable function reference
const optimizedAddTodo = useCallback((text) => {
setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
}, []);
const toggleTodo = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const deleteTodo = useCallback((id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
return (
<div>
<TodoForm onAdd={optimizedAddTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
</div>
);
}
// Child component won't re-render unnecessarily
const TodoForm = React.memo(({ onAdd }) => {
const [text, setText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onAdd(text.trim());
setText("");
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add todo..."
/>
<button type="submit">Add</button>
</form>
);
});
import { lazy, Suspense } from "react";
// Lazy load components
const Dashboard = lazy(() => import("./Dashboard"));
const Profile = lazy(() => import("./Profile"));
const Settings = lazy(() => import("./Settings"));
// Loading component
const LoadingSpinner = () => (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
function App() {
return (
<Router>
<Routes>
<Route
path="/"
element={
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
}
/>
<Route
path="/profile"
element={
<Suspense fallback={<LoadingSpinner />}>
<Profile />
</Suspense>
}
/>
<Route
path="/settings"
element={
<Suspense fallback={<LoadingSpinner />}>
<Settings />
</Suspense>
}
/>
</Routes>
</Router>
);
}
import { Component, lazy, Suspense } from "react";
// Error boundary for handling loading errors
class LazyLoadErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Lazy loading error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong loading this component.</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Advanced lazy loading with retry mechanism
const createLazyComponent = (importFunc, retries = 3) => {
return lazy(async () => {
let attempt = 0;
while (attempt < retries) {
try {
return await importFunc();
} catch (error) {
attempt++;
if (attempt >= retries) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
});
};
const HeavyComponent = createLazyComponent(() => import("./HeavyComponent"));
function App() {
return (
<LazyLoadErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
</LazyLoadErrorBoundary>
);
}
import { FixedSizeList as List } from "react-window";
// Virtualized list for thousands of items
function VirtualizedProductList({ products }) {
const Row = ({ index, style }) => (
<div style={style} className="product-row">
<ProductCard product={products[index]} />
</div>
);
return (
<List height={600} itemCount={products.length} itemSize={120} width="100%">
{Row}
</List>
);
}
// Infinite loading with virtualization
import InfiniteLoader from "react-window-infinite-loader";
function InfiniteProductList() {
const [products, setProducts] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const loadMoreItems = useCallback(
async (startIndex, stopIndex) => {
if (loading) return;
setLoading(true);
try {
const newProducts = await fetchProducts(startIndex, stopIndex);
setProducts((prev) => [...prev, ...newProducts]);
setHasMore(newProducts.length > 0);
} catch (error) {
console.error("Error loading products:", error);
} finally {
setLoading(false);
}
},
[loading]
);
const isItemLoaded = (index) => !!products[index];
const Row = ({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? (
<ProductCard product={products[index]} />
) : (
<div className="loading-placeholder">Loading...</div>
)}
</div>
);
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasMore ? products.length + 1 : products.length}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
ref={ref}
height={600}
itemCount={hasMore ? products.length + 1 : products.length}
itemSize={120}
onItemsRendered={onItemsRendered}
width="100%"
>
{Row}
</List>
)}
</InfiniteLoader>
);
}
// Lazy loading images with intersection observer
import { useState, useRef, useEffect } from "react";
function LazyImage({ src, alt, className, placeholder }) {
const [loaded, setLoaded] = useState(false);
const [inView, setInView] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} className={className}>
{inView && (
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{
opacity: loaded ? 1 : 0,
transition: "opacity 0.3s ease-in-out",
}}
/>
)}
{!loaded && inView && (
<div className="image-placeholder">{placeholder || "Loading..."}</div>
)}
</div>
);
}
// Progressive image loading
function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt }) {
const [highQualityLoaded, setHighQualityLoaded] = useState(false);
return (
<div className="progressive-image">
<img
src={lowQualitySrc}
alt={alt}
className={`low-quality ${highQualityLoaded ? "fade-out" : ""}`}
/>
<img
src={highQualitySrc}
alt={alt}
className={`high-quality ${highQualityLoaded ? "fade-in" : ""}`}
onLoad={() => setHighQualityLoaded(true)}
/>
</div>
);
}
# Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to package.json scripts
"analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
// ❌ Bad - imports entire library
import _ from "lodash";
import * as utils from "./utils";
// ✅ Good - import only what you need
import { debounce, throttle } from "lodash";
import { formatDate, validateEmail } from "./utils";
// ❌ Bad - creates unused code
const utils = {
formatDate: (date) => date.toLocaleDateString(),
formatTime: (date) => date.toLocaleTimeString(),
formatDateTime: (date) => date.toLocaleString(),
validateEmail: (email) => /\S+@\S+\.\S+/.test(email),
validatePhone: (phone) => /^\d{10}$/.test(phone),
};
// ✅ Good - separate utility functions
export const formatDate = (date) => date.toLocaleDateString();
export const validateEmail = (email) => /\S+@\S+\.\S+/.test(email);
// Custom hook for performance monitoring
import { useEffect, useRef } from "react";
function usePerformanceMonitor(componentName) {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const endTime = performance.now();
const renderTime = endTime - startTime.current;
console.log(
`${componentName} - Render #${
renderCount.current
} - Time: ${renderTime.toFixed(2)}ms`
);
startTime.current = performance.now();
});
useEffect(() => {
return () => {
console.log(
`${componentName} unmounted after ${renderCount.current} renders`
);
};
}, [componentName]);
}
// Usage
function MyComponent() {
usePerformanceMonitor("MyComponent");
// Component logic
return <div>My Component</div>;
}
// ❌ Anti-pattern: Creating objects in render
function BadComponent({ items }) {
return (
<div>
{items.map((item) => (
<ItemComponent
key={item.id}
item={item}
style={{ padding: "10px" }} // New object every render!
/>
))}
</div>
);
}
// ✅ Good: Stable object references
const itemStyle = { padding: "10px" };
function GoodComponent({ items }) {
return (
<div>
{items.map((item) => (
<ItemComponent key={item.id} item={item} style={itemStyle} />
))}
</div>
);
}
React performance optimization is about understanding when and why components re-render, then applying the right techniques to minimize unnecessary work. Start with measuring, identify bottlenecks, and apply these optimization techniques strategically.
Remember: fast enough is often better than perfectly optimized. Focus on the user experience and measure the impact of your optimizations! ⚡