Some performance optimization techniques that can be used in React applications. Most of these techniques are not specific to React or any frontend libraries or framework, but they are worth considering wherever applicable when building frontend apps.
Most frontend applications render a dynamic list of some kind, dumping a huge list of data into the DOM is inefficient and can cause severe performance issues. List virtualization (windowing) can render only a subset of the list currently visible in the viewport, with more list items added to the viewport as the user scrolls. This ensures optimum performance by reducing the number of nodes added to the DOM during rendering while efficiently adding more list items on demand.
💡 React Virtualized and React Window are worth considering for an efficient implementation of list virtualization.
A basic implementation in React might look like this:
import React, { useState, useEffect, useRef } from "react";
const VirtualizedList = () => {
// Sample data - in real usage this could be much larger
const [items] = useState(Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`));
// Configuration
const itemHeight = 40; // Height of each list item in pixels
const containerHeight = 400; // Height of the visible container
const overscan = 3; // Number of items to render above/below visible area
// State for scroll position and visible range
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);
// Calculate visible range
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(items.length, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan);
// Get visible items
const visibleItems = items.slice(startIndex, endIndex);
// Handle scroll events
const handleScroll = (event) => {
setScrollTop(event.target.scrollTop);
};
// Calculate total height of all items
const totalHeight = items.length * itemHeight;
// Calculate offset for current scroll position
const offsetY = startIndex * itemHeight;
return (
<div className="border border-gray-200 rounded">
<div ref={containerRef} className="overflow-auto" style={{ height: containerHeight }} onScroll={handleScroll}>
<div className="relative w-full" style={{ height: totalHeight }}>
<div className="absolute left-0 right-0" style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
className="px-4 py-2 border-b border-gray-100 hover:bg-gray-50"
style={{ height: itemHeight }}
>
{item}
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default VirtualizedList;
Memoization is a technique used to cache the results of expensive computations or function calls. It is particularly useful when a function repeatedly receives the same input for the same operation. By memoizing a function, it only runs when the input values change, thereby reducing unnecessary executions. In React, this is implemented through React.memo
, the useMemo
Hook, and the useCallback
Hook (which is particularly useful when passing functions as props to child components). Let's break down the main memoization tools in React:
const MyComponent = React.memo(function MyComponent(props) {
// render using props
});
This is for component-level memoization. It prevents re-renders if the props haven't changed. Useful for:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
This memoizes values and is useful when:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
This memoizes functions. Particularly important when:
A practical example of these memoization concepts:
function ExpensiveList({ items, onItemClick }) {
// Memoize expensive calculation
const sortedItems = useMemo(() => {
return items.sort((a, b) => b.value - a.value);
}, [items]);
// Memoize callback
const handleClick = useCallback(
(id) => {
onItemClick(id);
},
[onItemClick]
);
return (
<ul>
{sortedItems.map((item) => (
<ListItem key={item.id} item={item} onClick={()=> handleClick(item.id)} />
))}
</ul>
);
}
// Memoize the entire component
const MemoizedExpensiveList = React.memo(ExpensiveList);
Throttling is a technique used to limit how frequently a function or event handler can be invoked. It ensures that the function is called only at specified intervals, preventing excessive executions. For example, when detecting window resize events, if multiple invocations occur within the specified interval, the application only registers the first one and ignores subsequent calls until the interval has elapsed.
import React, { useState, useEffect } from "react";
import { throttle } from "lodash";
const WindowSizeDisplay = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
// Update window size
const handleResize = throttle(() => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 250); // Throttle to once every 250ms
// Set initial size
handleResize();
// Add event listener
window.addEventListener("resize", handleResize);
// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
handleResize.cancel();
};
}, []);
return (
<div className="w-64">
<div className="text-center">
<h2 className="text-xl font-semibold mb-4">Window Size</h2>
<div className="space-y-2">
<p className="text-gray-600">
Width: <span className="font-medium">{windowSize.width}px</span>
</p>
<p className="text-gray-600">
Height: <span className="font-medium">{windowSize.height}px</span>
</p>
</div>
</div>
</div>
);
};
export default WindowSizeDisplay;
Debouncing is a technique that delays the execution of a function until after a specified period of user inactivity. It's particularly useful for handling frequent events like typing in a search box. Without debouncing, functions might execute unnecessarily with every event (like every keystroke). For example, rather than making an API call after each keystroke as a user types, debouncing ensures the call only happens after the user has finished typing.
import React, { useState } from "react";
import { debounce } from "lodash";
function SearchBar() {
const [searchResults, setSearchResults] = useState([]);
// Create debounced search function
const debouncedSearch = debounce(async (query) => {
if (!query) {
setSearchResults([]);
return;
}
try {
// Simulated API call
console.log(`Searching for: ${query}`);
// const response = await fetch(`/api/search?q=${query}`);
// const data = await response.json();
// setSearchResults(data);
} catch (error) {
console.error("Search failed:", error);
}
}, 500);
const handleInputChange = (event) => {
const query = event.target.value;
debouncedSearch(query);
};
return (
<div>
<input type="text" placeholder="Search..." onChange={handleInputChange} className="border p-2 rounded" />
{/* Display results */}
<ul>
{searchResults.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
Key differences between debouncing and throttling:
Some Common use cases for debouncing:
Code Splitting is a technique used to break down large React/JavaScript bundles into smaller, more manageable chunks. Instead of loading the entire application code upfront, it allows you to load specific code pieces only when they're needed. This improves initial page load performance and reduces the amount of JavaScript that needs to be downloaded and parsed. The Bundle Analyzer tool can help visualize these code splits and identify opportunities for optimization.
Basic Route-based Code Splitting using React.lazy and Suspense:
import React, { Suspense, lazy } from "react";
const HeavyComponent = lazy(() => import("./HeavyComponent"));
export function MyComponent() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={()=> setShowHeavy(true)}>Load Heavy Component</button>
{showHeavy && (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}