React Performance Audit
An audit is only useful if every finding is measurable and ranked. Do not rewrite code until you have numbers.
Phase 1 — Baseline (do not skip)
- Build for production:
next buildorvite build. Record bundle sizes from the output (Next prints First Load JS per route). - If a browser is available, run Lighthouse or
npx unlighthouseagainst the two or three most important routes and record LCP, INP, CLS and TBT. - For a client-heavy app, open React DevTools Profiler, perform the slow interaction once, and note components with more than ~16 ms commit time or more than 5 re-renders per interaction.
- Save the numbers in
perf/baseline.mdso improvements can be proven.
Phase 2 — Static scan
Grep for the usual suspects and inspect each hit:
| Pattern | Command | Why it matters |
|---|---|---|
| Sequential awaits | rg "await .*\n\s*await" -U |
Waterfalls; use Promise.all |
| Barrel imports | rg "from ['\"]@/components['\"]" and lucide-react root imports |
Pulls whole library into the bundle |
useEffect fetching |
rg "useEffect\(.*fetch" -U |
Client waterfalls; move to server or a query library |
| Inline object/array props | rg "<\w+ [^>]*=\{\{" |
Breaks memoised children |
Missing key or index keys |
`rg "key={(i | index)}"` |
| Context providing large values | rg "createContext" then inspect |
Every consumer re-renders on any change |
| Heavy libs on client | rg "'use client'" -l then check imports of moment, lodash, chart libs |
Should be server-only or dynamically imported |
| Unbounded lists | JSX .map over data with no virtualisation and >200 items |
Layout and paint cost |
| Images without dimensions | rg "<img" |
CLS; use next/image or explicit width/height |
Also check next.config.* for optimizePackageImports, image formats and experimental.ppr, and check for React.StrictMode double-invocation confusion during profiling.