# React Performance Audit

> Audit React and Next.js codebases for wasted re-renders, bundle bloat, and Core Web Vitals regressions, then produce a prioritized fix list with measured impact. Use this skill whenever the user mentions slow renders, laggy typing or scrolling, large bundles, LCP/INP/CLS problems, Lighthouse scores, React DevTools Profiler output, memo/useMemo/useCallback decisions, or says something is "janky", "slow", or "re-rendering too much" — including when they just paste a component and ask why it feels slow.

- Skill: `jayeshsojitra103/react-performance-audit` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add jayeshsojitra103/react-performance-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jayeshsojitra103/react-performance-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jayeshsojitra103 (https://skillmd.com/u/jayeshsojitra103)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jayeshsojitra103/react-performance-audit

---


# React Performance Audit

Performance work goes wrong in two directions: optimizing what was never slow, and
sprinkling `useMemo` until the code is unreadable and no faster. This skill enforces the
measure → diagnose → fix → verify loop, and refuses to skip the first step.

## Step 0 — Establish what "slow" means here

Before reading any code, get the symptom and the number. Ask which of these it is, because
each has a different root cause family:

- **Slow initial load** → bundle size, server response, render-blocking resources → LCP
- **Slow interaction / laggy typing** → re-render cost, long tasks on the main thread → INP
- **Layout shifting while loading** → missing dimensions, late-injected content → CLS
- **Slow list scrolling** → per-row render cost, missing virtualization
- **Slow navigation between routes** → unsplit bundles, waterfall data fetching

If the user has no measurement, provide one before diagnosing: a Lighthouse run, a React
DevTools Profiler recording, or `npx source-map-explorer` on the build output. An audit
without a baseline cannot demonstrate improvement and usually optimizes the wrong thing.

## Step 1 — Static scan

Run the bundled scanner over the source to collect candidate issues fast:

```bash
node scripts/perf_scan.mjs <src-dir>
```

It flags high-signal patterns: inline object and arrow props passed to memoized children,
array index keys, `useEffect` with a missing or over-broad dependency array, state that
should be derived, context values constructed inline, heavy top-level imports, and
`useState` holding values that never trigger a render.

Treat its output as leads, not verdicts. The scanner cannot see how often a component
renders or how expensive that render is, and a flagged pattern in a component that renders
twice a session is not a problem worth a PR.

## Step 2 — Diagnose against the real cost model

React re-renders a component when its state changes, its context value changes, or its
parent re-renders. The last one is the source of most confusion — a parent re-render
re-renders all children regardless of whether their props changed, unless the child is
memoized *and* its props are referentially stable.

Work through these in order of typical impact:

**Context that changes too often.** A context whose value is a fresh object every render
invalidates every consumer in the tree. Split contexts by update frequency — a stable
`dispatch`-like context and a volatile `state` context — and memoize the value. This single
fix routinely removes more wasted renders than every `React.memo` in a codebase.

**Uncontrolled subtree re-renders.** Lifting state higher than needed makes the whole tree
re-render on every keystroke. Push state down to the smallest component that owns it, or
move the expensive siblings into `children` so they keep the same element identity across
parent renders.

**Memoization that does nothing.** `React.memo` on a component receiving `style={{...}}`
or `onClick={() => ...}` compares a new reference every time and always re-renders, having
added a comparison cost for no benefit. Either stabilize the props or drop the memo.

**Over-memoization.** `useMemo` on a cheap computation costs a dependency comparison plus
retained memory to save a few microseconds. Reserve it for expensive derivations, large
list transforms, and referential stability that something downstream actually depends on.
With the React Compiler enabled, most manual memoization becomes redundant — check for it
before adding more.

**Expensive renders rather than frequent ones.** Long lists without virtualization, chart
libraries rendering thousands of DOM nodes, date formatting inside a map. Fix the cost per
render, not the count.

**Bundle weight.** Check for moment, full lodash, unshaken icon packs, and duplicated
transitive dependencies. Route-level `React.lazy` and dynamic `import()` for anything below
the fold; `next/dynamic` with `ssr: false` for browser-only widgets.

**Data waterfalls.** A component fetching, then rendering a child that fetches, serializes
round trips. Hoist to a parallel fetch, or in the App Router move the fetch into a Server
Component and stream with Suspense.

## Step 3 — Report

Use this structure so the reader can act without reading everything:

```markdown
## Summary
One paragraph: what's slow, why, expected gain from the top three fixes.

## Baseline
| Metric | Current | Target |
|---|---|---|
| LCP / INP / CLS / bundle (gzip) | | |

## Findings
### 1. <Title> — Impact: High | Effort: Low
**Where:** `src/path/File.tsx:42`
**What happens:** the mechanism, in terms of React's render model
**Evidence:** profiler number, bundle bytes, or scanner hit
**Fix:** minimal diff
**Expected gain:** quantified, with the assumption stated

## Not worth fixing
Patterns that look wrong but cost nothing here — with the reason. This section keeps the
team from "fixing" things later and reintroducing complexity.

## Verification
The exact command or profiling step that confirms each fix landed.
```

Order findings by impact-to-effort, never by file order. Cap the list at the eight that
matter; a 40-item audit is ignored.

## Step 4 — Verify and prevent

Every fix needs a before/after number from the same measurement used in Step 0. Then close
the loop so the regression cannot silently return:

- Bundle budget in CI (`size-limit` or `@next/bundle-analyzer` with a threshold that fails)
- Lighthouse CI on preview deployments with asserted budgets
- Real-user monitoring via `web-vitals` reporting INP and LCP at p75, since lab numbers on
  fast hardware hide the problem that users actually have

## Anti-patterns to avoid in your own recommendations

Do not recommend wrapping everything in `memo`, do not suggest `useCallback` for handlers
passed to plain DOM elements, and do not present a micro-optimization as a headline fix
when a 400 KB dependency is sitting in the entry chunk. Credibility in a performance review
comes from correct prioritization more than from knowing obscure APIs.

For the mechanics of profiling — Profiler flame chart reading, `why-did-you-render` setup,
Chrome performance traces, and INP attribution — see `references/measuring.md`.

