Org Skills

react-performance

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.

Download .zip Raw Source
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.

npx skills add AGCO-Global/org-skills --skill react-performance
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill react-performance -g

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 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 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".