A concrete checklist of React performance best practices to minimize unnecessary renders, reduce bundle size, and keep your UI fast and responsive.
1. Memoize expensive components with React.memo
Wrap pure functional components in React.memo to skip re-renders when props have not changed. Combine with a custom comparator as the second argument for fine-grained control.
2. Cache derived values with useMemo
Use useMemo for computationally expensive calculations so they only recompute when their dependencies change, not on every render. Avoid overusing it on cheap operations where the memoization overhead outweighs the benefit.
3. Stabilize callbacks with useCallback
Wrap event handlers and functions passed as props with useCallback so child components that depend on referential equality do not re-render unnecessarily. Provide an accurate dependency array to avoid stale closures.
4. Virtualize long lists with a windowing library
Use react-window or react-virtual to render only the visible portion of large lists or tables instead of mounting thousands of DOM nodes. This dramatically reduces initial render time and memory usage.
5. Split code with dynamic imports and React.lazy
Use React.lazy with Suspense and dynamic import() to load route-level or heavy components on demand, reducing the initial JavaScript bundle sent to the browser.
6. Avoid anonymous functions and objects in JSX
Defining functions or object literals inline in JSX creates a new reference on every render, breaking memoization for child components. Extract them outside the render or stabilize them with useCallback/useMemo.
7. Keep component state as local as possible
Lifting state unnecessarily high causes large subtrees to re-render. Co-locate state with the component that owns it, and pass only the minimal props required by each child.
8. Use context selectively and split contexts by concern
A single large Context causes every consumer to re-render when any part of the value changes. Split contexts by update frequency (e.g., theme vs. user data) and use a selector library like use-context-selector for granular subscriptions.
9. Analyze renders with the React DevTools Profiler
Use the Profiler tab in React DevTools to record interactions, identify which components render most often, and measure their render durations before and after optimizations. Never guess — always profile first.
10. Defer non-critical updates with useTransition or useDeferredValue
Mark expensive state updates as transitions with useTransition so React can interrupt them in favor of urgent user interactions. Use useDeferredValue to debounce a derived value without blocking input responsiveness.
11. Lazy-initialize expensive useState values
Pass a function (not a value) to useState when the initial state requires a heavy computation, so the computation runs only once on mount instead of on every render invocation.
12. Optimize images and static assets
Use next/image or equivalent lazy-loading solutions, serve correctly sized images, and apply modern formats like WebP. Large images are a leading cause of poor Largest Contentful Paint scores in React apps.
13. Minimize and tree-shake your production bundle
Ensure your bundler (Vite, webpack, etc.) runs in production mode with tree-shaking enabled. Import only the specific functions you need from large libraries (e.g., import debounce from 'lodash/debounce' instead of the whole lodash).
14. Avoid layout thrashing in effects and event handlers
Batch DOM reads before writes and avoid alternating reads and writes inside useEffect or event handlers, as this forces the browser to recalculate layout repeatedly. Use requestAnimationFrame when manually manipulating the DOM.