Articles › React
React Performance Optimization: Re-renders, Memoization, Code Splitting and Virtualization
Make React apps fast: understand re-renders, measure with the Profiler, use memo, useMemo and useCallback wisely, split code, virtualize long lists, defer work and optimize images.
Most React apps are fast by default. When they're not, the cause is usually one of a few patterns: components re-rendering far more than needed, expensive work on every render, too much JavaScript on first load, or thousands of DOM nodes. This guide shows how to find the real bottleneck and apply the right fix — without sprinkling useMemo everywhere.
Measure first
Optimizing blind wastes time and makes code harder to read. Use:
- React DevTools Profiler — records renders, shows which components rendered, why, and how long they took. Enable "Highlight updates when components render" to see re-renders live.
- Chrome Performance panel — long tasks, layout thrashing, scripting time.
- Lighthouse / Core Web Vitals — real-world loading and interaction metrics (LCP, INP, CLS).
Fix what the measurements point to.
Why components re-render
A component re-renders when:
- Its state changes,
- Its parent re-renders (by default, children re-render too),
- A context it consumes changes.
Re-rendering isn't automatically bad — React only touches the DOM where output actually changed. It becomes a problem when a re-render is expensive or happens constantly (typing, scrolling, animations).
1. Keep state close to where it's used
The cheapest optimization is structural. State high in the tree re-renders everything below it:
// ❌ Typing in the search box re-renders the whole page
function Page() {
const [query, setQuery] = useState("");
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<HeavyChart />
<ArticleGrid />
</>
);
}
// ✅ Move the state down into its own component
function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Another trick: pass expensive subtrees as children — they're created by the parent and don't re-render when the wrapper's state changes:
function ScrollTracker({ children }) {
const [y, setY] = useState(0);
// ... updates y on scroll
return <div data-y={y}>{children}</div>; // children don't re-render on scroll
}
2. React.memo for pure components
React.memo skips re-rendering a component when its props are shallowly equal:
const ArticleCard = memo(function ArticleCard({ article, onOpen }) {
return (
<article onClick={() => onOpen(article.slug)}>
<h3>{article.title}</h3>
</article>
);
});
It only helps if props are stable. A new object or function on every parent render defeats it — that's where useMemo and useCallback come in.
3. useMemo and useCallback — with purpose
function ArticleList({ articles, tag, onOpen }) {
const visible = useMemo(
() => articles.filter((a) => !tag || a.tag === tag).sort(byDateDesc),
[articles, tag]
);
const handleOpen = useCallback((slug) => onOpen(slug), [onOpen]);
return visible.map((a) => <ArticleCard key={a.id} article={a} onOpen={handleOpen} />);
}
Use them when:
- a calculation is genuinely expensive (measurable), or
- a value/function is passed to a memoized child or used as an effect dependency.
Don't use them for cheap calculations "just in case" — memoization has its own cost and adds noise. The React Compiler can now insert most of this memoization automatically; if your project uses it, write plain code and let the compiler optimize.
4. Stable keys in lists
// ❌ index keys break state and cause extra work when items are inserted or reordered
items.map((item, i) => <Row key={i} item={item} />)
// ✅ stable unique ids
items.map((item) => <Row key={item.id} item={item} />)
5. Split contexts and avoid giant providers
When a context value changes, every consumer re-renders. Split contexts by update frequency (ThemeContext vs CartContext), memoize provider values, or use a store with selectors (Zustand, Redux) for frequently changing state.
6. Code splitting: ship less JavaScript
Users shouldn't download the admin panel to read an article:
const AdminPanel = lazy(() => import("./admin/AdminPanel"));
const ArticlePage = lazy(() => import("./pages/ArticlePage"));
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/admin/*" element={<AdminPanel />} />
<Route path="/articles/:slug" element={<ArticlePage />} />
</Routes>
</Suspense>
Also:
- Import only what you use (
import { debounce } from "lodash-es", not the whole library). - Analyze bundles with
rollup-plugin-visualizerorvite-bundle-visualizer. - Lazy-load heavy libraries (charts, editors, syntax highlighters) inside the components that need them.
On this site, moving Markdown parsing and syntax highlighting into the lazily loaded article page cut the main bundle by roughly 40%.
7. Virtualize long lists
Rendering 5,000 rows creates 5,000 DOM nodes. Virtualization renders only the visible ones:
import { useVirtualizer } from "@tanstack/react-virtual";
function LogList({ rows }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((v) => (
<div key={v.key} style={{ position: "absolute", top: 0, transform: `translateY(${v.start}px)`, height: v.size, width: "100%" }}>
{rows[v.index].message}
</div>
))}
</div>
</div>
);
}
For moderately long lists, pagination or "load more" is often simpler and better UX.
8. Keep interactions responsive with transitions
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState(allArticles);
function handleChange(e) {
setQuery(e.target.value); // urgent: update the input
startTransition(() => {
setResults(filterArticles(allArticles, e.target.value)); // non-urgent
});
}
useTransition (or useDeferredValue) lets React keep typing smooth while a heavy list re-renders in the background — directly improving the INP metric.
9. Debounce and throttle expensive events
Search-as-you-type, resize and scroll handlers can fire dozens of times per second. Debounce network requests; throttle scroll handlers or use IntersectionObserver instead of scroll events for visibility detection.
10. Images and assets
Often the biggest win isn't React at all:
- Serve modern formats (WebP/AVIF) at the right size; use
srcset. - Add
loading="lazy"to below-the-fold images and explicitwidth/heightto prevent layout shift. - Preload the hero image (
fetchpriority="high"). - Self-host and subset fonts; use
font-display: swap.
11. Animate the cheap properties
Animate transform and opacity — they're handled by the compositor. Animating width, top or box-shadow triggers layout or paint on every frame. Libraries like GSAP make this easy, and prefers-reduced-motion should disable non-essential motion.
Checklist
| Symptom | Likely fix |
|---|---|
| Typing feels laggy | move state down, useTransition, debounce |
| Whole page re-renders on small changes | split components/contexts, memo |
| Slow first load | code splitting, smaller bundles, image optimization |
| Huge list scrolls poorly | virtualization or pagination |
| Janky animations | animate transform/opacity, avoid layout thrashing |
Summary
Measure before optimizing. Then fix structure first — keep state local, split contexts, pass children — and reach for memo, useMemo and useCallback only where profiling shows real cost. Ship less JavaScript with code splitting, virtualize big lists, keep interactions responsive with transitions, and don't forget images, fonts and animation properties. Fast React apps are mostly the result of good architecture, not micro-optimizations.
FAQ
Should I wrap every component in React.memo?
No. It adds comparison overhead and complexity. Use it for components that re-render often with the same props and are expensive to render.
Does StrictMode slow down my app?
Only in development, where it intentionally double-invokes some functions. Production builds aren't affected.
Is React slower than other frameworks?
For typical apps the difference is rarely noticeable. Architecture, bundle size and asset optimization matter much more than the framework choice.