CodingDeveloper skill

React Performance Pass

Reviews React/Next.js code for performance in bottleneck order: request waterfalls first, then re-renders, then bundle size.

What it does

When a web app feels slow, the usual response is to start optimising wherever the code looks suspicious. That often means a lot of small changes to how the screen redraws, while the real delay is somewhere else entirely - usually the app asking the server for one thing, waiting, then asking for the next.

With this skill active, the assistant works through the causes in the order they actually cost time. It starts by asking which kind of slow you mean, takes a measurement, and only then reads the code. Requests queued behind each other come first, then heavy work on every screen update, then unnecessary redraws, then the amount of code the browser has to download.

You get findings ranked by how many milliseconds each one is worth, with the file, the exact change, and whether the number was measured or estimated.

You end up with a ranked list of what is actually costing time, each with the file, the fix, and the measurement behind it.

When to reach for it

Use it when a page is slow to load, when typing or scrolling stutters, or before shipping a screen where speed matters. It is also worth running after adding data fetching, a large table, or a heavy third-party component.

It will not help if the delay is in your database or your API rather than the app itself - it will tell you that is the case, but the fix then lives on the server side.

What's inside

react-performance-pass/
├── SKILL.md
└── references/
    └── examples.md
SKILL.md
---
name: react-performance-pass
description: Reviews a React or Next.js codebase for performance in bottleneck order - request waterfalls first, then work that blocks the main thread, then wasted re-renders, then bundle and asset weight - and returns findings ranked by how many milliseconds each one actually costs. Use when a page feels slow, when typing or scrolling stutters, or before shipping a performance-sensitive route.
---

# React Performance Pass

You are the reviewer who refuses to memoise anything until the waterfall is gone. Effort goes where the milliseconds are, and the ordering of the findings is most of the value.

## Workflow

1. **Ask what "slow" means.** Slow to appear, slow to respond, or slow to scroll. These have three different causes and almost no overlap.
2. **Establish the setup.** App Router, Pages Router, or a client-only SPA. Which components are server components. Where data is fetched.
3. **Get or take a measurement.** A network trace, a React Profiler recording, or a Lighthouse run. Without one you are guessing, and guesses default to memoisation.
4. **Walk the bottleneck ladder in order** (below). Stop descending once you have found something that plausibly accounts for the reported delay.
5. **Rank findings by cost**, with the evidence next to each.
6. **Fix one thing, re-measure.** Stacked changes hide which one worked.

```text
Performance pass checklist
[ ] Symptom classified: slow load / slow input / slow scroll
[ ] Router model and server-vs-client boundary understood
[ ] A real measurement in hand before any code changes
[ ] Data fetching checked for sequential awaits
[ ] Main-thread work checked before re-renders
[ ] Re-render causes traced to a source, not assumed
[ ] Bundle checked with an analyser, not by eye
[ ] Findings ordered by measured cost, each with a fix
[ ] One change made, then re-measured
```

## The bottleneck ladder

### 1. Request waterfalls - usually the biggest win

Sequential awaits, or a component that fetches after its parent has already fetched. Each hop costs a full round-trip before anything renders.

Look for: `await` on one line, another `await` using the first result on the next; a child component with its own `useEffect` fetch; a layout that fetches, then a page that fetches.

Fix: run independent requests with `Promise.all`. Move fetching to the server component or route loader. Where a genuine dependency exists, start the independent half immediately and await both at the end. Stream what you can render early.

One waterfall fix routinely beats every render optimisation in the file combined. Say so in the report.

### 2. Client fetching that should be server-side

Data fetched in a `useEffect` after hydration costs the user a second round-trip they can see. If the server could have fetched it at request time, it should have.

Fix: move the fetch into a server component, or a loader, and keep client components for interactivity. Watch for the opposite bug too: a page rendered on the server that refetches the same data on the client anyway.

### 3. Main-thread work

Before blaming re-render counts, check what each render *does*. Sorting 10,000 rows, parsing dates, running a syntax highlighter, or building a large derived object on every keystroke will stutter at any render count.

Fix: memoise the computation with `useMemo`, move it out of render, do it once at the data layer, or push it to a worker. Virtualise long lists rather than rendering thousands of nodes.

### 4. Wasted re-renders

Now look at what renders and why. The React Profiler names the trigger; use it rather than guessing.

Common causes, in the order they are usually the answer:

- State living too high, so a keystroke re-renders a page.
- A context whose value is a new object every render, waking every consumer.
- New object, array, or function props created inline, breaking every child's identity check.
- A parent re-rendering for an unrelated reason and dragging expensive children with it.

Fix in the same order: move state down or colocate it, split the context, stabilise the value, and only then reach for `memo`, `useMemo` or `useCallback`. Memoisation is the last step because it hides the cause rather than removing it, and it has its own cost.

### 5. Bundle and hydration weight

Measure with the bundle analyser before touching anything. Look for one dependency dominating the client bundle - a chart library, a date library, an editor, an icon set imported wholesale.

Fix: dynamic-import heavy client-only widgets, keep components on the server where possible, import single icons rather than the barrel file, replace a large utility library with a smaller one or a few lines of your own.

### 6. Images, fonts and layout shift

Right-size and lazy-load images, use the framework's image primitive, always set dimensions, subset fonts and set `font-display`. These rarely cause the complaint but they are cheap and they move the visible metrics.

## Output format

Ordered by impact, highest first. Each finding:

- **Impact** - High / Medium / Low, with the cost in milliseconds or kilobytes where you have it.
- **Where** - file and symbol.
- **Problem** - the specific pattern, in one or two sentences.
- **Fix** - the change, with a short before-and-after where it clarifies.
- **Confidence** - measured, or inferred from reading the code.

A flat unordered list wastes the reader's time. If two findings are close, say which to do first and why.

## Example

**High impact - `app/dashboard/page.tsx`, measured 1.9s to first byte.**

Problem: three sequential awaits before anything renders.

```ts
const user = await getUser();                 // 180ms
const team = await getTeam(user.teamId);      // 210ms
const projects = await getProjects(team.id);  // 240ms  -> 630ms serial
```

Fix: `getTeam` and `getProjects` both depend on `user`, but not on each other. Once the user is loaded they can run together.

```ts
const user = await getUser();                                    // 180ms
const [team, projects] = await Promise.all([                     // 240ms
  getTeam(user.teamId),
  getProjectsByTeamId(user.teamId),
]);
```

Saves roughly 210ms on every dashboard load. `getProjectsByTeamId` already exists in `lib/projects.ts`; the original chain was passing `team.id`, which equals `user.teamId`, so no new query is needed.

**Confidence:** measured, server timing headers on ten loads.

## Common false wins

- Wrapping everything in `memo` while a three-request waterfall upstream costs 600ms.
- Adding `useCallback` to a prop that was never the reason a child re-rendered.
- Debouncing an input to hide a re-render storm instead of moving the state down.
- Code-splitting a 4KB component while a 300KB dependency still ships on every page.
- Rendering a 5,000-row list unvirtualised and blaming React for the scroll jank.
- Server-rendering a page that refetches the same data on the client immediately after hydration.

Each of these is motion without movement. Confirm the bottleneck, then fix it.

## Rules

- Measure, or point to the evidence, before calling anything the bottleneck.
- Never lead with `memo`, `useMemo` or `useCallback`. They are the last step of stage 4, not the first thing you try.
- One change at a time, then re-measure. Two changes make the result unreadable.
- If a fix changes behaviour - stale data, different loading order, a lost error state - flag the trade-off rather than burying it.
- Do not report a finding you cannot tie to a file and a symbol.
- Say when a finding is inferred rather than measured. An honest "probably" beats a confident number you did not observe.

## Edge cases

- **No measurement available and none possible.** Review statically, order findings by how much they usually cost, and label the whole report as unmeasured.
- **The app is fast and the user is not.** Sometimes the slow part is the API, the database, or the network, not React. Say so and point at where the time goes rather than optimising the front end for a backend problem.
- **Slowness only in development.** Development builds re-render more, ship unminified code, and run extra checks. Reproduce against a production build before investigating.
- **A single user is slow.** Suspect their data volume, an extension, or an old device before changing code that is fine for everyone else.
- **The framework version matters.** Compiler-driven auto-memoisation and streaming behaviour differ between versions. Check what the project is on before recommending a pattern it does not support.
- **The real fix is architectural.** If the page needs a different data model, say that plainly and offer the cheap partial win separately, with its smaller number attached.

## Files

- `references/examples.md` - three full reviews with the code, the measurement, the fix and the result: a Next.js waterfall, a re-render storm traced to a context value, and a bundle dominated by one dependency. Read it for the shape of a finding and for how much evidence to attach.

Install it

Claude Code (this project)

Unzip the download into .claude/skills/. The folder is already named right.

Claude Code (every project)

Unzip into ~/.claude/skills/ to use it everywhere.

claude.ai

Upload the zip under Settings, then Capabilities, then Skills (paid plans).

How to call it

Just describe the task. The description line inside the skill does the matching. Or call it directly:

/react-performance-pass this page feels slow, find out whytyping lags in this search boxwhy is our first load so heavyreview this React app for performancecheck our data fetching for waterfalls

Works beyond Claude

Skills use the open Agent Skills format. The exact same folder works in OpenAI Codex, Gemini CLI, Cursor, GitHub Copilot and other tools that read the format.

Using a plain chat app? Paste SKILL.md into ChatGPT or Gemini as instructions. You lose auto-triggering, but the method still works.

agentskills.io

Pattern credit: vercel-labs/agent-skills - react-best-practices

Related skills