Performance Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
ANALYZE=true builds; remember the treemap shows uncompressed bytes, so cross-check against gzip/brotli transfer size in the Network panel before celebrating wins.index.ts files with export * silently defeat tree-shaking even on ES modules unless you set "sideEffects": false in package.json; prefer deep imports or mark packages side-effect-free so the bundler can prune unused code.LCP < 2.5s, INP < 200ms, CLS < 0.1) against a production build.Suspense boundary with its own async Server Component fetches in parallel automatically, so splitting siblings into separate boundaries gives you parallelism without writing a Promise.all - one top-level boundary collapses that back into a waterfall..map(async row => prisma.x.findUnique(...)) issues one query per row; collapse to a single JOIN via include/select, and prefer Promise.allSettled when one partial rejection should not discard every other result.sizes, the browser assumes 100vw and downloads the largest srcset variant (often 3840px) for a thumbnail; pass a breakpoint-aware string matching your layout and add priority only to the one LCP image so bandwidth is spent where it matters.<link> tags with next/font/google (or next/font/local) so fonts ship from the same origin with immutable cache headers and auto-generated fallback metrics that eliminate FOUT and CLS; variable fonts save about 100 KB over loading multiple weight files.useMemo, useCallback, and React.memo only after the profiler shows a real cost; prematurely memoizing cheap components adds comparison overhead that frequently exceeds the work it saves.children (or an inline object/function prop) rebuilds that value on every parent render, so React.memo never shortcuts - either hoist the children, use the children-pattern state wrapper, or accept that memo won't help there.useEffect that starts a fetch, adds a listener, sets a timer, opens a WebSocket, or initializes a chart library must return a cleanup; Strict Mode's double-mount exposes missing cleanup rather than causing it, and leaked intervals/listeners retain the entire component state via closure.AbortController and call abort() in the cleanup, so dependency changes or unmounts don't leave stale responses overwriting newer state or blocking GC of large JSON payloads.cookies()/headers() or cache: "no-store" call disables the Full Route Cache for the whole route, so isolate that code in its own Suspense child.unstable_cache(fn, keyParts, { tags, revalidate }) with unique descriptive key parts, and remember revalidatePath("/x") does not touch child routes unless you pass "layout".numberOfRuns: 3) and compare the median; set CI thresholds lower than your local production numbers to avoid flaky failures.NODE_ENV=production, use react-dom/profiling, close the Components tab during recording, and compare actualDuration vs baseDuration to see if memoization is actually paying off.children to a state-owning wrapper; children are created in the parent's scope, so the stateful wrapper can re-render without re-rendering the static tree - no memo needed.useSyncExternalStore so fine-grained subscribers only re-render when their specific slice moves.experimental.reactCompiler: true (plus babel-plugin-react-compiler) inserts finer-grained memoization than hand-written useMemo/useCallback/memo - adopt the compiler before ripping out manual memoization so you never leave a gap where nothing is memoized.Date.now()/Math.random() during render, or reads from mutable globals either stop firing or cache stale - use useSyncExternalStore for external mutable sources."use client" at a layout contaminates the entire subtree and adds 100+ KB of hydration JS; keep the directive at the smallest interactive leaf and pass Server Components through children into Client Component wrappers like <ThemeProvider>.fs, prisma, or other server-only modules inside a "use client" file.useStore(s => s) subscribes to the entire store, and useStore(s => ({ a: s.a, b: s.b })) creates a fresh object each update causing spurious re-renders; wrap in useShallow from zustand/react/shallow to compare properties instead of identity.performance.hints: "error", size-limit, or Next.js's analyze output diffed against main) so regressions fail the build instead of silently shipping; pair that with a Lighthouse CI check so both bytes and user-perceived metrics gate merges.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥