---
name: optimise-react-performance
description: Reviews and speeds up a React or Next.js app by fixing the problems that matter most first - data-fetch waterfalls, large client bundles, too much client JavaScript, wasted re-renders and tangled component props. Use when a React or Next.js app needs to be faster, data fetches run in a waterfall, the bundle is big, components re-render too often, or the user asks for React best practices.
license: CC-BY-4.0
metadata:
  category: frontend
  tags: "react,nextjs,performance,quality"
  author: johnny-bravo
  version: "1.0"
  updated: "2026-10-31"
  status: published
  aliases: "react-best-practices,optimise-react-and-nextjs-performance,vercel-react-best-practices,react-composition-patterns"
---

# Optimise React and Next.js performance

Most React speed problems have the same few causes. This skill fixes them in order of impact: first waiting, then download size, then server work, then re-renders. Small tweaks such as `useMemo` come last, because they rarely matter.

## When to use
- Content appears in steps, one section after another.
- The JavaScript bundle is large, or Lighthouse flags unused JavaScript.
- Typing or clicking feels laggy because components re-render too often.
- Components have grown many boolean props and are hard to change.
- You are reviewing React code and want a checklist ordered by impact.

## Steps
1. **Measure first.** Build for production (`npm run build && npm start`). Note the routes that feel worst, the bundle sizes from the build output, and Lighthouse or Web Vitals numbers. Keep these to compare later.
2. **Remove waterfalls (biggest win).** A waterfall is a request that waits for another one it does not need.
   - Start independent requests together: `const [user, posts] = await Promise.all([getUser(id), getPosts(id)])`.
   - Start a request early and `await` it only where the value is used.
   - In Next.js App Router, fetch in server components and wrap the costly parts in `<Suspense fallback={...}>` so the rest streams first.
   - Do not fetch in a parent and again in a child. Fetch once, or dedupe with React `cache()`.
3. **Shrink the client bundle.**
   - Import from the exact module, not a big barrel file (`import Button from "./ui/button"`, not `from "./ui"`). For icon and utility libraries, Next.js `experimental.optimizePackageImports` helps.
   - Split out heavy, rarely used parts: `next/dynamic` or `React.lazy` for charts, editors, maps and modals.
   - Defer analytics and chat widgets with `next/script` `strategy="lazyOnload"`.
4. **Keep work on the server.**
   - Add `"use client"` only to the small interactive leaves, not to whole pages or layouts.
   - Pass client components only the fields they need. Every prop is sent to the browser.
   - Use `after()` in Next.js for logging or emails that should not delay the response.
   - Check auth inside every server action and route handler. They are public endpoints.
5. **Cut wasted re-renders.**
   - Compute values during render instead of copying props into state with `useEffect`.
   - Never define a component inside another component. It remounts on every render.
   - Use stable, unique `key`s from the data, not the array index, for lists that change.
   - Use the function form `setCount((c) => c + 1)` when the new state depends on the old one.
   - Wrap non-urgent updates in `useTransition`, and use `useDeferredValue` for search-as-you-type.
   - Virtualise lists with hundreds of rows (for example TanStack Virtual).
   - With the React Compiler turned on, skip hand-written `useMemo` and `useCallback` unless a profile shows a need.
6. **Simplify component design.**
   - Prefer `children` and small composed parts (`<Card><Card.Header/></Card>`) over many boolean props such as `isCompact` and `showFooter`.
   - Lift state only as high as needed. Use context for truly shared values, and split contexts that change at different speeds.
7. **Measure again.** Repeat step 1 with the same routes. Keep changes that helped and revert those that did not.

## Gotchas
- A `useEffect` that fetches data on the client after mount is often the hidden waterfall. Move it to the server or a data library.
- `React.memo` does nothing if a parent passes a new object or function on every render.
- Huge JSON props to a client component inflate the HTML and slow hydration.
- Do not optimise from guesses. React DevTools Profiler and the build output show the real cost.

## Safety
- Never pass secrets, API keys or other users' private data as props to client components. Anything a client component receives is visible in the browser.
- Keep server-only code in files marked with `import "server-only"`, so it cannot be imported into the client by mistake.

## Works with other stacks
The same order works with Remix, React Router, Vite + React or TanStack Start: remove waterfalls, split bundles, keep work on the server where possible, then fix re-renders. Only the API names change (loaders instead of server components, for example). For hydration mismatch errors, use `fix-react-hydration-errors`.

## Done when
- Every change is tied to a measurement, and the before and after numbers are written down.
- No independent requests run one after another on the main pages.
- The build passes, and no secrets or unneeded data reach client components.

## Credits
Written by TheSkillVault in our own words, CC BY 4.0. The topic list was informed by the public discussion around Vercel's React best-practices agent skill (vercel-labs/agent-skills). That repository has no licence file, so nothing from it was copied.

## Example prompt
"My Next.js dashboard takes four seconds to load and feels laggy. Find the biggest React performance problems and fix them, starting with the worst."
