Performance patterns focus on:
- Reducing unnecessary re-renders
- Minimizing computation
- Scaling UI efficiently as applications grow
Apply these patterns intentionally, not prematurely.
Caching values or functions so they are not recreated on every render.
useMemouseCallbackReact.memo
- Expensive computations
- Stable props passed to memoized components
- Preventing unnecessary re-renders
- Overusing memoization adds complexity with little benefit
A derived state is any value that can be computed from existing state or props.
const fullName = `${firstName} ${lastName}`;- Derived state should NOT be stored in React state.
const [fullName, setFullName] = useState('');- Causes unnecessary re-renders
- Requires syncing original state and derived state
- Leads to stale or inconsistent UI
- Increases cognitive complexity
- Compute derived values during render
- Memoize only if the computation is expensive
const fullName = `${firstName} ${lastName}`;
// or, if expensive
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);Controls how often a function runs:
- Debounce → runs after inactivity
- Throttle → runs at fixed intervals
- Search inputs
- Window resize
- Scroll handlers
- Autosave
- Forgetting cleanup causes memory leaks
- Incorrect dependencies cause stale closures
Code splitting, dynamic imports, and lazy-loaded assets improve performance when bundle size or initial load becomes a problem.
They reduce:
- Initial JavaScript payload
- Time to interactive (TTI)
- Unnecessary work on first render
Apply them based on user flow, not everywhere.
Load components only when they are actually needed.
const SettingsPage = React.lazy(() => import('./SettingsPage'));
<Suspense fallback={<Loader />}>
<SettingsPage />
</Suspense>;- Routes not visited immediately
- Heavy components (e.g., charts, editors)
- Admin or secondary screens
- Lazy-loading everything increases complexity
- Too many loading boundaries hurt UX
Load code at runtime instead of upfront.
if (isAdmin) {
import('./admin-utils').then(...)
}- Feature flags
- Role-based features
- Rarely used logic (PDF export, analytics, maps)
- Harder error handling
- Can hide architectural issues if overused
Load images only when they enter the viewport.
<img src="image.jpg" loading="lazy" />- Image-heavy pages
- Long scrolling pages
- Feeds, galleries, blogs
- Above-the-fold images should not be lazy-loaded
- Poor placeholders cause layout shift (CLS)
- Prevent unnecessary remounts
- Enable route-level code splitting
- Keep shared UI stable (header, sidebar)
- Load layouts eagerly
- Lazy-load only the content inside layouts
Rendering only visible items instead of entire lists.
- Large lists
- Tables
- Infinite scrolling
- Massive performance improvements
- Lower memory usage
Any context value change re-renders all consuming components.
- Split contexts by responsibility
- Avoid frequently changing values in context
- Memoize provider values
- Keep context scope minimal
Automatically optimizes React code at compile time.
- Reduces manual memoization
- Fewer unnecessary re-renders
- Simpler mental model
- Still evolving
- Does not replace good architectural decisions
- Does not eliminate the need for good component design
- Storing derived state in
useState - Memoizing everything blindly
- Putting frequently changing data in context
- Large components doing too much work
- Fix architecture before optimizing
- Avoid unnecessary state
- Measure before optimizing
- Prefer clarity, then performance
Performance anti-patterns are mistakes that look harmless but slowly degrade performance and maintainability. Most performance problems come from architecture decisions, not React itself.
const [fullName, setFullName] = useState('');- Causes extra re-renders
- Requires synchronization
- Leads to bugs and stale data
- Compute during render
- Memoize only if expensive
Using context for frequently changing values
- All consumers re-render
- Becomes a hidden global store
- Hard to debug
- Split contexts
- Keep values stable
- Prefer local state
Blindly using useMemo, useCallback, React.memo
- Adds complexity
- Often provides no real benefit
- Memoize only when profiling proves it helps
One component handling data, layout, and logic
- Split by responsibility
Updating state on every keystroke or scroll
- Debounce
- Throttle
- Use uncontrolled inputs where appropriate
If performance issues exist, remove state first — then optimize.
- Is state minimal and necessary?
- Is derived state computed, not stored?
- Are responsibilities clearly split?
- Are large lists virtualized?
- Are expensive computations memoized?
- Are unnecessary re-renders visible in DevTools?
- Is context scope minimal?
- Are fast-changing values kept out of context?
- Are provider values memoized?
- Are dependency arrays correct?
- Are effects cleaned up?
- Are effects doing too much work?
- Did you measure before optimizing?
- Are you fixing the slowest path first?
- ❌ No → Do nothing
- ✅ Yes → Continue
- ❌ No → Memoization won’t help
- ✅ Yes → Continue
- ❌ No → Memoization not needed
- ✅ Yes → Continue
- ❌ No → Fix data flow first
- ✅ Yes → Continue
- ✅ Yes → Reduce state
- ❌ No → Memoize
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter((i) => i.active));
}, [items]);- filtered is derived from items
- Extra state causes unnecessary re-renders
- Requires synchronization via useEffect
- Easy to introduce stale bugs
const filtered = items.filter((i) => i.active);Use this when filtering is cheap.
const filtered = useMemo(() => items.filter((i) => i.active), [items]);If a value can be computed from props or state, do not store it in state.
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click</button>;
});
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onClick={() => console.log('Child clicked')} />
</>
);
}Why Was Re-rendering
Even though Child is wrapped in React.memo, it still re-renders because:
<Child onClick={() => console.log('Child clicked')} />- A new function is created on every render
- React.memo does a shallow prop comparison
- New function reference ≠ previous one
- So Child re-renders every time count changes
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const handleChildClick = useCallback(() => {
console.log('Child clicked');
}, []);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onClick={handleChildClick} />
</>
);
}- Clicking Increment updates only App
- renders once
- Callback reference stays stable
- React.memo now works as intended
- React.memo only works if props are referentially stable.
- Use useCallback when passing functions to memoized children.