# React Performance Optimization Guide ## Overview This guide covers performance optimization strategies for React 19 applications. ## Measurement & Profiling ### React DevTools Profiler Record performance data: 1. Open React DevTools 2. Go to Profiler tab 3. Click record button 4. Interact with app 5. Stop recording 6. Analyze flame graph and ranked chart ### Profiler Component ```typescript import { Profiler } from 'react' const App = () => { const onRender = ( id: string, phase: 'mount' | 'update', actualDuration: number, baseDuration: number, startTime: number, commitTime: number ) => { console.log({ component: id, phase, actualDuration, // Time spent rendering this update baseDuration // Estimated time without memoization }) } return ( ) } ``` ### Performance Metrics ```typescript // Custom performance tracking const startTime = performance.now() // ... do work const endTime = performance.now() console.log(`Operation took ${endTime - startTime}ms`) // React rendering metrics import { unstable_trace as trace } from 'react' trace('expensive-operation', async () => { await performExpensiveOperation() }) ``` ## Memoization Strategies ### React.memo Prevent unnecessary re-renders: ```typescript // Basic memoization const ExpensiveComponent = memo(({ data }: Props) => { return
{processData(data)}
}) // Custom comparison const MemoizedComponent = memo( ({ user }: Props) => , (prevProps, nextProps) => { // Return true if props are equal (skip render) return prevProps.user.id === nextProps.user.id } ) ``` **When to use:** - Component renders often with same props - Rendering is expensive - Component receives complex prop objects **When NOT to use:** - Props change frequently - Component is already fast - Premature optimization ### useMemo Memoize computed values: ```typescript const SortedList = ({ items, filter }: Props) => { // Without memoization - runs every render const filteredItems = items.filter(item => item.type === filter) const sortedItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name)) // With memoization - only runs when dependencies change const sortedFilteredItems = useMemo(() => { const filtered = items.filter(item => item.type === filter) return filtered.sort((a, b) => a.name.localeCompare(b.name)) }, [items, filter]) return ( ) } ``` **When to use:** - Expensive calculations (sorting, filtering large arrays) - Creating stable object references - Computed values used as dependencies ### useCallback Memoize callback functions: ```typescript const Parent = () => { const [count, setCount] = useState(0) // Without useCallback - new function every render const handleClick = () => { setCount(c => c + 1) } // With useCallback - stable function reference const handleClickMemo = useCallback(() => { setCount(c => c + 1) }, []) return } const MemoizedChild = memo(({ onClick }: Props) => { return }) ``` **When to use:** - Passing callbacks to memoized components - Callback is used in dependency array - Callback is expensive to create ## React Compiler (Automatic Optimization) ### Enable React Compiler React 19 can automatically optimize without manual memoization: ```javascript // babel.config.js module.exports = { plugins: [ ['react-compiler', { compilationMode: 'all', // Optimize all components }] ] } ``` ### Compilation Modes ```javascript { compilationMode: 'annotation', // Only components with "use memo" compilationMode: 'all', // All components (recommended) compilationMode: 'infer' // Based on component complexity } ``` ### Directives ```typescript // Force memoization 'use memo' const Component = ({ data }: Props) => { return
{data}
} // Prevent memoization 'use no memo' const SimpleComponent = ({ text }: Props) => { return {text} } ``` ## State Management Optimization ### State Colocation Keep state as close as possible to where it's used: ```typescript // Bad - state too high const App = () => { const [showModal, setShowModal] = useState(false) return ( <>
setShowModal(false)} /> ) } // Good - state colocated const App = () => { return ( <>
) } const ModalContainer = () => { const [showModal, setShowModal] = useState(false) return setShowModal(false)} /> } ``` ### Split Context Avoid unnecessary re-renders by splitting context: ```typescript // Bad - single context causes all consumers to re-render const AppContext = createContext({ user, theme, settings }) // Good - split into separate contexts const UserContext = createContext(user) const ThemeContext = createContext(theme) const SettingsContext = createContext(settings) ``` ### Context with useMemo ```typescript const ThemeProvider = ({ children }: Props) => { const [theme, setTheme] = useState('light') // Memoize context value to prevent unnecessary re-renders const value = useMemo(() => ({ theme, setTheme }), [theme]) return ( {children} ) } ``` ## Code Splitting & Lazy Loading ### React.lazy Split components into separate bundles: ```typescript import { lazy, Suspense } from 'react' // Lazy load components const Dashboard = lazy(() => import('./Dashboard')) const Settings = lazy(() => import('./Settings')) const Profile = lazy(() => import('./Profile')) const App = () => { return ( }> } /> } /> } /> ) } ``` ### Route-based Splitting ```typescript // App.tsx const routes = [ { path: '/', component: lazy(() => import('./pages/Home')) }, { path: '/about', component: lazy(() => import('./pages/About')) }, { path: '/products', component: lazy(() => import('./pages/Products')) }, ] const App = () => ( }> {routes.map(({ path, component: Component }) => ( } /> ))} ) ``` ### Component-based Splitting ```typescript // Split expensive components const HeavyChart = lazy(() => import('./HeavyChart')) const Dashboard = () => { const [showChart, setShowChart] = useState(false) return ( <> {showChart && ( }> )} ) } ``` ## List Rendering Optimization ### Keys Always use stable, unique keys: ```typescript // Bad - index as key (causes issues on reorder/insert) {items.map((item, index) => ( ))} // Good - unique ID as key {items.map(item => ( ))} // For static lists without IDs {items.map(item => ( ))} ``` ### Virtualization For long lists, render only visible items: ```typescript import { useVirtualizer } from '@tanstack/react-virtual' const VirtualList = ({ items }: { items: Item[] }) => { const parentRef = useRef(null) const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // Estimated item height overscan: 5 // Render 5 extra items above/below viewport }) return (
{virtualizer.getVirtualItems().map(virtualItem => (
))}
) } ``` ### Pagination ```typescript const PaginatedList = ({ items }: Props) => { const [page, setPage] = useState(1) const itemsPerPage = 20 const paginatedItems = useMemo(() => { const start = (page - 1) * itemsPerPage const end = start + itemsPerPage return items.slice(start, end) }, [items, page, itemsPerPage]) return ( <> {paginatedItems.map(item => ( ))} ) } ``` ## Transitions & Concurrent Features ### useTransition Keep UI responsive during expensive updates: ```typescript const SearchPage = () => { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [isPending, startTransition] = useTransition() const handleSearch = (value: string) => { setQuery(value) // Urgent - update input immediately // Non-urgent - can be interrupted startTransition(() => { const filtered = expensiveFilter(items, value) setResults(filtered) }) } return ( <> handleSearch(e.target.value)} /> {isPending && } ) } ``` ### useDeferredValue Defer non-urgent renders: ```typescript const SearchPage = () => { const [query, setQuery] = useState('') const deferredQuery = useDeferredValue(query) // Input updates immediately // Results update with deferred value (can be interrupted) const results = useMemo(() => { return expensiveFilter(items, deferredQuery) }, [deferredQuery]) return ( <> setQuery(e.target.value)} /> ) } ``` ## Image & Asset Optimization ### Lazy Load Images ```typescript const LazyImage = ({ src, alt }: Props) => { const [isLoaded, setIsLoaded] = useState(false) return (
{!isLoaded && } {alt} setIsLoaded(true)} className={isLoaded ? 'opacity-100' : 'opacity-0'} />
) } ``` ### Next.js Image Component ```typescript import Image from 'next/image' const OptimizedImage = () => ( Hero ) ``` ## Bundle Size Optimization ### Tree Shaking Import only what you need: ```typescript // Bad - imports entire library import _ from 'lodash' // Good - import only needed functions import debounce from 'lodash/debounce' import throttle from 'lodash/throttle' // Even better - use native methods when possible const debounce = (fn, delay) => { let timeoutId return (...args) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn(...args), delay) } } ``` ### Analyze Bundle ```bash # Next.js ANALYZE=true npm run build # Create React App npm install --save-dev webpack-bundle-analyzer ``` ### Dynamic Imports ```typescript // Load library only when needed const handleExport = async () => { const { jsPDF } = await import('jspdf') const doc = new jsPDF() doc.save('report.pdf') } ``` ## Common Performance Pitfalls ### 1. Inline Object Creation ```typescript // Bad - new object every render // Good - stable reference const style = { margin: 10 } // Or use useMemo const style = useMemo(() => ({ margin: 10 }), []) ``` ### 2. Inline Functions ```typescript // Bad - new function every render (if child is memoized) handleClick(id)} /> // Good const handleClickMemo = useCallback(() => handleClick(id), [id]) ``` ### 3. Spreading Props ```typescript // Bad - causes re-renders even when props unchanged // Good - pass only needed props ``` ### 4. Large Context ```typescript // Bad - everything re-renders on any state change const AppContext = createContext({ user, theme, cart, settings, ... }) // Good - split into focused contexts const UserContext = createContext(user) const ThemeContext = createContext(theme) const CartContext = createContext(cart) ``` ## Performance Checklist - [ ] Measure before optimizing (use Profiler) - [ ] Use React DevTools to identify slow components - [ ] Implement code splitting for large routes - [ ] Lazy load below-the-fold content - [ ] Virtualize long lists - [ ] Memoize expensive calculations - [ ] Split large contexts - [ ] Colocate state close to usage - [ ] Use transitions for non-urgent updates - [ ] Optimize images and assets - [ ] Analyze and minimize bundle size - [ ] Remove console.logs in production - [ ] Use production build for testing - [ ] Monitor real-world performance metrics ## References - React Performance: https://react.dev/learn/render-and-commit - React Profiler: https://react.dev/reference/react/Profiler - React Compiler: https://react.dev/reference/react-compiler - Web Vitals: https://web.dev/vitals/