From 9aa71a1a97f1f050dac839b595e4a5147e907f06 Mon Sep 17 00:00:00 2001 From: Riku Inada Date: Fri, 16 Jan 2026 20:32:35 +0900 Subject: [PATCH 1/2] feat: add react-best-practices power - Add POWER.md with overview and quick reference - Add 45 individual rule files in steering/ - Add full-guide.md (compiled AGENTS.md) - Update README.md Source: https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices License: MIT --- README.md | 7 + react-best-practices/POWER.md | 153 ++ .../steering/advanced-event-handler-refs.md | 55 + .../steering/advanced-use-latest.md | 49 + .../steering/async-api-routes.md | 38 + .../steering/async-defer-await.md | 80 + .../steering/async-dependencies.md | 36 + .../steering/async-parallel.md | 28 + .../steering/async-suspense-boundaries.md | 99 + .../steering/bundle-barrel-imports.md | 59 + .../steering/bundle-conditional.md | 31 + .../steering/bundle-defer-third-party.md | 49 + .../steering/bundle-dynamic-imports.md | 35 + .../steering/bundle-preload.md | 50 + .../steering/client-event-listeners.md | 74 + .../steering/client-swr-dedup.md | 56 + react-best-practices/steering/full-guide.md | 2249 +++++++++++++++++ .../steering/js-batch-dom-css.md | 82 + .../steering/js-cache-function-results.md | 80 + .../steering/js-cache-property-access.md | 28 + .../steering/js-cache-storage.md | 70 + .../steering/js-combine-iterations.md | 32 + .../steering/js-early-exit.md | 50 + .../steering/js-hoist-regexp.md | 45 + .../steering/js-index-maps.md | 37 + .../steering/js-length-check-first.md | 49 + .../steering/js-min-max-loop.md | 82 + .../steering/js-set-map-lookups.md | 24 + .../steering/js-tosorted-immutable.md | 57 + .../steering/rendering-activity.md | 26 + .../steering/rendering-animate-svg-wrapper.md | 47 + .../steering/rendering-conditional-render.md | 40 + .../steering/rendering-content-visibility.md | 38 + .../steering/rendering-hoist-jsx.md | 46 + .../rendering-hydration-no-flicker.md | 82 + .../steering/rendering-svg-precision.md | 28 + .../steering/rerender-defer-reads.md | 39 + .../steering/rerender-dependencies.md | 45 + .../steering/rerender-derived-state.md | 29 + .../steering/rerender-functional-setstate.md | 74 + .../steering/rerender-lazy-state-init.md | 58 + .../steering/rerender-memo.md | 44 + .../steering/rerender-transitions.md | 40 + .../steering/server-after-nonblocking.md | 73 + .../steering/server-cache-lru.md | 41 + .../steering/server-cache-react.md | 26 + .../steering/server-parallel-fetching.md | 79 + .../steering/server-serialization.md | 38 + 48 files changed, 4677 insertions(+) create mode 100644 react-best-practices/POWER.md create mode 100644 react-best-practices/steering/advanced-event-handler-refs.md create mode 100644 react-best-practices/steering/advanced-use-latest.md create mode 100644 react-best-practices/steering/async-api-routes.md create mode 100644 react-best-practices/steering/async-defer-await.md create mode 100644 react-best-practices/steering/async-dependencies.md create mode 100644 react-best-practices/steering/async-parallel.md create mode 100644 react-best-practices/steering/async-suspense-boundaries.md create mode 100644 react-best-practices/steering/bundle-barrel-imports.md create mode 100644 react-best-practices/steering/bundle-conditional.md create mode 100644 react-best-practices/steering/bundle-defer-third-party.md create mode 100644 react-best-practices/steering/bundle-dynamic-imports.md create mode 100644 react-best-practices/steering/bundle-preload.md create mode 100644 react-best-practices/steering/client-event-listeners.md create mode 100644 react-best-practices/steering/client-swr-dedup.md create mode 100644 react-best-practices/steering/full-guide.md create mode 100644 react-best-practices/steering/js-batch-dom-css.md create mode 100644 react-best-practices/steering/js-cache-function-results.md create mode 100644 react-best-practices/steering/js-cache-property-access.md create mode 100644 react-best-practices/steering/js-cache-storage.md create mode 100644 react-best-practices/steering/js-combine-iterations.md create mode 100644 react-best-practices/steering/js-early-exit.md create mode 100644 react-best-practices/steering/js-hoist-regexp.md create mode 100644 react-best-practices/steering/js-index-maps.md create mode 100644 react-best-practices/steering/js-length-check-first.md create mode 100644 react-best-practices/steering/js-min-max-loop.md create mode 100644 react-best-practices/steering/js-set-map-lookups.md create mode 100644 react-best-practices/steering/js-tosorted-immutable.md create mode 100644 react-best-practices/steering/rendering-activity.md create mode 100644 react-best-practices/steering/rendering-animate-svg-wrapper.md create mode 100644 react-best-practices/steering/rendering-conditional-render.md create mode 100644 react-best-practices/steering/rendering-content-visibility.md create mode 100644 react-best-practices/steering/rendering-hoist-jsx.md create mode 100644 react-best-practices/steering/rendering-hydration-no-flicker.md create mode 100644 react-best-practices/steering/rendering-svg-precision.md create mode 100644 react-best-practices/steering/rerender-defer-reads.md create mode 100644 react-best-practices/steering/rerender-dependencies.md create mode 100644 react-best-practices/steering/rerender-derived-state.md create mode 100644 react-best-practices/steering/rerender-functional-setstate.md create mode 100644 react-best-practices/steering/rerender-lazy-state-init.md create mode 100644 react-best-practices/steering/rerender-memo.md create mode 100644 react-best-practices/steering/rerender-transitions.md create mode 100644 react-best-practices/steering/server-after-nonblocking.md create mode 100644 react-best-practices/steering/server-cache-lru.md create mode 100644 react-best-practices/steering/server-cache-react.md create mode 100644 react-best-practices/steering/server-parallel-fetching.md create mode 100644 react-best-practices/steering/server-serialization.md diff --git a/README.md b/README.md index 3857e72..c31ffc1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,13 @@ Documentation is available at https://kiro.dev/docs/powers/ --- +### react-best-practices +**React Best Practices** - React and Next.js performance optimization guidelines from Vercel Engineering. 45 rules across 8 categories, prioritized by impact from CRITICAL to LOW. + +**MCP Servers:** None (Knowledge Base Power) + +--- + ### saas-builder **SaaS Builder** - Build production-ready multi-tenant SaaS applications with serverless architecture, integrated billing, and enterprise-grade security. diff --git a/react-best-practices/POWER.md b/react-best-practices/POWER.md new file mode 100644 index 0000000..7d29293 --- /dev/null +++ b/react-best-practices/POWER.md @@ -0,0 +1,153 @@ +--- +name: "react-best-practices" +displayName: "React Best Practices" +description: "React and Next.js performance optimization guidelines from Vercel Engineering. 45 rules across 8 categories, prioritized by impact from CRITICAL to LOW." +keywords: ["react", "nextjs", "performance", "optimization", "bundle", "waterfall", "re-render", "ssr", "vercel"] +author: "Vercel" +--- + +# React Best Practices + +## Overview + +Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel Engineering. Contains 45 rules across 8 categories, prioritized by impact to guide code review, refactoring, and code generation. + +**Core Principle:** Performance work should start at the top of the stack. If a request waterfall adds 600ms of waiting time, optimizing `useMemo` calls won't help. Fix waterfalls first, then bundle size, then work down the priority list. + +## When to Apply + +Reference these guidelines when: +- Writing new React components or Next.js pages +- Implementing data fetching (client or server-side) +- Reviewing code for performance issues +- Refactoring existing React/Next.js code +- Optimizing bundle size or load times + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | Rules | +|----------|----------|--------|--------|-------| +| 1 | Eliminating Waterfalls | CRITICAL | `async-` | 5 | +| 2 | Bundle Size Optimization | CRITICAL | `bundle-` | 5 | +| 3 | Server-Side Performance | HIGH | `server-` | 5 | +| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` | 2 | +| 5 | Re-render Optimization | MEDIUM | `rerender-` | 7 | +| 6 | Rendering Performance | MEDIUM | `rendering-` | 7 | +| 7 | JavaScript Performance | LOW-MEDIUM | `js-` | 12 | +| 8 | Advanced Patterns | LOW | `advanced-` | 2 | + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +steering/async-parallel.md +steering/bundle-barrel-imports.md +``` + +For the complete guide with all rules expanded: `steering/full-guide.md` + +## Available Steering Files + +Each rule is available as a separate steering file. Use `readSteering` with the rule name to get detailed explanations and code examples. + +- **full-guide** - Complete compiled document with all 45 rules (2,200+ lines) + +### 1. Eliminating Waterfalls (CRITICAL) + +- **async-defer-await** - Move await into branches where actually used +- **async-parallel** - Use Promise.all() for independent operations +- **async-dependencies** - Use better-all for partial dependencies +- **async-api-routes** - Start promises early, await late in API routes +- **async-suspense-boundaries** - Use Suspense to stream content + +### 2. Bundle Size Optimization (CRITICAL) + +- **bundle-barrel-imports** - Import directly, avoid barrel files +- **bundle-dynamic-imports** - Use next/dynamic for heavy components +- **bundle-defer-third-party** - Load analytics/logging after hydration +- **bundle-conditional** - Load modules only when feature is activated +- **bundle-preload** - Preload on hover/focus for perceived speed + +### 3. Server-Side Performance (HIGH) + +- **server-cache-react** - Use React.cache() for per-request deduplication +- **server-cache-lru** - Use LRU cache for cross-request caching +- **server-serialization** - Minimize data passed to client components +- **server-parallel-fetching** - Restructure components to parallelize fetches +- **server-after-nonblocking** - Use after() for non-blocking operations + +### 4. Client-Side Data Fetching (MEDIUM-HIGH) + +- **client-swr-dedup** - Use SWR for automatic request deduplication +- **client-event-listeners** - Deduplicate global event listeners + +### 5. Re-render Optimization (MEDIUM) + +- **rerender-defer-reads** - Don't subscribe to state only used in callbacks +- **rerender-memo** - Extract expensive work into memoized components +- **rerender-dependencies** - Use primitive dependencies in effects +- **rerender-derived-state** - Subscribe to derived booleans, not raw values +- **rerender-functional-setstate** - Use functional setState for stable callbacks +- **rerender-lazy-state-init** - Pass function to useState for expensive values +- **rerender-transitions** - Use startTransition for non-urgent updates + +### 6. Rendering Performance (MEDIUM) + +- **rendering-animate-svg-wrapper** - Animate div wrapper, not SVG element +- **rendering-content-visibility** - Use content-visibility for long lists +- **rendering-hoist-jsx** - Extract static JSX outside components +- **rendering-svg-precision** - Reduce SVG coordinate precision +- **rendering-hydration-no-flicker** - Use inline script for client-only data +- **rendering-activity** - Use Activity component for show/hide +- **rendering-conditional-render** - Use ternary, not && for conditionals + +### 7. JavaScript Performance (LOW-MEDIUM) + +- **js-batch-dom-css** - Group CSS changes via classes or cssText +- **js-index-maps** - Build Map for repeated lookups +- **js-cache-property-access** - Cache object properties in loops +- **js-cache-function-results** - Cache function results in module-level Map +- **js-cache-storage** - Cache localStorage/sessionStorage reads +- **js-combine-iterations** - Combine multiple filter/map into one loop +- **js-length-check-first** - Check array length before expensive comparison +- **js-early-exit** - Return early from functions +- **js-hoist-regexp** - Hoist RegExp creation outside loops +- **js-min-max-loop** - Use loop for min/max instead of sort +- **js-set-map-lookups** - Use Set/Map for O(1) lookups +- **js-tosorted-immutable** - Use toSorted() for immutability + +### 8. Advanced Patterns (LOW) + +- **advanced-event-handler-refs** - Store event handlers in refs +- **advanced-use-latest** - useLatest for stable callback refs + +## Best Practices + +### ✅ Do: +- Start with CRITICAL rules (waterfalls, bundle size) before micro-optimizations +- Use `Promise.all()` for independent async operations +- Import directly from source files, not barrel files +- Use `next/dynamic` for heavy components not needed on initial render +- Use `React.cache()` for per-request deduplication on the server +- Use SWR for client-side data fetching with automatic deduplication + +### ❌ Don't: +- Optimize `useMemo`/`useCallback` before fixing waterfalls +- Import from barrel files (`import { X } from 'library'`) +- Load analytics/tracking scripts before hydration +- Subscribe to entire objects when you only need a derived boolean +- Use `&&` for conditional rendering (use ternary instead) + +## References + +- [React Documentation](https://react.dev) +- [Next.js Documentation](https://nextjs.org) +- [SWR Documentation](https://swr.vercel.app) +- [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) +- [Source Repository](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices) + +--- + +**License:** MIT +**Original Author:** [@shuding](https://x.com/shuding) at Vercel diff --git a/react-best-practices/steering/advanced-event-handler-refs.md b/react-best-practices/steering/advanced-event-handler-refs.md new file mode 100644 index 0000000..3a45152 --- /dev/null +++ b/react-best-practices/steering/advanced-event-handler-refs.md @@ -0,0 +1,55 @@ +--- +title: Store Event Handlers in Refs +impact: LOW +impactDescription: stable subscriptions +tags: advanced, hooks, refs, event-handlers, optimization +--- + +## Store Event Handlers in Refs + +Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes. + +**Incorrect (re-subscribes on every render):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + useEffect(() => { + window.addEventListener(event, handler) + return () => window.removeEventListener(event, handler) + }, [event, handler]) +} +``` + +**Correct (stable subscription):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + const handlerRef = useRef(handler) + useEffect(() => { + handlerRef.current = handler + }, [handler]) + + useEffect(() => { + const listener = () => handlerRef.current() + window.addEventListener(event, listener) + return () => window.removeEventListener(event, listener) + }, [event]) +} +``` + +**Alternative: use `useEffectEvent` if you're on latest React:** + +```tsx +import { useEffectEvent } from 'react' + +function useWindowEvent(event: string, handler: () => void) { + const onEvent = useEffectEvent(handler) + + useEffect(() => { + window.addEventListener(event, onEvent) + return () => window.removeEventListener(event, onEvent) + }, [event]) +} +``` + +`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler. diff --git a/react-best-practices/steering/advanced-use-latest.md b/react-best-practices/steering/advanced-use-latest.md new file mode 100644 index 0000000..3facdf2 --- /dev/null +++ b/react-best-practices/steering/advanced-use-latest.md @@ -0,0 +1,49 @@ +--- +title: useLatest for Stable Callback Refs +impact: LOW +impactDescription: prevents effect re-runs +tags: advanced, hooks, useLatest, refs, optimization +--- + +## useLatest for Stable Callback Refs + +Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. + +**Implementation:** + +```typescript +function useLatest(value: T) { + const ref = useRef(value) + useEffect(() => { + ref.current = value + }, [value]) + return ref +} +``` + +**Incorrect (effect re-runs on every callback change):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + + useEffect(() => { + const timeout = setTimeout(() => onSearch(query), 300) + return () => clearTimeout(timeout) + }, [query, onSearch]) +} +``` + +**Correct (stable effect, fresh callback):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + const onSearchRef = useLatest(onSearch) + + useEffect(() => { + const timeout = setTimeout(() => onSearchRef.current(query), 300) + return () => clearTimeout(timeout) + }, [query]) +} +``` diff --git a/react-best-practices/steering/async-api-routes.md b/react-best-practices/steering/async-api-routes.md new file mode 100644 index 0000000..6feda1e --- /dev/null +++ b/react-best-practices/steering/async-api-routes.md @@ -0,0 +1,38 @@ +--- +title: Prevent Waterfall Chains in API Routes +impact: CRITICAL +impactDescription: 2-10× improvement +tags: api-routes, server-actions, waterfalls, parallelization +--- + +## Prevent Waterfall Chains in API Routes + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect (config waits for auth, data waits for both):** + +```typescript +export async function GET(request: Request) { + const session = await auth() + const config = await fetchConfig() + const data = await fetchData(session.user.id) + return Response.json({ data, config }) +} +``` + +**Correct (auth and config start immediately):** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth() + const configPromise = fetchConfig() + const session = await sessionPromise + const [config, data] = await Promise.all([ + configPromise, + fetchData(session.user.id) + ]) + return Response.json({ data, config }) +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). diff --git a/react-best-practices/steering/async-defer-await.md b/react-best-practices/steering/async-defer-await.md new file mode 100644 index 0000000..ea7082a --- /dev/null +++ b/react-best-practices/steering/async-defer-await.md @@ -0,0 +1,80 @@ +--- +title: Defer Await Until Needed +impact: HIGH +impactDescription: avoids blocking unused code paths +tags: async, await, conditional, optimization +--- + +## Defer Await Until Needed + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect (blocks both branches):** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct (only blocks when needed):** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example (early return optimization):** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. diff --git a/react-best-practices/steering/async-dependencies.md b/react-best-practices/steering/async-dependencies.md new file mode 100644 index 0000000..fb90d86 --- /dev/null +++ b/react-best-practices/steering/async-dependencies.md @@ -0,0 +1,36 @@ +--- +title: Dependency-Based Parallelization +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, dependencies, better-all +--- + +## Dependency-Based Parallelization + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect (profile waits for config unnecessarily):** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct (config and profile run in parallel):** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) diff --git a/react-best-practices/steering/async-parallel.md b/react-best-practices/steering/async-parallel.md new file mode 100644 index 0000000..64133f6 --- /dev/null +++ b/react-best-practices/steering/async-parallel.md @@ -0,0 +1,28 @@ +--- +title: Promise.all() for Independent Operations +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, promises, waterfalls +--- + +## Promise.all() for Independent Operations + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect (sequential execution, 3 round trips):** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct (parallel execution, 1 round trip):** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` diff --git a/react-best-practices/steering/async-suspense-boundaries.md b/react-best-practices/steering/async-suspense-boundaries.md new file mode 100644 index 0000000..1fbc05b --- /dev/null +++ b/react-best-practices/steering/async-suspense-boundaries.md @@ -0,0 +1,99 @@ +--- +title: Strategic Suspense Boundaries +impact: HIGH +impactDescription: faster initial paint +tags: async, suspense, streaming, layout-shift +--- + +## Strategic Suspense Boundaries + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect (wrapper blocked by data fetching):** + +```tsx +async function Page() { + const data = await fetchData() // Blocks entire page + + return ( +
+
Sidebar
+
Header
+
+ +
+
Footer
+
+ ) +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct (wrapper shows immediately, data streams in):** + +```tsx +function Page() { + return ( +
+
Sidebar
+
Header
+
+ }> + + +
+
Footer
+
+ ) +} + +async function DataDisplay() { + const data = await fetchData() // Only blocks this component + return
{data.content}
+} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative (share promise across components):** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData() + + return ( +
+
Sidebar
+
Header
+ }> + + + +
Footer
+
+ ) +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Unwraps the promise + return
{data.content}
+} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Reuses the same promise + return
{data.summary}
+} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) +- SEO-critical content above the fold +- Small, fast queries where suspense overhead isn't worth it +- When you want to avoid layout shift (loading → content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. diff --git a/react-best-practices/steering/bundle-barrel-imports.md b/react-best-practices/steering/bundle-barrel-imports.md new file mode 100644 index 0000000..ee48f32 --- /dev/null +++ b/react-best-practices/steering/bundle-barrel-imports.md @@ -0,0 +1,59 @@ +--- +title: Avoid Barrel File Imports +impact: CRITICAL +impactDescription: 200-800ms import cost, slow builds +tags: bundle, imports, tree-shaking, barrel-files, performance +--- + +## Avoid Barrel File Imports + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect (imports entire library):** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct (imports only what you need):** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative (Next.js 13.5+):** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) diff --git a/react-best-practices/steering/bundle-conditional.md b/react-best-practices/steering/bundle-conditional.md new file mode 100644 index 0000000..40bd6f9 --- /dev/null +++ b/react-best-practices/steering/bundle-conditional.md @@ -0,0 +1,31 @@ +--- +title: Conditional Module Loading +impact: HIGH +impactDescription: loads large data only when needed +tags: bundle, conditional-loading, lazy-loading +--- + +## Conditional Module Loading + +Load large data or modules only when a feature is activated. + +**Example (lazy-load animation frames):** + +```tsx +function AnimationPlayer({ enabled }: { enabled: boolean }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. diff --git a/react-best-practices/steering/bundle-defer-third-party.md b/react-best-practices/steering/bundle-defer-third-party.md new file mode 100644 index 0000000..db041d1 --- /dev/null +++ b/react-best-practices/steering/bundle-defer-third-party.md @@ -0,0 +1,49 @@ +--- +title: Defer Non-Critical Third-Party Libraries +impact: MEDIUM +impactDescription: loads after hydration +tags: bundle, third-party, analytics, defer +--- + +## Defer Non-Critical Third-Party Libraries + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect (blocks initial bundle):** + +```tsx +import { Analytics } from '@vercel/analytics/react' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +**Correct (loads after hydration):** + +```tsx +import dynamic from 'next/dynamic' + +const Analytics = dynamic( + () => import('@vercel/analytics/react').then(m => m.Analytics), + { ssr: false } +) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` diff --git a/react-best-practices/steering/bundle-dynamic-imports.md b/react-best-practices/steering/bundle-dynamic-imports.md new file mode 100644 index 0000000..60b6269 --- /dev/null +++ b/react-best-practices/steering/bundle-dynamic-imports.md @@ -0,0 +1,35 @@ +--- +title: Dynamic Imports for Heavy Components +impact: CRITICAL +impactDescription: directly affects TTI and LCP +tags: bundle, dynamic-import, code-splitting, next-dynamic +--- + +## Dynamic Imports for Heavy Components + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect (Monaco bundles with main chunk ~300KB):** + +```tsx +import { MonacoEditor } from './monaco-editor' + +function CodePanel({ code }: { code: string }) { + return +} +``` + +**Correct (Monaco loads on demand):** + +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor').then(m => m.MonacoEditor), + { ssr: false } +) + +function CodePanel({ code }: { code: string }) { + return +} +``` diff --git a/react-best-practices/steering/bundle-preload.md b/react-best-practices/steering/bundle-preload.md new file mode 100644 index 0000000..7000504 --- /dev/null +++ b/react-best-practices/steering/bundle-preload.md @@ -0,0 +1,50 @@ +--- +title: Preload Based on User Intent +impact: MEDIUM +impactDescription: reduces perceived latency +tags: bundle, preload, user-intent, hover +--- + +## Preload Based on User Intent + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example (preload on hover/focus):** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example (preload when feature flag is enabled):** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. diff --git a/react-best-practices/steering/client-event-listeners.md b/react-best-practices/steering/client-event-listeners.md new file mode 100644 index 0000000..aad4ae9 --- /dev/null +++ b/react-best-practices/steering/client-event-listeners.md @@ -0,0 +1,74 @@ +--- +title: Deduplicate Global Event Listeners +impact: LOW +impactDescription: single listener for N components +tags: client, swr, event-listeners, subscription +--- + +## Deduplicate Global Event Listeners + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect (N instances = N listeners):** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct (N instances = 1 listener):** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` diff --git a/react-best-practices/steering/client-swr-dedup.md b/react-best-practices/steering/client-swr-dedup.md new file mode 100644 index 0000000..2a430f2 --- /dev/null +++ b/react-best-practices/steering/client-swr-dedup.md @@ -0,0 +1,56 @@ +--- +title: Use SWR for Automatic Deduplication +impact: MEDIUM-HIGH +impactDescription: automatic deduplication +tags: client, swr, deduplication, data-fetching +--- + +## Use SWR for Automatic Deduplication + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect (no deduplication, each instance fetches):** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct (multiple instances share one request):** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) diff --git a/react-best-practices/steering/full-guide.md b/react-best-practices/steering/full-guide.md new file mode 100644 index 0000000..280a6ae --- /dev/null +++ b/react-best-practices/steering/full-guide.md @@ -0,0 +1,2249 @@ +# React Best Practices + +**Version 1.0.0** +Vercel Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React and Next.js codebases at Vercel. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) +3. [Server-Side Performance](#3-server-side-performance) — **HIGH** + - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching) + - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries) + - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition) + - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache) + - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) + - 4.2 [Use SWR for Automatic Deduplication](#42-use-swr-for-automatic-deduplication) +5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) + - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components) + - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies) + - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state) + - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates) + - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization) + - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates) +6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) + - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) + - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) +7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** + - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes) + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) + - 7.8 [Early Return from Functions](#78-early-return-from-functions) + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) +8. [Advanced Patterns](#8-advanced-patterns) — **LOW** + - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) + - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs) + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example: early return optimization** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10× improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) + +### 1.3 Prevent Waterfall Chains in API Routes + +**Impact: CRITICAL (2-10× improvement)** + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect: config waits for auth, data waits for both** + +```typescript +export async function GET(request: Request) { + const session = await auth() + const config = await fetchConfig() + const data = await fetchData(session.user.id) + return Response.json({ data, config }) +} +``` + +**Correct: auth and config start immediately** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth() + const configPromise = fetchConfig() + const session = await sessionPromise + const [config, data] = await Promise.all([ + configPromise, + fetchData(session.user.id) + ]) + return Response.json({ data, config }) +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). + +### 1.4 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10× improvement)** + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +### 1.5 Strategic Suspense Boundaries + +**Impact: HIGH (faster initial paint)** + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect: wrapper blocked by data fetching** + +```tsx +async function Page() { + const data = await fetchData() // Blocks entire page + + return ( +
+
Sidebar
+
Header
+
+ +
+
Footer
+
+ ) +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct: wrapper shows immediately, data streams in** + +```tsx +function Page() { + return ( +
+
Sidebar
+
Header
+
+ }> + + +
+
Footer
+
+ ) +} + +async function DataDisplay() { + const data = await fetchData() // Only blocks this component + return
{data.content}
+} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative: share promise across components** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData() + + return ( +
+
Sidebar
+
Header
+ }> + + + +
Footer
+
+ ) +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Unwraps the promise + return
{data.content}
+} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Reuses the same promise + return
{data.summary}
+} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) + +- SEO-critical content above the fold + +- Small, fast queries where suspense overhead isn't worth it + +- When you want to avoid layout shift (loading → content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative: Next.js 13.5+** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ enabled }: { enabled: boolean }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. + +### 2.3 Defer Non-Critical Third-Party Libraries + +**Impact: MEDIUM (loads after hydration)** + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect: blocks initial bundle** + +```tsx +import { Analytics } from '@vercel/analytics/react' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +**Correct: loads after hydration** + +```tsx +import dynamic from 'next/dynamic' + +const Analytics = dynamic( + () => import('@vercel/analytics/react').then(m => m.Analytics), + { ssr: false } +) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +### 2.4 Dynamic Imports for Heavy Components + +**Impact: CRITICAL (directly affects TTI and LCP)** + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect: Monaco bundles with main chunk ~300KB** + +```tsx +import { MonacoEditor } from './monaco-editor' + +function CodePanel({ code }: { code: string }) { + return +} +``` + +**Correct: Monaco loads on demand** + +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor').then(m => m.MonacoEditor), + { ssr: false } +) + +function CodePanel({ code }: { code: string }) { + return +} +``` + +### 2.5 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example: preload on hover/focus** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example: preload when feature flag is enabled** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. + +--- + +## 3. Server-Side Performance + +**Impact: HIGH** + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### 3.1 Cross-Request LRU Caching + +**Impact: HIGH (caches across requests)** + +`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. + +**Implementation:** + +```typescript +import { LRUCache } from 'lru-cache' + +const cache = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000 // 5 minutes +}) + +export async function getUser(id: string) { + const cached = cache.get(id) + if (cached) return cached + + const user = await db.user.findUnique({ where: { id } }) + cache.set(id, user) + return user +} + +// Request 1: DB query, result cached +// Request 2: cache hit, no DB query +``` + +Use when sequential user actions hit multiple endpoints needing the same data within seconds. + +**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis. + +**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. + +Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) + +### 3.2 Minimize Serialization at RSC Boundaries + +**Impact: HIGH (reduces data transfer size)** + +The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. + +**Incorrect: serializes all 50 fields** + +```tsx +async function Page() { + const user = await fetchUser() // 50 fields + return +} + +'use client' +function Profile({ user }: { user: User }) { + return
{user.name}
// uses 1 field +} +``` + +**Correct: serializes only 1 field** + +```tsx +async function Page() { + const user = await fetchUser() + return +} + +'use client' +function Profile({ name }: { name: string }) { + return
{name}
+} +``` + +### 3.3 Parallel Data Fetching with Component Composition + +**Impact: CRITICAL (eliminates server-side waterfalls)** + +React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. + +**Incorrect: Sidebar waits for Page's fetch to complete** + +```tsx +export default async function Page() { + const header = await fetchHeader() + return ( +
+
{header}
+ +
+ ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} +``` + +**Correct: both fetch simultaneously** + +```tsx +async function Header() { + const data = await fetchHeader() + return
{data}
+} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( +
+
+ +
+ ) +} +``` + +**Alternative with children prop:** + +```tsx +async function Layout({ children }: { children: ReactNode }) { + const header = await fetchHeader() + return ( +
+
{header}
+ {children} +
+ ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( + + + + ) +} +``` + +### 3.4 Per-Request Deduplication with React.cache() + +**Impact: MEDIUM (deduplicates within request)** + +Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. + +**Usage:** + +```typescript +import { cache } from 'react' + +export const getCurrentUser = cache(async () => { + const session = await auth() + if (!session?.user?.id) return null + return await db.user.findUnique({ + where: { id: session.user.id } + }) +}) +``` + +Within a single request, multiple calls to `getCurrentUser()` execute the query only once. + +### 3.5 Use after() for Non-Blocking Operations + +**Impact: MEDIUM (faster response times)** + +Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. + +**Incorrect: blocks response** + +```tsx +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Logging blocks the response + const userAgent = request.headers.get('user-agent') || 'unknown' + await logUserAction({ userAgent }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +**Correct: non-blocking** + +```tsx +import { after } from 'next/server' +import { headers, cookies } from 'next/headers' +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Log after response is sent + after(async () => { + const userAgent = (await headers()).get('user-agent') || 'unknown' + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' + + logUserAction({ sessionCookie, userAgent }) + }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +The response is sent immediately while logging happens in the background. + +**Common use cases:** + +- Analytics tracking + +- Audit logging + +- Sending notifications + +- Cache invalidation + +- Cleanup tasks + +**Important notes:** + +- `after()` runs even if the response fails or redirects + +- Works in Server Actions, Route Handlers, and Server Components + +Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) + +--- + +## 4. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### 4.1 Deduplicate Global Event Listeners + +**Impact: LOW (single listener for N components)** + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect: N instances = N listeners** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct: N instances = 1 listener** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` + +### 4.2 Use SWR for Automatic Deduplication + +**Impact: MEDIUM-HIGH (automatic deduplication)** + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect: no deduplication, each instance fetches** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct: multiple instances share one request** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) + +--- + +## 5. Re-render Optimization + +**Impact: MEDIUM** + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### 5.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary subscriptions)** + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect: subscribes to all searchParams changes** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams() + + const handleShare = () => { + const ref = searchParams.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +**Correct: reads on demand, no subscription** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search) + const ref = params.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +### 5.2 Extract to Memoized Components + +**Impact: MEDIUM (enables early returns)** + +Extract expensive work into memoized components to enable early returns before computation. + +**Incorrect: computes avatar even when loading** + +```tsx +function Profile({ user, loading }: Props) { + const avatar = useMemo(() => { + const id = computeAvatarId(user) + return + }, [user]) + + if (loading) return + return
{avatar}
+} +``` + +**Correct: skips computation when loading** + +```tsx +const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { + const id = useMemo(() => computeAvatarId(user), [user]) + return +}) + +function Profile({ user, loading }: Props) { + if (loading) return + return ( +
+ +
+ ) +} +``` + +**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders. + +### 5.3 Narrow Effect Dependencies + +**Impact: LOW (minimizes effect re-runs)** + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect: re-runs on any user field change** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user]) +``` + +**Correct: re-runs only when id changes** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user.id]) +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode() + } +}, [width]) + +// Correct: runs only on boolean transition +const isMobile = width < 768 +useEffect(() => { + if (isMobile) { + enableMobileMode() + } +}, [isMobile]) +``` + +### 5.4 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-render frequency)** + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect: re-renders on every pixel change** + +```tsx +function Sidebar() { + const width = useWindowWidth() // updates continuously + const isMobile = width < 768 + return