React Performance
React Performance
Section titled “React Performance”React is fast by default, but large apps can develop performance issues. The main cause: unnecessary re-renders. Understand what triggers re-renders before optimising.
What Causes Re-renders
Section titled “What Causes Re-renders”A component re-renders when:
- Its own state changes
- Its parent re-renders (even if props haven’t changed)
- A context it consumes changes
React.memo
Section titled “React.memo”Prevents re-render when parent re-renders but props haven’t changed:
const UserCard = React.memo(function UserCard({ user }: { user: User }) { return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> );});React.memo does a shallow comparison of props. Objects and arrays that are recreated on each render will still trigger re-renders — combine with useMemo and useCallback.
useMemo
Section titled “useMemo”Memoises the result of an expensive computation:
function ProductList({ products, filter }: Props) { const filteredProducts = useMemo( () => products.filter(p => p.category === filter), [products, filter] );
return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>;}Don’t reach for useMemo reflexively — profiling first is better.
useCallback
Section titled “useCallback”Memoises a function reference so it doesn’t change between renders:
function Parent() { const [count, setCount] = useState(0);
const handleClick = useCallback(() => { console.log('clicked'); }, []);
return <Child onClick={handleClick} />;}Code Splitting with React.lazy
Section titled “Code Splitting with React.lazy”Split your bundle so users only download the code they need:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));const Settings = lazy(() => import('./pages/Settings'));
function App() { return ( <Suspense fallback={<Spinner />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> );}Avoiding Re-renders in Context
Section titled “Avoiding Re-renders in Context”Context value changing triggers a re-render in every consumer.
Memoize the value:
function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light'); const toggleTheme = useCallback( () => setTheme(t => (t === 'light' ? 'dark' : 'light')), [] );
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;}Split contexts by update frequency:
const UserActionsContext = createContext({ logout: () => {} });const UserDataContext = createContext<User | null>(null);List Virtualisation
Section titled “List Virtualisation”Render only visible rows for very long lists:
npm install @tanstack/react-virtualimport { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) { const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, });
return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: virtualizer.getTotalSize() }}> {virtualizer.getVirtualItems().map(row => ( <div key={row.index} style={{ transform: `translateY(${row.start}px)`, position: 'absolute' }} > {items[row.index].name} </div> ))} </div> </div> );}Profiling with React DevTools
Section titled “Profiling with React DevTools”- Install React DevTools browser extension
- Open DevTools → Profiler tab
- Click record, interact with the app, stop recording
- Inspect flame graph to see which components re-render most
Key Rules
Section titled “Key Rules”- Profile before optimising — measure, don’t guess
React.memo+useCallback/useMemowork together- Lift state down: keep state close to where it’s used
- Lazy-load heavy routes and modals