AGENTS.md
.agents/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
Quality
64/100
Scores the file, not the repository.Length
11,044 words
74 headings · 146 code blocksRepository
49k
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# React Best Practices23**Version 1.0.0**4Vercel Engineering5January 202667> **Note:**8> This document is mainly for agents and LLMs to follow when maintaining,9> generating, or refactoring React and Next.js codebases. Humans10> may also find it useful, but guidance here is optimized for automation11> and consistency by AI-assisted workflows.1213---1415## Abstract1617Comprehensive 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.1819---2021## Table of Contents22231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**24 - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)25 - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)26 - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)27 - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)28 - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)292. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**30 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)31 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)32 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)33 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)34 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)353. [Server-Side Performance](#3-server-side-performance) — **HIGH**36 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)37 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)38 - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)39 - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level)40 - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries)41 - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition)42 - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache)43 - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations)444. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**45 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)46 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)47 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)48 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)495. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**50 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)51 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)52 - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)53 - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)54 - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)55 - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)56 - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)57 - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)58 - 5.9 [Subscribe to Derived State](#59-subscribe-to-derived-state)59 - 5.10 [Use Functional setState Updates](#510-use-functional-setstate-updates)60 - 5.11 [Use Lazy State Initialization](#511-use-lazy-state-initialization)61 - 5.12 [Use Transitions for Non-Urgent Updates](#512-use-transitions-for-non-urgent-updates)62 - 5.13 [Use useRef for Transient Values](#513-use-useref-for-transient-values)636. [Rendering Performance](#6-rendering-performance) — **MEDIUM**64 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)65 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)66 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)67 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)68 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)69 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)70 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)71 - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)72 - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)73 - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)74 - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)757. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**76 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)77 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)78 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)79 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)80 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)81 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)82 - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)83 - 7.8 [Early Return from Functions](#78-early-return-from-functions)84 - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)85 - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass)86 - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort)87 - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups)88 - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability)898. [Advanced Patterns](#8-advanced-patterns) — **LOW**90 - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)91 - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)92 - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)9394---9596## 1. Eliminating Waterfalls9798**Impact: CRITICAL**99100Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.101102### 1.1 Defer Await Until Needed103104**Impact: HIGH (avoids blocking unused code paths)**105106Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.107108**Incorrect: blocks both branches**109110```typescript111async function handleRequest(userId: string, skipProcessing: boolean) {112 const userData = await fetchUserData(userId)113114 if (skipProcessing) {115 // Returns immediately but still waited for userData116 return { skipped: true }117 }118119 // Only this branch uses userData120 return processUserData(userData)121}122```123124**Correct: only blocks when needed**125126```typescript127async function handleRequest(userId: string, skipProcessing: boolean) {128 if (skipProcessing) {129 // Returns immediately without waiting130 return { skipped: true }131 }132133 // Fetch only when needed134 const userData = await fetchUserData(userId)135 return processUserData(userData)136}137```138139**Another example: early return optimization**140141```typescript142// Incorrect: always fetches permissions143async function updateResource(resourceId: string, userId: string) {144 const permissions = await fetchPermissions(userId)145 const resource = await getResource(resourceId)146147 if (!resource) {148 return { error: 'Not found' }149 }150151 if (!permissions.canEdit) {152 return { error: 'Forbidden' }153 }154155 return await updateResourceData(resource, permissions)156}157158// Correct: fetches only when needed159async function updateResource(resourceId: string, userId: string) {160 const resource = await getResource(resourceId)161162 if (!resource) {163 return { error: 'Not found' }164 }165166 const permissions = await fetchPermissions(userId)167168 if (!permissions.canEdit) {169 return { error: 'Forbidden' }170 }171172 return await updateResourceData(resource, permissions)173}174```175176This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.177178### 1.2 Dependency-Based Parallelization179180**Impact: CRITICAL (2-10× improvement)**181182For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.183184**Incorrect: profile waits for config unnecessarily**185186```typescript187const [user, config] = await Promise.all([188 fetchUser(),189 fetchConfig()190])191const profile = await fetchProfile(user.id)192```193194**Correct: config and profile run in parallel**195196```typescript197import { all } from 'better-all'198199const { user, config, profile } = await all({200 async user() { return fetchUser() },201 async config() { return fetchConfig() },202 async profile() {203 return fetchProfile((await this.$.user).id)204 }205})206```207208**Alternative without extra dependencies:**209210```typescript211const userPromise = fetchUser()212const profilePromise = userPromise.then(user => fetchProfile(user.id))213214const [user, config, profile] = await Promise.all([215 userPromise,216 fetchConfig(),217 profilePromise218])219```220221We can also create all the promises first, and do `Promise.all()` at the end.222223Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)224225### 1.3 Prevent Waterfall Chains in API Routes226227**Impact: CRITICAL (2-10× improvement)**228229In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.230231**Incorrect: config waits for auth, data waits for both**232233```typescript234export async function GET(request: Request) {235 const session = await auth()236 const config = await fetchConfig()237 const data = await fetchData(session.user.id)238 return Response.json({ data, config })239}240```241242**Correct: auth and config start immediately**243244```typescript245export async function GET(request: Request) {246 const sessionPromise = auth()247 const configPromise = fetchConfig()248 const session = await sessionPromise249 const [config, data] = await Promise.all([250 configPromise,251 fetchData(session.user.id)252 ])253 return Response.json({ data, config })254}255```256257For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).258259### 1.4 Promise.all() for Independent Operations260261**Impact: CRITICAL (2-10× improvement)**262263When async operations have no interdependencies, execute them concurrently using `Promise.all()`.264265**Incorrect: sequential execution, 3 round trips**266267```typescript268const user = await fetchUser()269const posts = await fetchPosts()270const comments = await fetchComments()271```272273**Correct: parallel execution, 1 round trip**274275```typescript276const [user, posts, comments] = await Promise.all([277 fetchUser(),278 fetchPosts(),279 fetchComments()280])281```282283### 1.5 Strategic Suspense Boundaries284285**Impact: HIGH (faster initial paint)**286287Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.288289**Incorrect: wrapper blocked by data fetching**290291```tsx292async function Page() {293 const data = await fetchData() // Blocks entire page294295 return (296 <div>297 <div>Sidebar</div>298 <div>Header</div>299 <div>300 <DataDisplay data={data} />301 </div>302 <div>Footer</div>303 </div>304 )305}306```307308The entire layout waits for data even though only the middle section needs it.309310**Correct: wrapper shows immediately, data streams in**311312```tsx313function Page() {314 return (315 <div>316 <div>Sidebar</div>317 <div>Header</div>318 <div>319 <Suspense fallback={<Skeleton />}>320 <DataDisplay />321 </Suspense>322 </div>323 <div>Footer</div>324 </div>325 )326}327328async function DataDisplay() {329 const data = await fetchData() // Only blocks this component330 return <div>{data.content}</div>331}332```333334Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.335336**Alternative: share promise across components**337338```tsx339function Page() {340 // Start fetch immediately, but don't await341 const dataPromise = fetchData()342343 return (344 <div>345 <div>Sidebar</div>346 <div>Header</div>347 <Suspense fallback={<Skeleton />}>348 <DataDisplay dataPromise={dataPromise} />349 <DataSummary dataPromise={dataPromise} />350 </Suspense>351 <div>Footer</div>352 </div>353 )354}355356function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {357 const data = use(dataPromise) // Unwraps the promise358 return <div>{data.content}</div>359}360361function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {362 const data = use(dataPromise) // Reuses the same promise363 return <div>{data.summary}</div>364}365```366367Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.368369**When NOT to use this pattern:**370371- Critical data needed for layout decisions (affects positioning)372373- SEO-critical content above the fold374375- Small, fast queries where suspense overhead isn't worth it376377- When you want to avoid layout shift (loading → content jump)378379**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.380381---382383## 2. Bundle Size Optimization384385**Impact: CRITICAL**386387Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.388389### 2.1 Avoid Barrel File Imports390391**Impact: CRITICAL (200-800ms import cost, slow builds)**392393Import 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'`).394395Popular 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.396397**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.398399**Incorrect: imports entire library**400401```tsx402import { Check, X, Menu } from 'lucide-react'403// Loads 1,583 modules, takes ~2.8s extra in dev404// Runtime cost: 200-800ms on every cold start405406import { Button, TextField } from '@mui/material'407// Loads 2,225 modules, takes ~4.2s extra in dev408```409410**Correct: imports only what you need**411412```tsx413import Check from 'lucide-react/dist/esm/icons/check'414import X from 'lucide-react/dist/esm/icons/x'415import Menu from 'lucide-react/dist/esm/icons/menu'416// Loads only 3 modules (~2KB vs ~1MB)417418import Button from '@mui/material/Button'419import TextField from '@mui/material/TextField'420// Loads only what you use421```422423**Alternative: Next.js 13.5+**424425```js426// next.config.js - use optimizePackageImports427module.exports = {428 experimental: {429 optimizePackageImports: ['lucide-react', '@mui/material']430 }431}432433// Then you can keep the ergonomic barrel imports:434import { Check, X, Menu } from 'lucide-react'435// Automatically transformed to direct imports at build time436```437438Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.439440Libraries 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`.441442Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)443444### 2.2 Conditional Module Loading445446**Impact: HIGH (loads large data only when needed)**447448Load large data or modules only when a feature is activated.449450**Example: lazy-load animation frames**451452```tsx453function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {454 const [frames, setFrames] = useState<Frame[] | null>(null)455456 useEffect(() => {457 if (enabled && !frames && typeof window !== 'undefined') {458 import('./animation-frames.js')459 .then(mod => setFrames(mod.frames))460 .catch(() => setEnabled(false))461 }462 }, [enabled, frames, setEnabled])463464 if (!frames) return <Skeleton />465 return <Canvas frames={frames} />466}467```468469The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.470471### 2.3 Defer Non-Critical Third-Party Libraries472473**Impact: MEDIUM (loads after hydration)**474475Analytics, logging, and error tracking don't block user interaction. Load them after hydration.476477**Incorrect: blocks initial bundle**478479```tsx480import { Analytics } from '@vercel/analytics/react'481482export default function RootLayout({ children }) {483 return (484 <html>485 <body>486 {children}487 <Analytics />488 </body>489 </html>490 )491}492```493494**Correct: loads after hydration**495496```tsx497import dynamic from 'next/dynamic'498499const Analytics = dynamic(500 () => import('@vercel/analytics/react').then(m => m.Analytics),501 { ssr: false }502)503504export default function RootLayout({ children }) {505 return (506 <html>507 <body>508 {children}509 <Analytics />510 </body>511 </html>512 )513}514```515516### 2.4 Dynamic Imports for Heavy Components517518**Impact: CRITICAL (directly affects TTI and LCP)**519520Use `next/dynamic` to lazy-load large components not needed on initial render.521522**Incorrect: Monaco bundles with main chunk ~300KB**523524```tsx525import { MonacoEditor } from './monaco-editor'526527function CodePanel({ code }: { code: string }) {528 return <MonacoEditor value={code} />529}530```531532**Correct: Monaco loads on demand**533534```tsx535import dynamic from 'next/dynamic'536537const MonacoEditor = dynamic(538 () => import('./monaco-editor').then(m => m.MonacoEditor),539 { ssr: false }540)541542function CodePanel({ code }: { code: string }) {543 return <MonacoEditor value={code} />544}545```546547### 2.5 Preload Based on User Intent548549**Impact: MEDIUM (reduces perceived latency)**550551Preload heavy bundles before they're needed to reduce perceived latency.552553**Example: preload on hover/focus**554555```tsx556function EditorButton({ onClick }: { onClick: () => void }) {557 const preload = () => {558 if (typeof window !== 'undefined') {559 void import('./monaco-editor')560 }561 }562563 return (564 <button565 onMouseEnter={preload}566 onFocus={preload}567 onClick={onClick}568 >569 Open Editor570 </button>571 )572}573```574575**Example: preload when feature flag is enabled**576577```tsx578function FlagsProvider({ children, flags }: Props) {579 useEffect(() => {580 if (flags.editorEnabled && typeof window !== 'undefined') {581 void import('./monaco-editor').then(mod => mod.init())582 }583 }, [flags.editorEnabled])584585 return <FlagsContext.Provider value={flags}>586 {children}587 </FlagsContext.Provider>588}589```590591The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.592593---594595## 3. Server-Side Performance596597**Impact: HIGH**598599Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.600601### 3.1 Authenticate Server Actions Like API Routes602603**Impact: CRITICAL (prevents unauthorized access to server mutations)**604605Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.606607Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."608609**Incorrect: no authentication check**610611```typescript612'use server'613614export async function deleteUser(userId: string) {615 // Anyone can call this! No auth check616 await db.user.delete({ where: { id: userId } })617 return { success: true }618}619```620621**Correct: authentication inside the action**622623```typescript624'use server'625626import { verifySession } from '@/lib/auth'627import { unauthorized } from '@/lib/errors'628629export async function deleteUser(userId: string) {630 // Always check auth inside the action631 const session = await verifySession()632633 if (!session) {634 throw unauthorized('Must be logged in')635 }636637 // Check authorization too638 if (session.user.role !== 'admin' && session.user.id !== userId) {639 throw unauthorized('Cannot delete other users')640 }641642 await db.user.delete({ where: { id: userId } })643 return { success: true }644}645```646647**With input validation:**648649```typescript650'use server'651652import { verifySession } from '@/lib/auth'653import { z } from 'zod'654655const updateProfileSchema = z.object({656 userId: z.string().uuid(),657 name: z.string().min(1).max(100),658 email: z.string().email()659})660661export async function updateProfile(data: unknown) {662 // Validate input first663 const validated = updateProfileSchema.parse(data)664665 // Then authenticate666 const session = await verifySession()667 if (!session) {668 throw new Error('Unauthorized')669 }670671 // Then authorize672 if (session.user.id !== validated.userId) {673 throw new Error('Can only update own profile')674 }675676 // Finally perform the mutation677 await db.user.update({678 where: { id: validated.userId },679 data: {680 name: validated.name,681 email: validated.email682 }683 })684685 return { success: true }686}687```688689Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)690691### 3.2 Avoid Duplicate Serialization in RSC Props692693**Impact: LOW (reduces network payload by avoiding duplicate serialization)**694695RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.696697**Incorrect: duplicates array**698699```tsx700// RSC: sends 6 strings (2 arrays × 3 items)701<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />702```703704**Correct: sends 3 strings**705706```tsx707// RSC: send once708<ClientList usernames={usernames} />709710// Client: transform there711'use client'712const sorted = useMemo(() => [...usernames].sort(), [usernames])713```714715**Nested deduplication behavior:**716717```tsx718// string[] - duplicates everything719usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings720721// object[] - duplicates array structure only722users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)723```724725Deduplication works recursively. Impact varies by data type:726727- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated728729- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference730731**Operations breaking deduplication: create new references**732733- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`734735- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`736737**More examples:**738739```tsx740// ❌ Bad741<C users={users} active={users.filter(u => u.active)} />742<C product={product} productName={product.name} />743744// ✅ Good745<C users={users} />746<C product={product} />747// Do filtering/destructuring in client748```749750**Exception:** Pass derived data when transformation is expensive or client doesn't need original.751752### 3.3 Cross-Request LRU Caching753754**Impact: HIGH (caches across requests)**755756`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.757758**Implementation:**759760```typescript761import { LRUCache } from 'lru-cache'762763const cache = new LRUCache<string, any>({764 max: 1000,765 ttl: 5 * 60 * 1000 // 5 minutes766})767768export async function getUser(id: string) {769 const cached = cache.get(id)770 if (cached) return cached771772 const user = await db.user.findUnique({ where: { id } })773 cache.set(id, user)774 return user775}776777// Request 1: DB query, result cached778// Request 2: cache hit, no DB query779```780781Use when sequential user actions hit multiple endpoints needing the same data within seconds.782783**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.784785**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.786787Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)788789### 3.4 Hoist Static I/O to Module Level790791**Impact: HIGH (avoids repeated file/network I/O per request)**792793When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.794795**Incorrect: reads font file on every request**796797**Correct: loads once at module initialization**798799**Alternative: synchronous file reads with Node.js fs**800801**General Node.js example: loading config or templates**802803**When to use this pattern:**804805- Loading fonts for OG image generation806807- Loading static logos, icons, or watermarks808809- Reading configuration files that don't change at runtime810811- Loading email templates or other static templates812813- Any static asset that's the same across all requests814815**When NOT to use this pattern:**816817- Assets that vary per request or user818819- Files that may change during runtime (use caching with TTL instead)820821- Large files that would consume too much memory if kept loaded822823- Sensitive data that shouldn't persist in memory824825**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.826827**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.828829### 3.5 Minimize Serialization at RSC Boundaries830831**Impact: HIGH (reduces data transfer size)**832833The 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.834835**Incorrect: serializes all 50 fields**836837```tsx838async function Page() {839 const user = await fetchUser() // 50 fields840 return <Profile user={user} />841}842843'use client'844function Profile({ user }: { user: User }) {845 return <div>{user.name}</div> // uses 1 field846}847```848849**Correct: serializes only 1 field**850851```tsx852async function Page() {853 const user = await fetchUser()854 return <Profile name={user.name} />855}856857'use client'858function Profile({ name }: { name: string }) {859 return <div>{name}</div>860}861```862863### 3.6 Parallel Data Fetching with Component Composition864865**Impact: CRITICAL (eliminates server-side waterfalls)**866867React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.868869**Incorrect: Sidebar waits for Page's fetch to complete**870871```tsx872export default async function Page() {873 const header = await fetchHeader()874 return (875 <div>876 <div>{header}</div>877 <Sidebar />878 </div>879 )880}881882async function Sidebar() {883 const items = await fetchSidebarItems()884 return <nav>{items.map(renderItem)}</nav>885}886```887888**Correct: both fetch simultaneously**889890```tsx891async function Header() {892 const data = await fetchHeader()893 return <div>{data}</div>894}895896async function Sidebar() {897 const items = await fetchSidebarItems()898 return <nav>{items.map(renderItem)}</nav>899}900901export default function Page() {902 return (903 <div>904 <Header />905 <Sidebar />906 </div>907 )908}909```910911**Alternative with children prop:**912913```tsx914async function Header() {915 const data = await fetchHeader()916 return <div>{data}</div>917}918919async function Sidebar() {920 const items = await fetchSidebarItems()921 return <nav>{items.map(renderItem)}</nav>922}923924function Layout({ children }: { children: ReactNode }) {925 return (926 <div>927 <Header />928 {children}929 </div>930 )931}932933export default function Page() {934 return (935 <Layout>936 <Sidebar />937 </Layout>938 )939}940```941942### 3.7 Per-Request Deduplication with React.cache()943944**Impact: MEDIUM (deduplicates within request)**945946Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.947948**Usage:**949950```typescript951import { cache } from 'react'952953export const getCurrentUser = cache(async () => {954 const session = await auth()955 if (!session?.user?.id) return null956 return await db.user.findUnique({957 where: { id: session.user.id }958 })959})960```961962Within a single request, multiple calls to `getCurrentUser()` execute the query only once.963964**Avoid inline objects as arguments:**965966`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.967968**Incorrect: always cache miss**969970```typescript971const getUser = cache(async (params: { uid: number }) => {972 return await db.user.findUnique({ where: { id: params.uid } })973})974975// Each call creates new object, never hits cache976getUser({ uid: 1 })977getUser({ uid: 1 }) // Cache miss, runs query again978```979980**Correct: cache hit**981982```typescript983const params = { uid: 1 }984getUser(params) // Query runs985getUser(params) // Cache hit (same reference)986```987988If you must pass objects, pass the same reference:989990**Next.js-Specific Note:**991992In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:993994- Database queries (Prisma, Drizzle, etc.)995996- Heavy computations997998- Authentication checks9991000- File system operations10011002- Any non-fetch async work10031004Use `React.cache()` to deduplicate these operations across your component tree.10051006Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)10071008### 3.8 Use after() for Non-Blocking Operations10091010**Impact: MEDIUM (faster response times)**10111012Use 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.10131014**Incorrect: blocks response**10151016```tsx1017import { logUserAction } from '@/app/utils'10181019export async function POST(request: Request) {1020 // Perform mutation1021 await updateDatabase(request)10221023 // Logging blocks the response1024 const userAgent = request.headers.get('user-agent') || 'unknown'1025 await logUserAction({ userAgent })10261027 return new Response(JSON.stringify({ status: 'success' }), {1028 status: 200,1029 headers: { 'Content-Type': 'application/json' }1030 })1031}1032```10331034**Correct: non-blocking**10351036```tsx1037import { after } from 'next/server'1038import { headers, cookies } from 'next/headers'1039import { logUserAction } from '@/app/utils'10401041export async function POST(request: Request) {1042 // Perform mutation1043 await updateDatabase(request)10441045 // Log after response is sent1046 after(async () => {1047 const userAgent = (await headers()).get('user-agent') || 'unknown'1048 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'10491050 logUserAction({ sessionCookie, userAgent })1051 })10521053 return new Response(JSON.stringify({ status: 'success' }), {1054 status: 200,1055 headers: { 'Content-Type': 'application/json' }1056 })1057}1058```10591060The response is sent immediately while logging happens in the background.10611062**Common use cases:**10631064- Analytics tracking10651066- Audit logging10671068- Sending notifications10691070- Cache invalidation10711072- Cleanup tasks10731074**Important notes:**10751076- `after()` runs even if the response fails or redirects10771078- Works in Server Actions, Route Handlers, and Server Components10791080Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)10811082---10831084## 4. Client-Side Data Fetching10851086**Impact: MEDIUM-HIGH**10871088Automatic deduplication and efficient data fetching patterns reduce redundant network requests.10891090### 4.1 Deduplicate Global Event Listeners10911092**Impact: LOW (single listener for N components)**10931094Use `useSWRSubscription()` to share global event listeners across component instances.10951096**Incorrect: N instances = N listeners**10971098```tsx1099function useKeyboardShortcut(key: string, callback: () => void) {1100 useEffect(() => {1101 const handler = (e: KeyboardEvent) => {1102 if (e.metaKey && e.key === key) {1103 callback()1104 }1105 }1106 window.addEventListener('keydown', handler)1107 return () => window.removeEventListener('keydown', handler)1108 }, [key, callback])1109}1110```11111112When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.11131114**Correct: N instances = 1 listener**11151116```tsx1117import useSWRSubscription from 'swr/subscription'11181119// Module-level Map to track callbacks per key1120const keyCallbacks = new Map<string, Set<() => void>>()11211122function useKeyboardShortcut(key: string, callback: () => void) {1123 // Register this callback in the Map1124 useEffect(() => {1125 if (!keyCallbacks.has(key)) {1126 keyCallbacks.set(key, new Set())1127 }1128 keyCallbacks.get(key)!.add(callback)11291130 return () => {1131 const set = keyCallbacks.get(key)1132 if (set) {1133 set.delete(callback)1134 if (set.size === 0) {1135 keyCallbacks.delete(key)1136 }1137 }1138 }1139 }, [key, callback])11401141 useSWRSubscription('global-keydown', () => {1142 const handler = (e: KeyboardEvent) => {1143 if (e.metaKey && keyCallbacks.has(e.key)) {1144 keyCallbacks.get(e.key)!.forEach(cb => cb())1145 }1146 }1147 window.addEventListener('keydown', handler)1148 return () => window.removeEventListener('keydown', handler)1149 })1150}11511152function Profile() {1153 // Multiple shortcuts will share the same listener1154 useKeyboardShortcut('p', () => { /* ... */ })1155 useKeyboardShortcut('k', () => { /* ... */ })1156 // ...1157}1158```11591160### 4.2 Use Passive Event Listeners for Scrolling Performance11611162**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**11631164Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.11651166**Incorrect:**11671168```typescript1169useEffect(() => {1170 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1171 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11721173 document.addEventListener('touchstart', handleTouch)1174 document.addEventListener('wheel', handleWheel)11751176 return () => {1177 document.removeEventListener('touchstart', handleTouch)1178 document.removeEventListener('wheel', handleWheel)1179 }1180}, [])1181```11821183**Correct:**11841185```typescript1186useEffect(() => {1187 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1188 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11891190 document.addEventListener('touchstart', handleTouch, { passive: true })1191 document.addEventListener('wheel', handleWheel, { passive: true })11921193 return () => {1194 document.removeEventListener('touchstart', handleTouch)1195 document.removeEventListener('wheel', handleWheel)1196 }1197}, [])1198```11991200**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.12011202**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.12031204### 4.3 Use SWR for Automatic Deduplication12051206**Impact: MEDIUM-HIGH (automatic deduplication)**12071208SWR enables request deduplication, caching, and revalidation across component instances.12091210**Incorrect: no deduplication, each instance fetches**12111212```tsx1213function UserList() {1214 const [users, setUsers] = useState([])1215 useEffect(() => {1216 fetch('/api/users')1217 .then(r => r.json())1218 .then(setUsers)1219 }, [])1220}1221```12221223**Correct: multiple instances share one request**12241225```tsx1226import useSWR from 'swr'12271228function UserList() {1229 const { data: users } = useSWR('/api/users', fetcher)1230}1231```12321233**For immutable data:**12341235```tsx1236import { useImmutableSWR } from '@/lib/swr'12371238function StaticContent() {1239 const { data } = useImmutableSWR('/api/config', fetcher)1240}1241```12421243**For mutations:**12441245```tsx1246import { useSWRMutation } from 'swr/mutation'12471248function UpdateButton() {1249 const { trigger } = useSWRMutation('/api/user', updateUser)1250 return <button onClick={() => trigger()}>Update</button>1251}1252```12531254Reference: [https://swr.vercel.app](https://swr.vercel.app)12551256### 4.4 Version and Minimize localStorage Data12571258**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**12591260Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.12611262**Incorrect:**12631264```typescript1265// No version, stores everything, no error handling1266localStorage.setItem('userConfig', JSON.stringify(fullUserObject))1267const data = localStorage.getItem('userConfig')1268```12691270**Correct:**12711272```typescript1273const VERSION = 'v2'12741275function saveConfig(config: { theme: string; language: string }) {1276 try {1277 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))1278 } catch {1279 // Throws in incognito/private browsing, quota exceeded, or disabled1280 }1281}12821283function loadConfig() {1284 try {1285 const data = localStorage.getItem(`userConfig:${VERSION}`)1286 return data ? JSON.parse(data) : null1287 } catch {1288 return null1289 }1290}12911292// Migration from v1 to v21293function migrate() {1294 try {1295 const v1 = localStorage.getItem('userConfig:v1')1296 if (v1) {1297 const old = JSON.parse(v1)1298 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })1299 localStorage.removeItem('userConfig:v1')1300 }1301 } catch {}1302}1303```13041305**Store minimal fields from server responses:**13061307```typescript1308// User object has 20+ fields, only store what UI needs1309function cachePrefs(user: FullUser) {1310 try {1311 localStorage.setItem('prefs:v1', JSON.stringify({1312 theme: user.preferences.theme,1313 notifications: user.preferences.notifications1314 }))1315 } catch {}1316}1317```13181319**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.13201321**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.13221323---13241325## 5. Re-render Optimization13261327**Impact: MEDIUM**13281329Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.13301331### 5.1 Calculate Derived State During Rendering13321333**Impact: MEDIUM (avoids redundant renders and state drift)**13341335If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.13361337**Incorrect: redundant state and effect**13381339```tsx1340function Form() {1341 const [firstName, setFirstName] = useState('First')1342 const [lastName, setLastName] = useState('Last')1343 const [fullName, setFullName] = useState('')13441345 useEffect(() => {1346 setFullName(firstName + ' ' + lastName)1347 }, [firstName, lastName])13481349 return <p>{fullName}</p>1350}1351```13521353**Correct: derive during render**13541355```tsx1356function Form() {1357 const [firstName, setFirstName] = useState('First')1358 const [lastName, setLastName] = useState('Last')1359 const fullName = firstName + ' ' + lastName13601361 return <p>{fullName}</p>1362}1363```13641365Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)13661367### 5.2 Defer State Reads to Usage Point13681369**Impact: MEDIUM (avoids unnecessary subscriptions)**13701371Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.13721373**Incorrect: subscribes to all searchParams changes**13741375```tsx1376function ShareButton({ chatId }: { chatId: string }) {1377 const searchParams = useSearchParams()13781379 const handleShare = () => {1380 const ref = searchParams.get('ref')1381 shareChat(chatId, { ref })1382 }13831384 return <button onClick={handleShare}>Share</button>1385}1386```13871388**Correct: reads on demand, no subscription**13891390```tsx1391function ShareButton({ chatId }: { chatId: string }) {1392 const handleShare = () => {1393 const params = new URLSearchParams(window.location.search)1394 const ref = params.get('ref')1395 shareChat(chatId, { ref })1396 }13971398 return <button onClick={handleShare}>Share</button>1399}1400```14011402### 5.3 Do not wrap a simple expression with a primitive result type in useMemo14031404**Impact: LOW-MEDIUM (wasted computation on every render)**14051406When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.14071408Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.14091410**Incorrect:**14111412```tsx1413function Header({ user, notifications }: Props) {1414 const isLoading = useMemo(() => {1415 return user.isLoading || notifications.isLoading1416 }, [user.isLoading, notifications.isLoading])14171418 if (isLoading) return <Skeleton />1419 // return some markup1420}1421```14221423**Correct:**14241425```tsx1426function Header({ user, notifications }: Props) {1427 const isLoading = user.isLoading || notifications.isLoading14281429 if (isLoading) return <Skeleton />1430 // return some markup1431}1432```14331434### 5.4 Don't Define Components Inside Components14351436**Impact: HIGH (prevents remount on every render)**14371438Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.14391440A common reason developers do this is to access parent variables without passing props. Always pass props instead.14411442**Incorrect: remounts on every render**14431444```tsx1445function UserProfile({ user, theme }) {1446 // Defined inside to access `theme` - BAD1447 const Avatar = () => (1448 <img1449 src={user.avatarUrl}1450 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}1451 />1452 )14531454 // Defined inside to access `user` - BAD1455 const Stats = () => (1456 <div>1457 <span>{user.followers} followers</span>1458 <span>{user.posts} posts</span>1459 </div>1460 )14611462 return (1463 <div>1464 <Avatar />1465 <Stats />1466 </div>1467 )1468}1469```14701471Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.14721473**Correct: pass props instead**14741475```tsx1476function Avatar({ src, theme }: { src: string; theme: string }) {1477 return (1478 <img1479 src={src}1480 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}1481 />1482 )1483}14841485function Stats({ followers, posts }: { followers: number; posts: number }) {1486 return (1487 <div>1488 <span>{followers} followers</span>1489 <span>{posts} posts</span>1490 </div>1491 )1492}14931494function UserProfile({ user, theme }) {1495 return (1496 <div>1497 <Avatar src={user.avatarUrl} theme={theme} />1498 <Stats followers={user.followers} posts={user.posts} />1499 </div>1500 )1501}1502```15031504**Symptoms of this bug:**15051506- Input fields lose focus on every keystroke15071508- Animations restart unexpectedly15091510- `useEffect` cleanup/setup runs on every parent render15111512- Scroll position resets inside the component15131514### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant15151516**Impact: MEDIUM (restores memoization by using a constant for default value)**15171518When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.15191520To address this issue, extract the default value into a constant.15211522**Incorrect: `onClick` has different values on every rerender**15231524```tsx1525const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {1526 // ...1527})15281529// Used without optional onClick1530<UserAvatar />1531```15321533**Correct: stable default value**15341535```tsx1536const NOOP = () => {};15371538const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {1539 // ...1540})15411542// Used without optional onClick1543<UserAvatar />1544```15451546### 5.6 Extract to Memoized Components15471548**Impact: MEDIUM (enables early returns)**15491550Extract expensive work into memoized components to enable early returns before computation.15511552**Incorrect: computes avatar even when loading**15531554```tsx1555function Profile({ user, loading }: Props) {1556 const avatar = useMemo(() => {1557 const id = computeAvatarId(user)1558 return <Avatar id={id} />1559 }, [user])15601561 if (loading) return <Skeleton />1562 return <div>{avatar}</div>1563}1564```15651566**Correct: skips computation when loading**15671568```tsx1569const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1570 const id = useMemo(() => computeAvatarId(user), [user])1571 return <Avatar id={id} />1572})15731574function Profile({ user, loading }: Props) {1575 if (loading) return <Skeleton />1576 return (1577 <div>1578 <UserAvatar user={user} />1579 </div>1580 )1581}1582```15831584**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.15851586### 5.7 Narrow Effect Dependencies15871588**Impact: LOW (minimizes effect re-runs)**15891590Specify primitive dependencies instead of objects to minimize effect re-runs.15911592**Incorrect: re-runs on any user field change**15931594```tsx1595useEffect(() => {1596 console.log(user.id)1597}, [user])1598```15991600**Correct: re-runs only when id changes**16011602```tsx1603useEffect(() => {1604 console.log(user.id)1605}, [user.id])1606```16071608**For derived state, compute outside effect:**16091610```tsx1611// Incorrect: runs on width=767, 766, 765...1612useEffect(() => {1613 if (width < 768) {1614 enableMobileMode()1615 }1616}, [width])16171618// Correct: runs only on boolean transition1619const isMobile = width < 7681620useEffect(() => {1621 if (isMobile) {1622 enableMobileMode()1623 }1624}, [isMobile])1625```16261627### 5.8 Put Interaction Logic in Event Handlers16281629**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**16301631If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.16321633**Incorrect: event modeled as state + effect**16341635```tsx1636function Form() {1637 const [submitted, setSubmitted] = useState(false)1638 const theme = useContext(ThemeContext)16391640 useEffect(() => {1641 if (submitted) {1642 post('/api/register')1643 showToast('Registered', theme)1644 }1645 }, [submitted, theme])16461647 return <button onClick={() => setSubmitted(true)}>Submit</button>1648}1649```16501651**Correct: do it in the handler**16521653```tsx1654function Form() {1655 const theme = useContext(ThemeContext)16561657 function handleSubmit() {1658 post('/api/register')1659 showToast('Registered', theme)1660 }16611662 return <button onClick={handleSubmit}>Submit</button>1663}1664```16651666Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)16671668### 5.9 Subscribe to Derived State16691670**Impact: MEDIUM (reduces re-render frequency)**16711672Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.16731674**Incorrect: re-renders on every pixel change**16751676```tsx1677function Sidebar() {1678 const width = useWindowWidth() // updates continuously1679 const isMobile = width < 7681680 return <nav className={isMobile ? 'mobile' : 'desktop'} />1681}1682```16831684**Correct: re-renders only when boolean changes**16851686```tsx1687function Sidebar() {1688 const isMobile = useMediaQuery('(max-width: 767px)')1689 return <nav className={isMobile ? 'mobile' : 'desktop'} />1690}1691```16921693### 5.10 Use Functional setState Updates16941695**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**16961697When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.16981699**Incorrect: requires state as dependency**17001701```tsx1702function TodoList() {1703 const [items, setItems] = useState(initialItems)17041705 // Callback must depend on items, recreated on every items change1706 const addItems = useCallback((newItems: Item[]) => {1707 setItems([...items, ...newItems])1708 }, [items]) // ❌ items dependency causes recreations17091710 // Risk of stale closure if dependency is forgotten1711 const removeItem = useCallback((id: string) => {1712 setItems(items.filter(item => item.id !== id))1713 }, []) // ❌ Missing items dependency - will use stale items!17141715 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1716}1717```17181719The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.17201721**Correct: stable callbacks, no stale closures**17221723```tsx1724function TodoList() {1725 const [items, setItems] = useState(initialItems)17261727 // Stable callback, never recreated1728 const addItems = useCallback((newItems: Item[]) => {1729 setItems(curr => [...curr, ...newItems])1730 }, []) // ✅ No dependencies needed17311732 // Always uses latest state, no stale closure risk1733 const removeItem = useCallback((id: string) => {1734 setItems(curr => curr.filter(item => item.id !== id))1735 }, []) // ✅ Safe and stable17361737 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1738}1739```17401741**Benefits:**174217431. **Stable callback references** - Callbacks don't need to be recreated when state changes174417452. **No stale closures** - Always operates on the latest state value174617473. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks174817494. **Prevents bugs** - Eliminates the most common source of React closure bugs17501751**When to use functional updates:**17521753- Any setState that depends on the current state value17541755- Inside useCallback/useMemo when state is needed17561757- Event handlers that reference state17581759- Async operations that update state17601761**When direct updates are fine:**17621763- Setting state to a static value: `setCount(0)`17641765- Setting state from props/arguments only: `setName(newName)`17661767- State doesn't depend on previous value17681769**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.17701771### 5.11 Use Lazy State Initialization17721773**Impact: MEDIUM (wasted computation on every render)**17741775Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.17761777**Incorrect: runs on every render**17781779```tsx1780function FilteredList({ items }: { items: Item[] }) {1781 // buildSearchIndex() runs on EVERY render, even after initialization1782 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))1783 const [query, setQuery] = useState('')17841785 // When query changes, buildSearchIndex runs again unnecessarily1786 return <SearchResults index={searchIndex} query={query} />1787}17881789function UserProfile() {1790 // JSON.parse runs on every render1791 const [settings, setSettings] = useState(1792 JSON.parse(localStorage.getItem('settings') || '{}')1793 )17941795 return <SettingsForm settings={settings} onChange={setSettings} />1796}1797```17981799**Correct: runs only once**18001801```tsx1802function FilteredList({ items }: { items: Item[] }) {1803 // buildSearchIndex() runs ONLY on initial render1804 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))1805 const [query, setQuery] = useState('')18061807 return <SearchResults index={searchIndex} query={query} />1808}18091810function UserProfile() {1811 // JSON.parse runs only on initial render1812 const [settings, setSettings] = useState(() => {1813 const stored = localStorage.getItem('settings')1814 return stored ? JSON.parse(stored) : {}1815 })18161817 return <SettingsForm settings={settings} onChange={setSettings} />1818}1819```18201821Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.18221823For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.18241825### 5.12 Use Transitions for Non-Urgent Updates18261827**Impact: MEDIUM (maintains UI responsiveness)**18281829Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.18301831**Incorrect: blocks UI on every scroll**18321833```tsx1834function ScrollTracker() {1835 const [scrollY, setScrollY] = useState(0)1836 useEffect(() => {1837 const handler = () => setScrollY(window.scrollY)1838 window.addEventListener('scroll', handler, { passive: true })1839 return () => window.removeEventListener('scroll', handler)1840 }, [])1841}1842```18431844**Correct: non-blocking updates**18451846```tsx1847import { startTransition } from 'react'18481849function ScrollTracker() {1850 const [scrollY, setScrollY] = useState(0)1851 useEffect(() => {1852 const handler = () => {1853 startTransition(() => setScrollY(window.scrollY))1854 }1855 window.addEventListener('scroll', handler, { passive: true })1856 return () => window.removeEventListener('scroll', handler)1857 }, [])1858}1859```18601861### 5.13 Use useRef for Transient Values18621863**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**18641865When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.18661867**Incorrect: renders every update**18681869```tsx1870function Tracker() {1871 const [lastX, setLastX] = useState(0)18721873 useEffect(() => {1874 const onMove = (e: MouseEvent) => setLastX(e.clientX)1875 window.addEventListener('mousemove', onMove)1876 return () => window.removeEventListener('mousemove', onMove)1877 }, [])18781879 return (1880 <div1881 style={{1882 position: 'fixed',1883 top: 0,1884 left: lastX,1885 width: 8,1886 height: 8,1887 background: 'black',1888 }}1889 />1890 )1891}1892```18931894**Correct: no re-render for tracking**18951896```tsx1897function Tracker() {1898 const lastXRef = useRef(0)1899 const dotRef = useRef<HTMLDivElement>(null)19001901 useEffect(() => {1902 const onMove = (e: MouseEvent) => {1903 lastXRef.current = e.clientX1904 const node = dotRef.current1905 if (node) {1906 node.style.transform = `translateX(${e.clientX}px)`1907 }1908 }1909 window.addEventListener('mousemove', onMove)1910 return () => window.removeEventListener('mousemove', onMove)1911 }, [])19121913 return (1914 <div1915 ref={dotRef}1916 style={{1917 position: 'fixed',1918 top: 0,1919 left: 0,1920 width: 8,1921 height: 8,1922 background: 'black',1923 transform: 'translateX(0px)',1924 }}1925 />1926 )1927}1928```19291930---19311932## 6. Rendering Performance19331934**Impact: MEDIUM**19351936Optimizing the rendering process reduces the work the browser needs to do.19371938### 6.1 Animate SVG Wrapper Instead of SVG Element19391940**Impact: LOW (enables hardware acceleration)**19411942Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.19431944**Incorrect: animating SVG directly - no hardware acceleration**19451946```tsx1947function LoadingSpinner() {1948 return (1949 <svg1950 className="animate-spin"1951 width="24"1952 height="24"1953 viewBox="0 0 24 24"1954 >1955 <circle cx="12" cy="12" r="10" stroke="currentColor" />1956 </svg>1957 )1958}1959```19601961**Correct: animating wrapper div - hardware accelerated**19621963```tsx1964function LoadingSpinner() {1965 return (1966 <div className="animate-spin">1967 <svg1968 width="24"1969 height="24"1970 viewBox="0 0 24 24"1971 >1972 <circle cx="12" cy="12" r="10" stroke="currentColor" />1973 </svg>1974 </div>1975 )1976}1977```19781979This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.19801981### 6.2 CSS content-visibility for Long Lists19821983**Impact: HIGH (faster initial render)**19841985Apply `content-visibility: auto` to defer off-screen rendering.19861987**CSS:**19881989```css1990.message-item {1991 content-visibility: auto;1992 contain-intrinsic-size: 0 80px;1993}1994```19951996**Example:**19971998```tsx1999function MessageList({ messages }: { messages: Message[] }) {2000 return (2001 <div className="overflow-y-auto h-screen">2002 {messages.map(msg => (2003 <div key={msg.id} className="message-item">2004 <Avatar user={msg.author} />2005 <div>{msg.content}</div>2006 </div>2007 ))}2008 </div>2009 )2010}2011```20122013For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).20142015### 6.3 Hoist Static JSX Elements20162017**Impact: LOW (avoids re-creation)**20182019Extract static JSX outside components to avoid re-creation.20202021**Incorrect: recreates element every render**20222023```tsx2024function LoadingSkeleton() {2025 return <div className="animate-pulse h-20 bg-gray-200" />2026}20272028function Container() {2029 return (2030 <div>2031 {loading && <LoadingSkeleton />}2032 </div>2033 )2034}2035```20362037**Correct: reuses same element**20382039```tsx2040const loadingSkeleton = (2041 <div className="animate-pulse h-20 bg-gray-200" />2042)20432044function Container() {2045 return (2046 <div>2047 {loading && loadingSkeleton}2048 </div>2049 )2050}2051```20522053This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.20542055**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.20562057### 6.4 Optimize SVG Precision20582059**Impact: LOW (reduces file size)**20602061Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.20622063**Incorrect: excessive precision**20642065```svg2066<path d="M 10.293847 20.847362 L 30.938472 40.192837" />2067```20682069**Correct: 1 decimal place**20702071```svg2072<path d="M 10.3 20.8 L 30.9 40.2" />2073```20742075**Automate with SVGO:**20762077```bash2078npx svgo --precision=1 --multipass icon.svg2079```20802081### 6.5 Prevent Hydration Mismatch Without Flickering20822083**Impact: MEDIUM (avoids visual flicker and hydration errors)**20842085When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.20862087**Incorrect: breaks SSR**20882089```tsx2090function ThemeWrapper({ children }: { children: ReactNode }) {2091 // localStorage is not available on server - throws error2092 const theme = localStorage.getItem('theme') || 'light'20932094 return (2095 <div className={theme}>2096 {children}2097 </div>2098 )2099}2100```21012102Server-side rendering will fail because `localStorage` is undefined.21032104**Incorrect: visual flickering**21052106```tsx2107function ThemeWrapper({ children }: { children: ReactNode }) {2108 const [theme, setTheme] = useState('light')21092110 useEffect(() => {2111 // Runs after hydration - causes visible flash2112 const stored = localStorage.getItem('theme')2113 if (stored) {2114 setTheme(stored)2115 }2116 }, [])21172118 return (2119 <div className={theme}>2120 {children}2121 </div>2122 )2123}2124```21252126Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.21272128**Correct: no flicker, no hydration mismatch**21292130```tsx2131function ThemeWrapper({ children }: { children: ReactNode }) {2132 return (2133 <>2134 <div id="theme-wrapper">2135 {children}2136 </div>2137 <script2138 dangerouslySetInnerHTML={{2139 __html: `2140 (function() {2141 try {2142 var theme = localStorage.getItem('theme') || 'light';2143 var el = document.getElementById('theme-wrapper');2144 if (el) el.className = theme;2145 } catch (e) {}2146 })();2147 `,2148 }}2149 />2150 </>2151 )2152}2153```21542155The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.21562157This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.21582159### 6.6 Suppress Expected Hydration Mismatches21602161**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**21622163In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.21642165**Incorrect: known mismatch warnings**21662167```tsx2168function Timestamp() {2169 return <span>{new Date().toLocaleString()}</span>2170}2171```21722173**Correct: suppress expected mismatch only**21742175```tsx2176function Timestamp() {2177 return (2178 <span suppressHydrationWarning>2179 {new Date().toLocaleString()}2180 </span>2181 )2182}2183```21842185### 6.7 Use Activity Component for Show/Hide21862187**Impact: MEDIUM (preserves state/DOM)**21882189Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.21902191**Usage:**21922193```tsx2194import { Activity } from 'react'21952196function Dropdown({ isOpen }: Props) {2197 return (2198 <Activity mode={isOpen ? 'visible' : 'hidden'}>2199 <ExpensiveMenu />2200 </Activity>2201 )2202}2203```22042205Avoids expensive re-renders and state loss.22062207### 6.8 Use defer or async on Script Tags22082209**Impact: HIGH (eliminates render-blocking)**22102211Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.22122213- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order22142215- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order22162217Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.22182219**Incorrect: blocks rendering**22202221```tsx2222export default function Document() {2223 return (2224 <html>2225 <head>2226 <script src="https://example.com/analytics.js" />2227 <script src="/scripts/utils.js" />2228 </head>2229 <body>{/* content */}</body>2230 </html>2231 )2232}2233```22342235**Correct: non-blocking**22362237```tsx2238import Script from 'next/script'22392240export default function Page() {2241 return (2242 <>2243 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />2244 <Script src="/scripts/utils.js" strategy="beforeInteractive" />2245 </>2246 )2247}2248```22492250**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:22512252Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)22532254### 6.9 Use Explicit Conditional Rendering22552256**Impact: LOW (prevents rendering 0 or NaN)**22572258Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.22592260**Incorrect: renders "0" when count is 0**22612262```tsx2263function Badge({ count }: { count: number }) {2264 return (2265 <div>2266 {count && <span className="badge">{count}</span>}2267 </div>2268 )2269}22702271// When count = 0, renders: <div>0</div>2272// When count = 5, renders: <div><span class="badge">5</span></div>2273```22742275**Correct: renders nothing when count is 0**22762277```tsx2278function Badge({ count }: { count: number }) {2279 return (2280 <div>2281 {count > 0 ? <span className="badge">{count}</span> : null}2282 </div>2283 )2284}22852286// When count = 0, renders: <div></div>2287// When count = 5, renders: <div><span class="badge">5</span></div>2288```22892290### 6.10 Use React DOM Resource Hints22912292**Impact: HIGH (reduces load time for critical resources)**22932294React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.22952296- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to22972298- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server22992300- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon23012302- **`preloadModule(href)`**: Fetch an ES module you'll use soon23032304- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script23052306- **`preinitModule(href)`**: Fetch and evaluate an ES module23072308**Example: preconnect to third-party APIs**23092310```tsx2311import { preconnect, prefetchDNS } from 'react-dom'23122313export default function App() {2314 prefetchDNS('https://analytics.example.com')2315 preconnect('https://api.example.com')23162317 return <main>{/* content */}</main>2318}2319```23202321**Example: preload critical fonts and styles**23222323```tsx2324import { preload, preinit } from 'react-dom'23252326export default function RootLayout({ children }) {2327 // Preload font file2328 preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })23292330 // Fetch and apply critical stylesheet immediately2331 preinit('/styles/critical.css', { as: 'style' })23322333 return (2334 <html>2335 <body>{children}</body>2336 </html>2337 )2338}2339```23402341**Example: preload modules for code-split routes**23422343```tsx2344import { preloadModule, preinitModule } from 'react-dom'23452346function Navigation() {2347 const preloadDashboard = () => {2348 preloadModule('/dashboard.js', { as: 'script' })2349 }23502351 return (2352 <nav>2353 <a href="/dashboard" onMouseEnter={preloadDashboard}>2354 Dashboard2355 </a>2356 </nav>2357 )2358}2359```23602361**When to use each:**23622363| API | Use case |23642365|-----|----------|23662367| `prefetchDNS` | Third-party domains you'll connect to later |23682369| `preconnect` | APIs or CDNs you'll fetch from immediately |23702371| `preload` | Critical resources needed for current page |23722373| `preloadModule` | JS modules for likely next navigation |23742375| `preinit` | Stylesheets/scripts that must execute early |23762377| `preinitModule` | ES modules that must execute early |23782379Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)23802381### 6.11 Use useTransition Over Manual Loading States23822383**Impact: LOW (reduces re-renders and improves code clarity)**23842385Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.23862387**Incorrect: manual loading state**23882389```tsx2390function SearchResults() {2391 const [query, setQuery] = useState('')2392 const [results, setResults] = useState([])2393 const [isLoading, setIsLoading] = useState(false)23942395 const handleSearch = async (value: string) => {2396 setIsLoading(true)2397 setQuery(value)2398 const data = await fetchResults(value)2399 setResults(data)2400 setIsLoading(false)2401 }24022403 return (2404 <>2405 <input onChange={(e) => handleSearch(e.target.value)} />2406 {isLoading && <Spinner />}2407 <ResultsList results={results} />2408 </>2409 )2410}2411```24122413**Correct: useTransition with built-in pending state**24142415```tsx2416import { useTransition, useState } from 'react'24172418function SearchResults() {2419 const [query, setQuery] = useState('')2420 const [results, setResults] = useState([])2421 const [isPending, startTransition] = useTransition()24222423 const handleSearch = (value: string) => {2424 setQuery(value) // Update input immediately24252426 startTransition(async () => {2427 // Fetch and update results2428 const data = await fetchResults(value)2429 setResults(data)2430 })2431 }24322433 return (2434 <>2435 <input onChange={(e) => handleSearch(e.target.value)} />2436 {isPending && <Spinner />}2437 <ResultsList results={results} />2438 </>2439 )2440}2441```24422443**Benefits:**24442445- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`24462447- **Error resilience**: Pending state correctly resets even if the transition throws24482449- **Better responsiveness**: Keeps the UI responsive during updates24502451- **Interrupt handling**: New transitions automatically cancel pending ones24522453Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)24542455---24562457## 7. JavaScript Performance24582459**Impact: LOW-MEDIUM**24602461Micro-optimizations for hot paths can add up to meaningful improvements.24622463### 7.1 Avoid Layout Thrashing24642465**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**24662467Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.24682469**This is OK: browser batches style changes**24702471```typescript2472function updateElementStyles(element: HTMLElement) {2473 // Each line invalidates style, but browser batches the recalculation2474 element.style.width = '100px'2475 element.style.height = '200px'2476 element.style.backgroundColor = 'blue'2477 element.style.border = '1px solid black'2478}2479```24802481**Incorrect: interleaved reads and writes force reflows**24822483```typescript2484function layoutThrashing(element: HTMLElement) {2485 element.style.width = '100px'2486 const width = element.offsetWidth // Forces reflow2487 element.style.height = '200px'2488 const height = element.offsetHeight // Forces another reflow2489}2490```24912492**Correct: batch writes, then read once**24932494```typescript2495function updateElementStyles(element: HTMLElement) {2496 // Batch all writes together2497 element.style.width = '100px'2498 element.style.height = '200px'2499 element.style.backgroundColor = 'blue'2500 element.style.border = '1px solid black'25012502 // Read after all writes are done (single reflow)2503 const { width, height } = element.getBoundingClientRect()2504}2505```25062507**Correct: batch reads, then writes**25082509```typescript2510function updateElementStyles(element: HTMLElement) {2511 element.classList.add('highlighted-box')25122513 const { width, height } = element.getBoundingClientRect()2514}2515```25162517**Better: use CSS classes**25182519**React example:**25202521```tsx2522// Incorrect: interleaving style changes with layout queries2523function Box({ isHighlighted }: { isHighlighted: boolean }) {2524 const ref = useRef<HTMLDivElement>(null)25252526 useEffect(() => {2527 if (ref.current && isHighlighted) {2528 ref.current.style.width = '100px'2529 const width = ref.current.offsetWidth // Forces layout2530 ref.current.style.height = '200px'2531 }2532 }, [isHighlighted])25332534 return <div ref={ref}>Content</div>2535}25362537// Correct: toggle class2538function Box({ isHighlighted }: { isHighlighted: boolean }) {2539 return (2540 <div className={isHighlighted ? 'highlighted-box' : ''}>2541 Content2542 </div>2543 )2544}2545```25462547Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.25482549See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.25502551### 7.2 Build Index Maps for Repeated Lookups25522553**Impact: LOW-MEDIUM (1M ops to 2K ops)**25542555Multiple `.find()` calls by the same key should use a Map.25562557**Incorrect (O(n) per lookup):**25582559```typescript2560function processOrders(orders: Order[], users: User[]) {2561 return orders.map(order => ({2562 ...order,2563 user: users.find(u => u.id === order.userId)2564 }))2565}2566```25672568**Correct (O(1) per lookup):**25692570```typescript2571function processOrders(orders: Order[], users: User[]) {2572 const userById = new Map(users.map(u => [u.id, u]))25732574 return orders.map(order => ({2575 ...order,2576 user: userById.get(order.userId)2577 }))2578}2579```25802581Build map once (O(n)), then all lookups are O(1).25822583For 1000 orders × 1000 users: 1M ops → 2K ops.25842585### 7.3 Cache Property Access in Loops25862587**Impact: LOW-MEDIUM (reduces lookups)**25882589Cache object property lookups in hot paths.25902591**Incorrect: 3 lookups × N iterations**25922593```typescript2594for (let i = 0; i < arr.length; i++) {2595 process(obj.config.settings.value)2596}2597```25982599**Correct: 1 lookup total**26002601```typescript2602const value = obj.config.settings.value2603const len = arr.length2604for (let i = 0; i < len; i++) {2605 process(value)2606}2607```26082609### 7.4 Cache Repeated Function Calls26102611**Impact: MEDIUM (avoid redundant computation)**26122613Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.26142615**Incorrect: redundant computation**26162617```typescript2618function ProjectList({ projects }: { projects: Project[] }) {2619 return (2620 <div>2621 {projects.map(project => {2622 // slugify() called 100+ times for same project names2623 const slug = slugify(project.name)26242625 return <ProjectCard key={project.id} slug={slug} />2626 })}2627 </div>2628 )2629}2630```26312632**Correct: cached results**26332634```typescript2635// Module-level cache2636const slugifyCache = new Map<string, string>()26372638function cachedSlugify(text: string): string {2639 if (slugifyCache.has(text)) {2640 return slugifyCache.get(text)!2641 }2642 const result = slugify(text)2643 slugifyCache.set(text, result)2644 return result2645}26462647function ProjectList({ projects }: { projects: Project[] }) {2648 return (2649 <div>2650 {projects.map(project => {2651 // Computed only once per unique project name2652 const slug = cachedSlugify(project.name)26532654 return <ProjectCard key={project.id} slug={slug} />2655 })}2656 </div>2657 )2658}2659```26602661**Simpler pattern for single-value functions:**26622663```typescript2664let isLoggedInCache: boolean | null = null26652666function isLoggedIn(): boolean {2667 if (isLoggedInCache !== null) {2668 return isLoggedInCache2669 }26702671 isLoggedInCache = document.cookie.includes('auth=')2672 return isLoggedInCache2673}26742675// Clear cache when auth changes2676function onAuthChange() {2677 isLoggedInCache = null2678}2679```26802681Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.26822683Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)26842685### 7.5 Cache Storage API Calls26862687**Impact: LOW-MEDIUM (reduces expensive I/O)**26882689`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.26902691**Incorrect: reads storage on every call**26922693```typescript2694function getTheme() {2695 return localStorage.getItem('theme') ?? 'light'2696}2697// Called 10 times = 10 storage reads2698```26992700**Correct: Map cache**27012702```typescript2703const storageCache = new Map<string, string | null>()27042705function getLocalStorage(key: string) {2706 if (!storageCache.has(key)) {2707 storageCache.set(key, localStorage.getItem(key))2708 }2709 return storageCache.get(key)2710}27112712function setLocalStorage(key: string, value: string) {2713 localStorage.setItem(key, value)2714 storageCache.set(key, value) // keep cache in sync2715}2716```27172718Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.27192720**Cookie caching:**27212722```typescript2723let cookieCache: Record<string, string> | null = null27242725function getCookie(name: string) {2726 if (!cookieCache) {2727 cookieCache = Object.fromEntries(2728 document.cookie.split('; ').map(c => c.split('='))2729 )2730 }2731 return cookieCache[name]2732}2733```27342735**Important: invalidate on external changes**27362737```typescript2738window.addEventListener('storage', (e) => {2739 if (e.key) storageCache.delete(e.key)2740})27412742document.addEventListener('visibilitychange', () => {2743 if (document.visibilityState === 'visible') {2744 storageCache.clear()2745 }2746})2747```27482749If storage can change externally (another tab, server-set cookies), invalidate cache:27502751### 7.6 Combine Multiple Array Iterations27522753**Impact: LOW-MEDIUM (reduces iterations)**27542755Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.27562757**Incorrect: 3 iterations**27582759```typescript2760const admins = users.filter(u => u.isAdmin)2761const testers = users.filter(u => u.isTester)2762const inactive = users.filter(u => !u.isActive)2763```27642765**Correct: 1 iteration**27662767```typescript2768const admins: User[] = []2769const testers: User[] = []2770const inactive: User[] = []27712772for (const user of users) {2773 if (user.isAdmin) admins.push(user)2774 if (user.isTester) testers.push(user)2775 if (!user.isActive) inactive.push(user)2776}2777```27782779### 7.7 Early Length Check for Array Comparisons27802781**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**27822783When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.27842785In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).27862787**Incorrect: always runs expensive comparison**27882789```typescript2790function hasChanges(current: string[], original: string[]) {2791 // Always sorts and joins, even when lengths differ2792 return current.sort().join() !== original.sort().join()2793}2794```27952796Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.27972798**Correct (O(1) length check first):**27992800```typescript2801function hasChanges(current: string[], original: string[]) {2802 // Early return if lengths differ2803 if (current.length !== original.length) {2804 return true2805 }2806 // Only sort when lengths match2807 const currentSorted = current.toSorted()2808 const originalSorted = original.toSorted()2809 for (let i = 0; i < currentSorted.length; i++) {2810 if (currentSorted[i] !== originalSorted[i]) {2811 return true2812 }2813 }2814 return false2815}2816```28172818This new approach is more efficient because:28192820- It avoids the overhead of sorting and joining the arrays when lengths differ28212822- It avoids consuming memory for the joined strings (especially important for large arrays)28232824- It avoids mutating the original arrays28252826- It returns early when a difference is found28272828### 7.8 Early Return from Functions28292830**Impact: LOW-MEDIUM (avoids unnecessary computation)**28312832Return early when result is determined to skip unnecessary processing.28332834**Incorrect: processes all items even after finding answer**28352836```typescript2837function validateUsers(users: User[]) {2838 let hasError = false2839 let errorMessage = ''28402841 for (const user of users) {2842 if (!user.email) {2843 hasError = true2844 errorMessage = 'Email required'2845 }2846 if (!user.name) {2847 hasError = true2848 errorMessage = 'Name required'2849 }2850 // Continues checking all users even after error found2851 }28522853 return hasError ? { valid: false, error: errorMessage } : { valid: true }2854}2855```28562857**Correct: returns immediately on first error**28582859```typescript2860function validateUsers(users: User[]) {2861 for (const user of users) {2862 if (!user.email) {2863 return { valid: false, error: 'Email required' }2864 }2865 if (!user.name) {2866 return { valid: false, error: 'Name required' }2867 }2868 }28692870 return { valid: true }2871}2872```28732874### 7.9 Hoist RegExp Creation28752876**Impact: LOW-MEDIUM (avoids recreation)**28772878Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.28792880**Incorrect: new RegExp every render**28812882```tsx2883function Highlighter({ text, query }: Props) {2884 const regex = new RegExp(`(${query})`, 'gi')2885 const parts = text.split(regex)2886 return <>{parts.map((part, i) => ...)}</>2887}2888```28892890**Correct: memoize or hoist**28912892```tsx2893const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/28942895function Highlighter({ text, query }: Props) {2896 const regex = useMemo(2897 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),2898 [query]2899 )2900 const parts = text.split(regex)2901 return <>{parts.map((part, i) => ...)}</>2902}2903```29042905**Warning: global regex has mutable state**29062907```typescript2908const regex = /foo/g2909regex.test('foo') // true, lastIndex = 32910regex.test('foo') // false, lastIndex = 02911```29122913Global regex (`/g`) has mutable `lastIndex` state:29142915### 7.10 Use flatMap to Map and Filter in One Pass29162917**Impact: LOW-MEDIUM (eliminates intermediate array)**29182919Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.29202921**Incorrect: 2 iterations, intermediate array**29222923```typescript2924const userNames = users2925 .map(user => user.isActive ? user.name : null)2926 .filter(Boolean)2927```29282929**Correct: 1 iteration, no intermediate array**29302931```typescript2932const userNames = users.flatMap(user =>2933 user.isActive ? [user.name] : []2934)2935```29362937**More examples:**29382939```typescript2940// Extract valid emails from responses2941// Before2942const emails = responses2943 .map(r => r.success ? r.data.email : null)2944 .filter(Boolean)29452946// After2947const emails = responses.flatMap(r =>2948 r.success ? [r.data.email] : []2949)29502951// Parse and filter valid numbers2952// Before2953const numbers = strings2954 .map(s => parseInt(s, 10))2955 .filter(n => !isNaN(n))29562957// After2958const numbers = strings.flatMap(s => {2959 const n = parseInt(s, 10)2960 return isNaN(n) ? [] : [n]2961})2962```29632964**When to use:**29652966- Transforming items while filtering some out29672968- Conditional mapping where some inputs produce no output29692970- Parsing/validating where invalid inputs should be skipped29712972### 7.11 Use Loop for Min/Max Instead of Sort29732974**Impact: LOW (O(n) instead of O(n log n))**29752976Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.29772978**Incorrect (O(n log n) - sort to find latest):**29792980```typescript2981interface Project {2982 id: string2983 name: string2984 updatedAt: number2985}29862987function getLatestProject(projects: Project[]) {2988 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)2989 return sorted[0]2990}2991```29922993Sorts the entire array just to find the maximum value.29942995**Incorrect (O(n log n) - sort for oldest and newest):**29962997```typescript2998function getOldestAndNewest(projects: Project[]) {2999 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)3000 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }3001}3002```30033004Still sorts unnecessarily when only min/max are needed.30053006**Correct (O(n) - single loop):**30073008```typescript3009function getLatestProject(projects: Project[]) {3010 if (projects.length === 0) return null30113012 let latest = projects[0]30133014 for (let i = 1; i < projects.length; i++) {3015 if (projects[i].updatedAt > latest.updatedAt) {3016 latest = projects[i]3017 }3018 }30193020 return latest3021}30223023function getOldestAndNewest(projects: Project[]) {3024 if (projects.length === 0) return { oldest: null, newest: null }30253026 let oldest = projects[0]3027 let newest = projects[0]30283029 for (let i = 1; i < projects.length; i++) {3030 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]3031 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]3032 }30333034 return { oldest, newest }3035}3036```30373038Single pass through the array, no copying, no sorting.30393040**Alternative: Math.min/Math.max for small arrays**30413042```typescript3043const numbers = [5, 2, 8, 1, 9]3044const min = Math.min(...numbers)3045const max = Math.max(...numbers)3046```30473048This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.30493050### 7.12 Use Set/Map for O(1) Lookups30513052**Impact: LOW-MEDIUM (O(n) to O(1))**30533054Convert arrays to Set/Map for repeated membership checks.30553056**Incorrect (O(n) per check):**30573058```typescript3059const allowedIds = ['a', 'b', 'c', ...]3060items.filter(item => allowedIds.includes(item.id))3061```30623063**Correct (O(1) per check):**30643065```typescript3066const allowedIds = new Set(['a', 'b', 'c', ...])3067items.filter(item => allowedIds.has(item.id))3068```30693070### 7.13 Use toSorted() Instead of sort() for Immutability30713072**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**30733074`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.30753076**Incorrect: mutates original array**30773078```typescript3079function UserList({ users }: { users: User[] }) {3080 // Mutates the users prop array!3081 const sorted = useMemo(3082 () => users.sort((a, b) => a.name.localeCompare(b.name)),3083 [users]3084 )3085 return <div>{sorted.map(renderUser)}</div>3086}3087```30883089**Correct: creates new array**30903091```typescript3092function UserList({ users }: { users: User[] }) {3093 // Creates new sorted array, original unchanged3094 const sorted = useMemo(3095 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),3096 [users]3097 )3098 return <div>{sorted.map(renderUser)}</div>3099}3100```31013102**Why this matters in React:**310331041. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only310531062. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior31073108**Browser support: fallback for older browsers**31093110```typescript3111// Fallback for older browsers3112const sorted = [...items].sort((a, b) => a.value - b.value)3113```31143115`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:31163117**Other immutable array methods:**31183119- `.toSorted()` - immutable sort31203121- `.toReversed()` - immutable reverse31223123- `.toSpliced()` - immutable splice31243125- `.with()` - immutable element replacement31263127---31283129## 8. Advanced Patterns31303131**Impact: LOW**31323133Advanced patterns for specific cases that require careful implementation.31343135### 8.1 Initialize App Once, Not Per Mount31363137**Impact: LOW-MEDIUM (avoids duplicate init in development)**31383139Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.31403141**Incorrect: runs twice in dev, re-runs on remount**31423143```tsx3144function Comp() {3145 useEffect(() => {3146 loadFromStorage()3147 checkAuthToken()3148 }, [])31493150 // ...3151}3152```31533154**Correct: once per app load**31553156```tsx3157let didInit = false31583159function Comp() {3160 useEffect(() => {3161 if (didInit) return3162 didInit = true3163 loadFromStorage()3164 checkAuthToken()3165 }, [])31663167 // ...3168}3169```31703171Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)31723173### 8.2 Store Event Handlers in Refs31743175**Impact: LOW (stable subscriptions)**31763177Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.31783179**Incorrect: re-subscribes on every render**31803181```tsx3182function useWindowEvent(event: string, handler: (e) => void) {3183 useEffect(() => {3184 window.addEventListener(event, handler)3185 return () => window.removeEventListener(event, handler)3186 }, [event, handler])3187}3188```31893190**Correct: stable subscription**31913192```tsx3193import { useEffectEvent } from 'react'31943195function useWindowEvent(event: string, handler: (e) => void) {3196 const onEvent = useEffectEvent(handler)31973198 useEffect(() => {3199 window.addEventListener(event, onEvent)3200 return () => window.removeEventListener(event, onEvent)3201 }, [event])3202}3203```32043205**Alternative: use `useEffectEvent` if you're on latest React:**32063207`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.32083209### 8.3 useEffectEvent for Stable Callback Refs32103211**Impact: LOW (prevents effect re-runs)**32123213Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.32143215**Incorrect: effect re-runs on every callback change**32163217```tsx3218function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3219 const [query, setQuery] = useState('')32203221 useEffect(() => {3222 const timeout = setTimeout(() => onSearch(query), 300)3223 return () => clearTimeout(timeout)3224 }, [query, onSearch])3225}3226```32273228**Correct: using React's useEffectEvent**32293230```tsx3231import { useEffectEvent } from 'react';32323233function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3234 const [query, setQuery] = useState('')3235 const onSearchEvent = useEffectEvent(onSearch)32363237 useEffect(() => {3238 const timeout = setTimeout(() => onSearchEvent(query), 300)3239 return () => clearTimeout(timeout)3240 }, [query])3241}3242```32433244---32453246## References324732481. [https://react.dev](https://react.dev)32492. [https://nextjs.org](https://nextjs.org)32503. [https://swr.vercel.app](https://swr.vercel.app)32514. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)32525. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)32536. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)32547. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)3255
Also in CherryHQ/cherry-studio
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| CherryHQ/cherry-studioCLAUDE.md · 49k | CLAUDE.md | setuptestlint-formatstyle+7 | 85/100 | 3 days ago | |
| CherryHQ/cherry-studiopackages/provider-registry/CLAUDE.md · 49k | CLAUDE.md | testgitdo-notagent-behaviour | 80/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago | |
| SkeneTechnologies/skene-cookbookAGENTS.md · 51 | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 2 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| aaif-goose/gooseAGENTS.md · 52k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 67k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 2 days ago |
