When agents use itUse this whenever the user mentions React being slow, laggy, "re-rendering too much", bundle size, Lighthouse scores, React DevTools Profiler, useMemo/useCallback/memo decisions, virtualisation, code splitting, Suspense, or React Compiler. Also use it proactively when reviewing React code that maps over large arrays, creates objects/functions inline in JSX passed to memoised children, or fetches in effects.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill react-performance -a github-copilot
# or with the org installer (adds .github/skills/react-performance):
npx -y github:AGCO-Global/org-skills add skill react-performance
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-react-skills plugin, which bundles all Frontend / React skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-react-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill react-performance -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill react-performance -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill react-performance -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill react-performance -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
React Performance
Performance work is measurement first, then the smallest fix that moves the number. Never recommend useMemo everywhere; recommend a profiler session.
1. Measure before touching code
Ask which of these the user actually has, and get a number:
Symptom
Tool
Metric to capture
Page loads slowly
Lighthouse / PageSpeed, WebPageTest
LCP, TTFB, total JS transferred
Interactions feel laggy
Chrome Performance panel, Web Vitals extension
INP, long tasks > 50 ms
"It re-renders too much"
React DevTools Profiler → "Record why each component rendered"
Commit count per interaction, render duration
Layout jumps
Lighthouse, Layout Shift regions in DevTools
CLS
Memory grows over time
DevTools Memory → heap snapshots between navigations
Write the baseline down. Every fix is judged against it.
2. Rendering: the re-render decision tree
React re-renders a component when its parent renders, its state changes, or a context it consumes changes. Work through in this order:
Is the re-render actually expensive? A component rendering in < 1 ms 50 times is fine. Fix only what the Profiler shows as a long commit.
Move state down. If a parent holds state only one child needs, move it into that child. Eliminates the parent render entirely.
Lift content up. Pass expensive static subtrees as children — React skips re-rendering identical children element references.
Split context. One AppContext with 15 values re-renders every consumer on any change. Split by change frequency, or move to Zustand/Jotai selectors.
Then, and only then, memoise.memo(Component) on the expensive leaf, and useMemo/useCallback on the props passed to it so the memo actually holds.
React Compiler. If the project is on React 19+, enable the compiler (babel-plugin-react-compiler); it auto-memoises and most manual useMemo/useCallback can be deleted. Verify with the Profiler that memoisation is happening.
Memoisation footguns to flag in review:
memo on a component that receives a new object/array/function prop every render — the memo never hits.
useMemo for a computation cheaper than the memo check itself (string concat, small filters).
Dependency arrays with objects recreated each render.
3. Lists
> ~100 rows rendered at once → virtualise.@tanstack/react-virtual (headless) or react-window. Fixed row height is far cheaper than dynamic; measure only if design requires it.
Stable keys from data ids, never array index for reorderable or filterable lists (index keys cause state to leak between rows and full remounts).
Row components wrapped in memo; row callbacks bound via data-* attributes and one delegated handler, or via useCallback with stable ids.
Paginate or infinite-scroll on the server; don't fetch 10 000 records to virtualise them client-side.
4. Loading and bundle size
Route-level code splitting is mandatory; component-level (lazy() + Suspense) for heavy widgets (charts, editors, maps).
Analyse the bundle: look for full-library imports (import _ from 'lodash' → lodash-es per-function), moment.js (→ date-fns/Temporal), duplicate React copies in monorepos, icon libraries pulling every icon.
Prefer platform APIs: Intl.NumberFormat/DateTimeFormat instead of formatting libraries.
Ship modern JS: set browserslist to real users; drop legacy polyfills.
Images: next/image or <img loading="lazy" decoding="async"> with explicit width/height, AVIF/WebP, responsive srcset. Images are the #1 LCP cause.
Fonts: font-display: swap or optional, preload the one critical font, subset it.
Third-party scripts: load with next/script strategy="lazyOnload" or requestIdleCallback; measure their long tasks separately — they are often the whole INP problem.
5. Server-side and data-fetching performance (Next.js / RSC)
Fetch in Server Components and stream with <Suspense> boundaries so the shell paints before slow data arrives.
Parallelise independent fetches with Promise.all; sequential awaits are the most common server waterfall.
Cache: fetch with next: { revalidate } or unstable_cache; tag-based invalidation on mutation.
Keep client components small — every "use client" boundary ships its whole subtree's JS.
generateStaticParams + ISR for content pages; dynamic rendering only for personalised routes.
6. Interaction responsiveness (INP)
Keep event handlers under 50 ms. Move heavy work to startTransition (non-urgent state updates) or a Web Worker (CPU-bound parsing, search indexing).
useDeferredValue for expensive derived UI driven by fast input (search-as-you-type).
Debounce network calls, not the UI update itself — the input must echo instantly.
Avoid layout thrash: batch DOM reads then writes; never read offsetHeight inside a loop that also mutates styles.
content-visibility: auto and contain: layout on long off-screen sections.
7. Memory leaks
Look for these when heap grows across navigations:
Subscriptions (addEventListener, WebSocket, setInterval, store subscribe) without cleanup in the effect return.
Closures capturing large data in long-lived callbacks (e.g. a global event bus).
Detached DOM kept alive by refs stored in module scope.
Query caches with no gcTime/staleTime for huge, rarely-revisited datasets.
8. Report format
When delivering a performance review, use this structure:
## Baseline
<metric>: <value> (tool, device, network)
## Findings (ordered by impact)
1. <problem> — <evidence from profiler/bundle> — est. impact
2. ...
## Fixes
For each finding: the minimal change, code diff, expected metric movement.
## Verify
Re-measure the same metric under the same conditions; attach before/after.
Anti-patterns to reject
"Wrap everything in useMemo/useCallback" — adds cost and hides the real cause.
React.memo on every component by policy.
Fetching in useEffect then rendering a spinner — waterfall; use a query library, loaders, or RSC.
Storing derived data in state and syncing with effects.
Inline style={{}} objects and arrow functions passed to memoised children.
Disabling StrictMode to "fix" double renders in dev.
Skipping measurement because "it's obviously the list".
---
name: react-performance
description: Diagnose and fix React performance problems — slow renders, re-render storms, large bundles, poor Core Web Vitals (LCP, INP, CLS), memory leaks, and janky lists. Use this whenever the user mentions React being slow, laggy, "re-rendering too much", bundle size, Lighthouse scores, React DevTools Profiler, useMemo/useCallback/memo decisions, virtualisation, code splitting, Suspense, or React Compiler. Also use it proactively when reviewing React code that maps over large arrays, creates objects/functions inline in JSX passed to memoised children, or fetches in effects.
metadata:
technology: React
type: performance
---
# React Performance
Performance work is measurement first, then the smallest fix that moves the number. Never recommend `useMemo` everywhere; recommend a profiler session.
## 1. Measure before touching code
Ask which of these the user actually has, and get a number:
| Symptom | Tool | Metric to capture |
|---|---|---|
| Page loads slowly | Lighthouse / PageSpeed, WebPageTest | LCP, TTFB, total JS transferred |
| Interactions feel laggy | Chrome Performance panel, Web Vitals extension | INP, long tasks > 50 ms |
| "It re-renders too much" | React DevTools Profiler → "Record why each component rendered" | Commit count per interaction, render duration |
| Layout jumps | Lighthouse, Layout Shift regions in DevTools | CLS |
| Memory grows over time | DevTools Memory → heap snapshots between navigations | Retained size of detached nodes / listeners |
| Bundle is big | `source-map-explorer`, `@next/bundle-analyzer`, `vite-bundle-visualizer` | Largest chunks, duplicated deps |
Write the baseline down. Every fix is judged against it.
## 2. Rendering: the re-render decision tree
React re-renders a component when its parent renders, its state changes, or a context it consumes changes. Work through in this order:
1. **Is the re-render actually expensive?** A component rendering in < 1 ms 50 times is fine. Fix only what the Profiler shows as a long commit.
2. **Move state down.** If a parent holds state only one child needs, move it into that child. Eliminates the parent render entirely.
3. **Lift content up.** Pass expensive static subtrees as `children` — React skips re-rendering identical `children` element references.
4. **Split context.** One `AppContext` with 15 values re-renders every consumer on any change. Split by change frequency, or move to Zustand/Jotai selectors.
5. **Then, and only then, memoise.** `memo(Component)` on the expensive leaf, and `useMemo`/`useCallback` on the props passed to it so the memo actually holds.
6. **React Compiler.** If the project is on React 19+, enable the compiler (`babel-plugin-react-compiler`); it auto-memoises and most manual `useMemo`/`useCallback` can be deleted. Verify with the Profiler that memoisation is happening.
Memoisation footguns to flag in review:
- `memo` on a component that receives a new object/array/function prop every render — the memo never hits.
- `useMemo` for a computation cheaper than the memo check itself (string concat, small filters).
- Dependency arrays with objects recreated each render.
## 3. Lists
- **> ~100 rows rendered at once → virtualise.** `@tanstack/react-virtual` (headless) or `react-window`. Fixed row height is far cheaper than dynamic; measure only if design requires it.
- Stable `key`s from data ids, never array index for reorderable or filterable lists (index keys cause state to leak between rows and full remounts).
- Row components wrapped in `memo`; row callbacks bound via `data-*` attributes and one delegated handler, or via `useCallback` with stable ids.
- Paginate or infinite-scroll on the server; don't fetch 10 000 records to virtualise them client-side.
## 4. Loading and bundle size
- **Route-level code splitting** is mandatory; component-level (`lazy()` + `Suspense`) for heavy widgets (charts, editors, maps).
- Analyse the bundle: look for full-library imports (`import _ from 'lodash'` → `lodash-es` per-function), moment.js (→ `date-fns`/Temporal), duplicate React copies in monorepos, icon libraries pulling every icon.
- Prefer platform APIs: `Intl.NumberFormat`/`DateTimeFormat` instead of formatting libraries.
- Ship modern JS: set `browserslist` to real users; drop legacy polyfills.
- Images: `next/image` or `<img loading="lazy" decoding="async">` with explicit `width`/`height`, AVIF/WebP, responsive `srcset`. Images are the #1 LCP cause.
- Fonts: `font-display: swap` or `optional`, preload the one critical font, subset it.
- Third-party scripts: load with `next/script strategy="lazyOnload"` or `requestIdleCallback`; measure their long tasks separately — they are often the whole INP problem.
## 5. Server-side and data-fetching performance (Next.js / RSC)
- Fetch in Server Components and stream with `<Suspense>` boundaries so the shell paints before slow data arrives.
- Parallelise independent fetches with `Promise.all`; sequential awaits are the most common server waterfall.
- Cache: `fetch` with `next: { revalidate }` or `unstable_cache`; tag-based invalidation on mutation.
- Keep client components small — every `"use client"` boundary ships its whole subtree's JS.
- `generateStaticParams` + ISR for content pages; dynamic rendering only for personalised routes.
## 6. Interaction responsiveness (INP)
- Keep event handlers under 50 ms. Move heavy work to `startTransition` (non-urgent state updates) or a Web Worker (CPU-bound parsing, search indexing).
- `useDeferredValue` for expensive derived UI driven by fast input (search-as-you-type).
- Debounce network calls, not the UI update itself — the input must echo instantly.
- Avoid layout thrash: batch DOM reads then writes; never read `offsetHeight` inside a loop that also mutates styles.
- `content-visibility: auto` and `contain: layout` on long off-screen sections.
## 7. Memory leaks
Look for these when heap grows across navigations:
- Subscriptions (`addEventListener`, WebSocket, `setInterval`, store `subscribe`) without cleanup in the effect return.
- Closures capturing large data in long-lived callbacks (e.g. a global event bus).
- Detached DOM kept alive by refs stored in module scope.
- Query caches with no `gcTime`/`staleTime` for huge, rarely-revisited datasets.
## 8. Report format
When delivering a performance review, use this structure:
```
## Baseline
<metric>: <value> (tool, device, network)
## Findings (ordered by impact)
1. <problem> — <evidence from profiler/bundle> — est. impact
2. ...
## Fixes
For each finding: the minimal change, code diff, expected metric movement.
## Verify
Re-measure the same metric under the same conditions; attach before/after.
```
## Anti-patterns to reject
- "Wrap everything in `useMemo`/`useCallback`" — adds cost and hides the real cause.
- `React.memo` on every component by policy.
- Fetching in `useEffect` then rendering a spinner — waterfall; use a query library, loaders, or RSC.
- Storing derived data in state and syncing with effects.
- Inline `style={{}}` objects and arrow functions passed to memoised children.
- Disabling StrictMode to "fix" double renders in dev.
- Skipping measurement because "it's obviously the list".