AGENTS.md
.cursor/skills/web-frontend/references/react-best-practices/AGENTS.mdAGENTS.md
Quality
65/100
Scores the file, not the repository.Length
13,174 words
82 headings · 174 code blocksRepository
0
— · pushed 1 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 [Check Cheap Conditions Before Async Flags](#11-check-cheap-conditions-before-async-flags)25 - 1.2 [Defer Await Until Needed](#12-defer-await-until-needed)26 - 1.3 [Dependency-Based Parallelization](#13-dependency-based-parallelization)27 - 1.4 [Prevent Waterfall Chains in API Routes](#14-prevent-waterfall-chains-in-api-routes)28 - 1.5 [Promise.all() for Independent Operations](#15-promiseall-for-independent-operations)29 - 1.6 [Strategic Suspense Boundaries](#16-strategic-suspense-boundaries)302. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**31 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)32 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)33 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)34 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)35 - 2.5 [Prefer Statically Analyzable Paths](#25-prefer-statically-analyzable-paths)36 - 2.6 [Preload Based on User Intent](#26-preload-based-on-user-intent)373. [Server-Side Performance](#3-server-side-performance) — **HIGH**38 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)39 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)40 - 3.3 [Avoid Shared Module State for Request Data](#33-avoid-shared-module-state-for-request-data)41 - 3.4 [Cross-Request LRU Caching](#34-cross-request-lru-caching)42 - 3.5 [Hoist Static I/O to Module Level](#35-hoist-static-io-to-module-level)43 - 3.6 [Minimize Serialization at RSC Boundaries](#36-minimize-serialization-at-rsc-boundaries)44 - 3.7 [Parallel Data Fetching with Component Composition](#37-parallel-data-fetching-with-component-composition)45 - 3.8 [Parallel Nested Data Fetching](#38-parallel-nested-data-fetching)46 - 3.9 [Per-Request Deduplication with React.cache()](#39-per-request-deduplication-with-reactcache)47 - 3.10 [Use after() for Non-Blocking Operations](#310-use-after-for-non-blocking-operations)484. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**49 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)50 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)51 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)52 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)535. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**54 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)55 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)56 - 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)57 - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)58 - 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)59 - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)60 - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)61 - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)62 - 5.9 [Split Combined Hook Computations](#59-split-combined-hook-computations)63 - 5.10 [Subscribe to Derived State](#510-subscribe-to-derived-state)64 - 5.11 [Use Functional setState Updates](#511-use-functional-setstate-updates)65 - 5.12 [Use Lazy State Initialization](#512-use-lazy-state-initialization)66 - 5.13 [Use Transitions for Non-Urgent Updates](#513-use-transitions-for-non-urgent-updates)67 - 5.14 [Use useDeferredValue for Expensive Derived Renders](#514-use-usedeferredvalue-for-expensive-derived-renders)68 - 5.15 [Use useRef for Transient Values](#515-use-useref-for-transient-values)696. [Rendering Performance](#6-rendering-performance) — **MEDIUM**70 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)71 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)72 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)73 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)74 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)75 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)76 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)77 - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)78 - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)79 - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)80 - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)817. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**82 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)83 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)84 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)85 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)86 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)87 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)88 - 7.7 [Defer Non-Critical Work with requestIdleCallback](#77-defer-non-critical-work-with-requestidlecallback)89 - 7.8 [Early Length Check for Array Comparisons](#78-early-length-check-for-array-comparisons)90 - 7.9 [Early Return from Functions](#79-early-return-from-functions)91 - 7.10 [Hoist RegExp Creation](#710-hoist-regexp-creation)92 - 7.11 [Use flatMap to Map and Filter in One Pass](#711-use-flatmap-to-map-and-filter-in-one-pass)93 - 7.12 [Use Loop for Min/Max Instead of Sort](#712-use-loop-for-minmax-instead-of-sort)94 - 7.13 [Use Set/Map for O(1) Lookups](#713-use-setmap-for-o1-lookups)95 - 7.14 [Use toSorted() Instead of sort() for Immutability](#714-use-tosorted-instead-of-sort-for-immutability)968. [Advanced Patterns](#8-advanced-patterns) — **LOW**97 - 8.1 [Do Not Put Effect Events in Dependency Arrays](#81-do-not-put-effect-events-in-dependency-arrays)98 - 8.2 [Initialize App Once, Not Per Mount](#82-initialize-app-once-not-per-mount)99 - 8.3 [Store Event Handlers in Refs](#83-store-event-handlers-in-refs)100 - 8.4 [useEffectEvent for Stable Callback Refs](#84-useeffectevent-for-stable-callback-refs)101102---103104## 1. Eliminating Waterfalls105106**Impact: CRITICAL**107108Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.109110### 1.1 Check Cheap Conditions Before Async Flags111112**Impact: HIGH (avoids unnecessary async work when a synchronous guard already fails)**113114When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.115116This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.117118**Incorrect:**119120```typescript121const someFlag = await getFlag()122123if (someFlag && someCondition) {124 // ...125}126```127128**Correct:**129130```typescript131if (someCondition) {132 const someFlag = await getFlag()133 if (someFlag) {134 // ...135 }136}137```138139This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.140141Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.142143### 1.2 Defer Await Until Needed144145**Impact: HIGH (avoids blocking unused code paths)**146147Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.148149**Incorrect: blocks both branches**150151```typescript152async function handleRequest(userId: string, skipProcessing: boolean) {153 const userData = await fetchUserData(userId)154155 if (skipProcessing) {156 // Returns immediately but still waited for userData157 return { skipped: true }158 }159160 // Only this branch uses userData161 return processUserData(userData)162}163```164165**Correct: only blocks when needed**166167```typescript168async function handleRequest(userId: string, skipProcessing: boolean) {169 if (skipProcessing) {170 // Returns immediately without waiting171 return { skipped: true }172 }173174 // Fetch only when needed175 const userData = await fetchUserData(userId)176 return processUserData(userData)177}178```179180**Another example: early return optimization**181182```typescript183// Incorrect: always fetches permissions184async function updateResource(resourceId: string, userId: string) {185 const permissions = await fetchPermissions(userId)186 const resource = await getResource(resourceId)187188 if (!resource) {189 return { error: 'Not found' }190 }191192 if (!permissions.canEdit) {193 return { error: 'Forbidden' }194 }195196 return await updateResourceData(resource, permissions)197}198199// Correct: fetches only when needed200async function updateResource(resourceId: string, userId: string) {201 const resource = await getResource(resourceId)202203 if (!resource) {204 return { error: 'Not found' }205 }206207 const permissions = await fetchPermissions(userId)208209 if (!permissions.canEdit) {210 return { error: 'Forbidden' }211 }212213 return await updateResourceData(resource, permissions)214}215```216217This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.218219For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).220221### 1.3 Dependency-Based Parallelization222223**Impact: CRITICAL (2-10× improvement)**224225For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.226227**Incorrect: profile waits for config unnecessarily**228229```typescript230const [user, config] = await Promise.all([231 fetchUser(),232 fetchConfig()233])234const profile = await fetchProfile(user.id)235```236237**Correct: config and profile run in parallel**238239```typescript240import { all } from 'better-all'241242const { user, config, profile } = await all({243 async user() { return fetchUser() },244 async config() { return fetchConfig() },245 async profile() {246 return fetchProfile((await this.$.user).id)247 }248})249```250251**Alternative without extra dependencies:**252253```typescript254const userPromise = fetchUser()255const profilePromise = userPromise.then(user => fetchProfile(user.id))256257const [user, config, profile] = await Promise.all([258 userPromise,259 fetchConfig(),260 profilePromise261])262```263264We can also create all the promises first, and do `Promise.all()` at the end.265266Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)267268### 1.4 Prevent Waterfall Chains in API Routes269270**Impact: CRITICAL (2-10× improvement)**271272In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.273274**Incorrect: config waits for auth, data waits for both**275276```typescript277export async function GET(request: Request) {278 const session = await auth()279 const config = await fetchConfig()280 const data = await fetchData(session.user.id)281 return Response.json({ data, config })282}283```284285**Correct: auth and config start immediately**286287```typescript288export async function GET(request: Request) {289 const sessionPromise = auth()290 const configPromise = fetchConfig()291 const session = await sessionPromise292 const [config, data] = await Promise.all([293 configPromise,294 fetchData(session.user.id)295 ])296 return Response.json({ data, config })297}298```299300For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).301302### 1.5 Promise.all() for Independent Operations303304**Impact: CRITICAL (2-10× improvement)**305306When async operations have no interdependencies, execute them concurrently using `Promise.all()`.307308**Incorrect: sequential execution, 3 round trips**309310```typescript311const user = await fetchUser()312const posts = await fetchPosts()313const comments = await fetchComments()314```315316**Correct: parallel execution, 1 round trip**317318```typescript319const [user, posts, comments] = await Promise.all([320 fetchUser(),321 fetchPosts(),322 fetchComments()323])324```325326### 1.6 Strategic Suspense Boundaries327328**Impact: HIGH (faster initial paint)**329330Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.331332**Incorrect: wrapper blocked by data fetching**333334```tsx335async function Page() {336 const data = await fetchData() // Blocks entire page337338 return (339 <div>340 <div>Sidebar</div>341 <div>Header</div>342 <div>343 <DataDisplay data={data} />344 </div>345 <div>Footer</div>346 </div>347 )348}349```350351The entire layout waits for data even though only the middle section needs it.352353**Correct: wrapper shows immediately, data streams in**354355```tsx356function Page() {357 return (358 <div>359 <div>Sidebar</div>360 <div>Header</div>361 <div>362 <Suspense fallback={<Skeleton />}>363 <DataDisplay />364 </Suspense>365 </div>366 <div>Footer</div>367 </div>368 )369}370371async function DataDisplay() {372 const data = await fetchData() // Only blocks this component373 return <div>{data.content}</div>374}375```376377Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.378379**Alternative: share promise across components**380381```tsx382function Page() {383 // Start fetch immediately, but don't await384 const dataPromise = fetchData()385386 return (387 <div>388 <div>Sidebar</div>389 <div>Header</div>390 <Suspense fallback={<Skeleton />}>391 <DataDisplay dataPromise={dataPromise} />392 <DataSummary dataPromise={dataPromise} />393 </Suspense>394 <div>Footer</div>395 </div>396 )397}398399function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {400 const data = use(dataPromise) // Unwraps the promise401 return <div>{data.content}</div>402}403404function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {405 const data = use(dataPromise) // Reuses the same promise406 return <div>{data.summary}</div>407}408```409410Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.411412**When NOT to use this pattern:**413414- Critical data needed for layout decisions (affects positioning)415416- SEO-critical content above the fold417418- Small, fast queries where suspense overhead isn't worth it419420- When you want to avoid layout shift (loading → content jump)421422**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.423424---425426## 2. Bundle Size Optimization427428**Impact: CRITICAL**429430Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.431432### 2.1 Avoid Barrel File Imports433434**Impact: CRITICAL (200-800ms import cost, slow builds)**435436Import 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'`).437438Popular 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.439440**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.441442**Incorrect: imports entire library**443444```tsx445import { Check, X, Menu } from 'lucide-react'446// Loads 1,583 modules, takes ~2.8s extra in dev447// Runtime cost: 200-800ms on every cold start448449import { Button, TextField } from '@mui/material'450// Loads 2,225 modules, takes ~4.2s extra in dev451```452453**Correct - Next.js 13.5+ (recommended):**454455```tsx456// Keep the standard imports - Next.js transforms them to direct imports457import { Check, X, Menu } from 'lucide-react'458// Full TypeScript support, no manual path wrangling459```460461This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.462463**Correct - Direct imports (non-Next.js projects):**464465```tsx466import Button from '@mui/material/Button'467import TextField from '@mui/material/TextField'468// Loads only what you use469```470471> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.472473These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.474475Libraries 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`.476477Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)478479### 2.2 Conditional Module Loading480481**Impact: HIGH (loads large data only when needed)**482483Load large data or modules only when a feature is activated.484485**Example: lazy-load animation frames**486487```tsx488function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {489 const [frames, setFrames] = useState<Frame[] | null>(null)490491 useEffect(() => {492 if (enabled && !frames && typeof window !== 'undefined') {493 import('./animation-frames.js')494 .then(mod => setFrames(mod.frames))495 .catch(() => setEnabled(false))496 }497 }, [enabled, frames, setEnabled])498499 if (!frames) return <Skeleton />500 return <Canvas frames={frames} />501}502```503504The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.505506### 2.3 Defer Non-Critical Third-Party Libraries507508**Impact: MEDIUM (loads after hydration)**509510Analytics, logging, and error tracking don't block user interaction. Load them after hydration.511512**Incorrect: blocks initial bundle**513514```tsx515import { Analytics } from '@vercel/analytics/react'516517export default function RootLayout({ children }) {518 return (519 <html>520 <body>521 {children}522 <Analytics />523 </body>524 </html>525 )526}527```528529**Correct: loads after hydration**530531```tsx532import dynamic from 'next/dynamic'533534const Analytics = dynamic(535 () => import('@vercel/analytics/react').then(m => m.Analytics),536 { ssr: false }537)538539export default function RootLayout({ children }) {540 return (541 <html>542 <body>543 {children}544 <Analytics />545 </body>546 </html>547 )548}549```550551### 2.4 Dynamic Imports for Heavy Components552553**Impact: CRITICAL (directly affects TTI and LCP)**554555Use `next/dynamic` to lazy-load large components not needed on initial render.556557**Incorrect: Monaco bundles with main chunk ~300KB**558559```tsx560import { MonacoEditor } from './monaco-editor'561562function CodePanel({ code }: { code: string }) {563 return <MonacoEditor value={code} />564}565```566567**Correct: Monaco loads on demand**568569```tsx570import dynamic from 'next/dynamic'571572const MonacoEditor = dynamic(573 () => import('./monaco-editor').then(m => m.MonacoEditor),574 { ssr: false }575)576577function CodePanel({ code }: { code: string }) {578 return <MonacoEditor value={code} />579}580```581582### 2.5 Prefer Statically Analyzable Paths583584**Impact: HIGH (avoids accidental broad bundles and file traces)**585586Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.587588Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with `import()` or reading files in server/build code.589590When analysis becomes too broad, the cost is real:591592- Larger server bundles593594- Slower builds595596- Worse cold starts597598- More memory use599600**Incorrect: the bundler cannot tell what may be imported**601602```ts603const PAGE_MODULES = {604 home: './pages/home',605 settings: './pages/settings',606} as const607608const Page = await import(PAGE_MODULES[pageName])609```610611**Correct: use an explicit map of allowed modules**612613```ts614const PAGE_MODULES = {615 home: () => import('./pages/home'),616 settings: () => import('./pages/settings'),617} as const618619const Page = await PAGE_MODULES[pageName]()620```621622**Incorrect: a 2-value enum still hides the final path from static analysis**623624```ts625const baseDir = path.join(process.cwd(), 'content/' + contentKind)626```627628**Correct: make each final path literal at the callsite**629630```ts631const baseDir =632 kind === ContentKind.Blog633 ? path.join(process.cwd(), 'content/blog')634 : path.join(process.cwd(), 'content/docs')635```636637In Next.js server code, this matters for output file tracing too. `path.join(process.cwd(), someVar)` can widen the traced file set because Next.js statically analyze `import`, `require`, and `fs` usage.638639Reference: [https://nextjs.org/docs/app/api-reference/config/next-config-js/output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output), [https://nextjs.org/learn/seo/dynamic-imports](https://nextjs.org/learn/seo/dynamic-imports), [https://vite.dev/guide/features.html](https://vite.dev/guide/features.html), [https://esbuild.github.io/api/](https://esbuild.github.io/api/), [https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars](https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars), [https://webpack.js.org/guides/dependency-management/](https://webpack.js.org/guides/dependency-management/)640641### 2.6 Preload Based on User Intent642643**Impact: MEDIUM (reduces perceived latency)**644645Preload heavy bundles before they're needed to reduce perceived latency.646647**Example: preload on hover/focus**648649```tsx650function EditorButton({ onClick }: { onClick: () => void }) {651 const preload = () => {652 if (typeof window !== 'undefined') {653 void import('./monaco-editor')654 }655 }656657 return (658 <button659 onMouseEnter={preload}660 onFocus={preload}661 onClick={onClick}662 >663 Open Editor664 </button>665 )666}667```668669**Example: preload when feature flag is enabled**670671```tsx672function FlagsProvider({ children, flags }: Props) {673 useEffect(() => {674 if (flags.editorEnabled && typeof window !== 'undefined') {675 void import('./monaco-editor').then(mod => mod.init())676 }677 }, [flags.editorEnabled])678679 return <FlagsContext.Provider value={flags}>680 {children}681 </FlagsContext.Provider>682}683```684685The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.686687---688689## 3. Server-Side Performance690691**Impact: HIGH**692693Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.694695### 3.1 Authenticate Server Actions Like API Routes696697**Impact: CRITICAL (prevents unauthorized access to server mutations)**698699Server 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.700701Next.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."702703**Incorrect: no authentication check**704705```typescript706'use server'707708export async function deleteUser(userId: string) {709 // Anyone can call this! No auth check710 await db.user.delete({ where: { id: userId } })711 return { success: true }712}713```714715**Correct: authentication inside the action**716717```typescript718'use server'719720import { verifySession } from '@/lib/auth'721import { unauthorized } from '@/lib/errors'722723export async function deleteUser(userId: string) {724 // Always check auth inside the action725 const session = await verifySession()726727 if (!session) {728 throw unauthorized('Must be logged in')729 }730731 // Check authorization too732 if (session.user.role !== 'admin' && session.user.id !== userId) {733 throw unauthorized('Cannot delete other users')734 }735736 await db.user.delete({ where: { id: userId } })737 return { success: true }738}739```740741**With input validation:**742743```typescript744'use server'745746import { verifySession } from '@/lib/auth'747import { z } from 'zod'748749const updateProfileSchema = z.object({750 userId: z.string().uuid(),751 name: z.string().min(1).max(100),752 email: z.string().email()753})754755export async function updateProfile(data: unknown) {756 // Validate input first757 const validated = updateProfileSchema.parse(data)758759 // Then authenticate760 const session = await verifySession()761 if (!session) {762 throw new Error('Unauthorized')763 }764765 // Then authorize766 if (session.user.id !== validated.userId) {767 throw new Error('Can only update own profile')768 }769770 // Finally perform the mutation771 await db.user.update({772 where: { id: validated.userId },773 data: {774 name: validated.name,775 email: validated.email776 }777 })778779 return { success: true }780}781```782783Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)784785### 3.2 Avoid Duplicate Serialization in RSC Props786787**Impact: LOW (reduces network payload by avoiding duplicate serialization)**788789RSC→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.790791**Incorrect: duplicates array**792793```tsx794// RSC: sends 6 strings (2 arrays × 3 items)795<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />796```797798**Correct: sends 3 strings**799800```tsx801// RSC: send once802<ClientList usernames={usernames} />803804// Client: transform there805'use client'806const sorted = useMemo(() => [...usernames].sort(), [usernames])807```808809**Nested deduplication behavior:**810811```tsx812// string[] - duplicates everything813usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings814815// object[] - duplicates array structure only816users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)817```818819Deduplication works recursively. Impact varies by data type:820821- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated822823- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference824825**Operations breaking deduplication: create new references**826827- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`828829- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`830831**More examples:**832833```tsx834// ❌ Bad835<C users={users} active={users.filter(u => u.active)} />836<C product={product} productName={product.name} />837838// ✅ Good839<C users={users} />840<C product={product} />841// Do filtering/destructuring in client842```843844**Exception:** Pass derived data when transformation is expensive or client doesn't need original.845846### 3.3 Avoid Shared Module State for Request Data847848**Impact: HIGH (prevents concurrency bugs and request data leaks)**849850For React Server Components and client components rendered during SSR, avoid using mutable module-level variables to share request-scoped data. Server renders can run concurrently in the same process. If one render writes to shared module state and another render reads it, you can get race conditions, cross-request contamination, and security bugs where one user's data appears in another user's response.851852Treat module scope on the server as process-wide shared memory, not request-local state.853854**Incorrect: request data leaks across concurrent renders**855856```tsx857let currentUser: User | null = null858859export default async function Page() {860 currentUser = await auth()861 return <Dashboard />862}863864async function Dashboard() {865 return <div>{currentUser?.name}</div>866}867```868869If two requests overlap, request A can set `currentUser`, then request B overwrites it before request A finishes rendering `Dashboard`.870871**Correct: keep request data local to the render tree**872873```tsx874export default async function Page() {875 const user = await auth()876 return <Dashboard user={user} />877}878879function Dashboard({ user }: { user: User | null }) {880 return <div>{user?.name}</div>881}882```883884Safe exceptions:885886- Immutable static assets or config loaded once at module scope887888- Shared caches intentionally designed for cross-request reuse and keyed correctly889890- Process-wide singletons that do not store request- or user-specific mutable data891892For static assets and config, see [Hoist Static I/O to Module Level](./server-hoist-static-io.md).893894### 3.4 Cross-Request LRU Caching895896**Impact: HIGH (caches across requests)**897898`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.899900**Implementation:**901902```typescript903import { LRUCache } from 'lru-cache'904905const cache = new LRUCache<string, any>({906 max: 1000,907 ttl: 5 * 60 * 1000 // 5 minutes908})909910export async function getUser(id: string) {911 const cached = cache.get(id)912 if (cached) return cached913914 const user = await db.user.findUnique({ where: { id } })915 cache.set(id, user)916 return user917}918919// Request 1: DB query, result cached920// Request 2: cache hit, no DB query921```922923Use when sequential user actions hit multiple endpoints needing the same data within seconds.924925**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.926927**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.928929Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)930931### 3.5 Hoist Static I/O to Module Level932933**Impact: HIGH (avoids repeated file/network I/O per request)**934935When 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.936937**Incorrect: reads font file on every request**938939```typescript940// app/api/og/route.tsx941import { ImageResponse } from 'next/og'942943export async function GET(request: Request) {944 // Runs on EVERY request - expensive!945 const fontData = await fetch(946 new URL('./fonts/Inter.ttf', import.meta.url)947 ).then(res => res.arrayBuffer())948949 const logoData = await fetch(950 new URL('./images/logo.png', import.meta.url)951 ).then(res => res.arrayBuffer())952953 return new ImageResponse(954 <div style={{ fontFamily: 'Inter' }}>955 <img src={logoData} />956 Hello World957 </div>,958 { fonts: [{ name: 'Inter', data: fontData }] }959 )960}961```962963**Correct: loads once at module initialization**964965```typescript966// app/api/og/route.tsx967import { ImageResponse } from 'next/og'968969// Module-level: runs ONCE when module is first imported970const fontData = fetch(971 new URL('./fonts/Inter.ttf', import.meta.url)972).then(res => res.arrayBuffer())973974const logoData = fetch(975 new URL('./images/logo.png', import.meta.url)976).then(res => res.arrayBuffer())977978export async function GET(request: Request) {979 // Await the already-started promises980 const [font, logo] = await Promise.all([fontData, logoData])981982 return new ImageResponse(983 <div style={{ fontFamily: 'Inter' }}>984 <img src={logo} />985 Hello World986 </div>,987 { fonts: [{ name: 'Inter', data: font }] }988 )989}990```991992**Correct: synchronous fs at module level**993994```typescript995// app/api/og/route.tsx996import { ImageResponse } from 'next/og'997import { readFileSync } from 'fs'998import { join } from 'path'9991000// Synchronous read at module level - blocks only during module init1001const fontData = readFileSync(1002 join(process.cwd(), 'public/fonts/Inter.ttf')1003)10041005const logoData = readFileSync(1006 join(process.cwd(), 'public/images/logo.png')1007)10081009export async function GET(request: Request) {1010 return new ImageResponse(1011 <div style={{ fontFamily: 'Inter' }}>1012 <img src={logoData} />1013 Hello World1014 </div>,1015 { fonts: [{ name: 'Inter', data: fontData }] }1016 )1017}1018```10191020**Incorrect: reads config on every call**10211022```typescript1023import fs from 'node:fs/promises'10241025export async function processRequest(data: Data) {1026 const config = JSON.parse(1027 await fs.readFile('./config.json', 'utf-8')1028 )1029 const template = await fs.readFile('./template.html', 'utf-8')10301031 return render(template, data, config)1032}1033```10341035**Correct: hoists config and template to module level**10361037```typescript1038import fs from 'node:fs/promises'10391040const configPromise = fs1041 .readFile('./config.json', 'utf-8')1042 .then(JSON.parse)1043const templatePromise = fs.readFile('./template.html', 'utf-8')10441045export async function processRequest(data: Data) {1046 const [config, template] = await Promise.all([1047 configPromise,1048 templatePromise,1049 ])10501051 return render(template, data, config)1052}1053```10541055When to use this pattern:10561057- Loading fonts for OG image generation10581059- Loading static logos, icons, or watermarks10601061- Reading configuration files that don't change at runtime10621063- Loading email templates or other static templates10641065- Any static asset that's the same across all requests10661067When not to use this pattern:10681069- Assets that vary per request or user10701071- Files that may change during runtime (use caching with TTL instead)10721073- Large files that would consume too much memory if kept loaded10741075- Sensitive data that shouldn't persist in memory10761077With 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.10781079In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.10801081### 3.6 Minimize Serialization at RSC Boundaries10821083**Impact: HIGH (reduces data transfer size)**10841085The 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.10861087**Incorrect: serializes all 50 fields**10881089```tsx1090async function Page() {1091 const user = await fetchUser() // 50 fields1092 return <Profile user={user} />1093}10941095'use client'1096function Profile({ user }: { user: User }) {1097 return <div>{user.name}</div> // uses 1 field1098}1099```11001101**Correct: serializes only 1 field**11021103```tsx1104async function Page() {1105 const user = await fetchUser()1106 return <Profile name={user.name} />1107}11081109'use client'1110function Profile({ name }: { name: string }) {1111 return <div>{name}</div>1112}1113```11141115### 3.7 Parallel Data Fetching with Component Composition11161117**Impact: CRITICAL (eliminates server-side waterfalls)**11181119React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.11201121**Incorrect: Sidebar waits for Page's fetch to complete**11221123```tsx1124export default async function Page() {1125 const header = await fetchHeader()1126 return (1127 <div>1128 <div>{header}</div>1129 <Sidebar />1130 </div>1131 )1132}11331134async function Sidebar() {1135 const items = await fetchSidebarItems()1136 return <nav>{items.map(renderItem)}</nav>1137}1138```11391140**Correct: both fetch simultaneously**11411142```tsx1143async function Header() {1144 const data = await fetchHeader()1145 return <div>{data}</div>1146}11471148async function Sidebar() {1149 const items = await fetchSidebarItems()1150 return <nav>{items.map(renderItem)}</nav>1151}11521153export default function Page() {1154 return (1155 <div>1156 <Header />1157 <Sidebar />1158 </div>1159 )1160}1161```11621163**Alternative with children prop:**11641165```tsx1166async function Header() {1167 const data = await fetchHeader()1168 return <div>{data}</div>1169}11701171async function Sidebar() {1172 const items = await fetchSidebarItems()1173 return <nav>{items.map(renderItem)}</nav>1174}11751176function Layout({ children }: { children: ReactNode }) {1177 return (1178 <div>1179 <Header />1180 {children}1181 </div>1182 )1183}11841185export default function Page() {1186 return (1187 <Layout>1188 <Sidebar />1189 </Layout>1190 )1191}1192```11931194### 3.8 Parallel Nested Data Fetching11951196**Impact: CRITICAL (eliminates server-side waterfalls)**11971198When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.11991200**Incorrect: a single slow item blocks all nested fetches**12011202```tsx1203const chats = await Promise.all(1204 chatIds.map(id => getChat(id))1205)12061207const chatAuthors = await Promise.all(1208 chats.map(chat => getUser(chat.author))1209)1210```12111212If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.12131214**Correct: each item chains its own nested fetch**12151216```tsx1217const chatAuthors = await Promise.all(1218 chatIds.map(id => getChat(id).then(chat => getUser(chat.author)))1219)1220```12211222Each item independently chains `getChat` → `getUser`, so a slow chat doesn't block author fetches for the others.12231224### 3.9 Per-Request Deduplication with React.cache()12251226**Impact: MEDIUM (deduplicates within request)**12271228Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.12291230**Usage:**12311232```typescript1233import { cache } from 'react'12341235export const getCurrentUser = cache(async () => {1236 const session = await auth()1237 if (!session?.user?.id) return null1238 return await db.user.findUnique({1239 where: { id: session.user.id }1240 })1241})1242```12431244Within a single request, multiple calls to `getCurrentUser()` execute the query only once.12451246**Avoid inline objects as arguments:**12471248`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.12491250**Incorrect: always cache miss**12511252```typescript1253const getUser = cache(async (params: { uid: number }) => {1254 return await db.user.findUnique({ where: { id: params.uid } })1255})12561257// Each call creates new object, never hits cache1258getUser({ uid: 1 })1259getUser({ uid: 1 }) // Cache miss, runs query again1260```12611262**Correct: cache hit**12631264```typescript1265const params = { uid: 1 }1266getUser(params) // Query runs1267getUser(params) // Cache hit (same reference)1268```12691270If you must pass objects, pass the same reference:12711272**Next.js-Specific Note:**12731274In 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:12751276- Database queries (Prisma, Drizzle, etc.)12771278- Heavy computations12791280- Authentication checks12811282- File system operations12831284- Any non-fetch async work12851286Use `React.cache()` to deduplicate these operations across your component tree.12871288Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)12891290### 3.10 Use after() for Non-Blocking Operations12911292**Impact: MEDIUM (faster response times)**12931294Use 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.12951296**Incorrect: blocks response**12971298```tsx1299import { logUserAction } from '@/app/utils'13001301export async function POST(request: Request) {1302 // Perform mutation1303 await updateDatabase(request)13041305 // Logging blocks the response1306 const userAgent = request.headers.get('user-agent') || 'unknown'1307 await logUserAction({ userAgent })13081309 return new Response(JSON.stringify({ status: 'success' }), {1310 status: 200,1311 headers: { 'Content-Type': 'application/json' }1312 })1313}1314```13151316**Correct: non-blocking**13171318```tsx1319import { after } from 'next/server'1320import { headers, cookies } from 'next/headers'1321import { logUserAction } from '@/app/utils'13221323export async function POST(request: Request) {1324 // Perform mutation1325 await updateDatabase(request)13261327 // Log after response is sent1328 after(async () => {1329 const userAgent = (await headers()).get('user-agent') || 'unknown'1330 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'13311332 logUserAction({ sessionCookie, userAgent })1333 })13341335 return new Response(JSON.stringify({ status: 'success' }), {1336 status: 200,1337 headers: { 'Content-Type': 'application/json' }1338 })1339}1340```13411342The response is sent immediately while logging happens in the background.13431344**Common use cases:**13451346- Analytics tracking13471348- Audit logging13491350- Sending notifications13511352- Cache invalidation13531354- Cleanup tasks13551356**Important notes:**13571358- `after()` runs even if the response fails or redirects13591360- Works in Server Actions, Route Handlers, and Server Components13611362Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)13631364---13651366## 4. Client-Side Data Fetching13671368**Impact: MEDIUM-HIGH**13691370Automatic deduplication and efficient data fetching patterns reduce redundant network requests.13711372### 4.1 Deduplicate Global Event Listeners13731374**Impact: LOW (single listener for N components)**13751376Use `useSWRSubscription()` to share global event listeners across component instances.13771378**Incorrect: N instances = N listeners**13791380```tsx1381function useKeyboardShortcut(key: string, callback: () => void) {1382 useEffect(() => {1383 const handler = (e: KeyboardEvent) => {1384 if (e.metaKey && e.key === key) {1385 callback()1386 }1387 }1388 window.addEventListener('keydown', handler)1389 return () => window.removeEventListener('keydown', handler)1390 }, [key, callback])1391}1392```13931394When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.13951396**Correct: N instances = 1 listener**13971398```tsx1399import useSWRSubscription from 'swr/subscription'14001401// Module-level Map to track callbacks per key1402const keyCallbacks = new Map<string, Set<() => void>>()14031404function useKeyboardShortcut(key: string, callback: () => void) {1405 // Register this callback in the Map1406 useEffect(() => {1407 if (!keyCallbacks.has(key)) {1408 keyCallbacks.set(key, new Set())1409 }1410 keyCallbacks.get(key)!.add(callback)14111412 return () => {1413 const set = keyCallbacks.get(key)1414 if (set) {1415 set.delete(callback)1416 if (set.size === 0) {1417 keyCallbacks.delete(key)1418 }1419 }1420 }1421 }, [key, callback])14221423 useSWRSubscription('global-keydown', () => {1424 const handler = (e: KeyboardEvent) => {1425 if (e.metaKey && keyCallbacks.has(e.key)) {1426 keyCallbacks.get(e.key)!.forEach(cb => cb())1427 }1428 }1429 window.addEventListener('keydown', handler)1430 return () => window.removeEventListener('keydown', handler)1431 })1432}14331434function Profile() {1435 // Multiple shortcuts will share the same listener1436 useKeyboardShortcut('p', () => { /* ... */ })1437 useKeyboardShortcut('k', () => { /* ... */ })1438 // ...1439}1440```14411442### 4.2 Use Passive Event Listeners for Scrolling Performance14431444**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**14451446Add `{ 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.14471448**Incorrect:**14491450```typescript1451useEffect(() => {1452 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1453 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)14541455 document.addEventListener('touchstart', handleTouch)1456 document.addEventListener('wheel', handleWheel)14571458 return () => {1459 document.removeEventListener('touchstart', handleTouch)1460 document.removeEventListener('wheel', handleWheel)1461 }1462}, [])1463```14641465**Correct:**14661467```typescript1468useEffect(() => {1469 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1470 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)14711472 document.addEventListener('touchstart', handleTouch, { passive: true })1473 document.addEventListener('wheel', handleWheel, { passive: true })14741475 return () => {1476 document.removeEventListener('touchstart', handleTouch)1477 document.removeEventListener('wheel', handleWheel)1478 }1479}, [])1480```14811482**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.14831484**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.14851486### 4.3 Use SWR for Automatic Deduplication14871488**Impact: MEDIUM-HIGH (automatic deduplication)**14891490SWR enables request deduplication, caching, and revalidation across component instances.14911492**Incorrect: no deduplication, each instance fetches**14931494```tsx1495function UserList() {1496 const [users, setUsers] = useState([])1497 useEffect(() => {1498 fetch('/api/users')1499 .then(r => r.json())1500 .then(setUsers)1501 }, [])1502}1503```15041505**Correct: multiple instances share one request**15061507```tsx1508import useSWR from 'swr'15091510function UserList() {1511 const { data: users } = useSWR('/api/users', fetcher)1512}1513```15141515**For immutable data:**15161517```tsx1518import { useImmutableSWR } from '@/lib/swr'15191520function StaticContent() {1521 const { data } = useImmutableSWR('/api/config', fetcher)1522}1523```15241525**For mutations:**15261527```tsx1528import { useSWRMutation } from 'swr/mutation'15291530function UpdateButton() {1531 const { trigger } = useSWRMutation('/api/user', updateUser)1532 return <button onClick={() => trigger()}>Update</button>1533}1534```15351536Reference: [https://swr.vercel.app](https://swr.vercel.app)15371538### 4.4 Version and Minimize localStorage Data15391540**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**15411542Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.15431544**Incorrect:**15451546```typescript1547// No version, stores everything, no error handling1548localStorage.setItem('userConfig', JSON.stringify(fullUserObject))1549const data = localStorage.getItem('userConfig')1550```15511552**Correct:**15531554```typescript1555const VERSION = 'v2'15561557function saveConfig(config: { theme: string; language: string }) {1558 try {1559 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))1560 } catch {1561 // Throws in incognito/private browsing, quota exceeded, or disabled1562 }1563}15641565function loadConfig() {1566 try {1567 const data = localStorage.getItem(`userConfig:${VERSION}`)1568 return data ? JSON.parse(data) : null1569 } catch {1570 return null1571 }1572}15731574// Migration from v1 to v21575function migrate() {1576 try {1577 const v1 = localStorage.getItem('userConfig:v1')1578 if (v1) {1579 const old = JSON.parse(v1)1580 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })1581 localStorage.removeItem('userConfig:v1')1582 }1583 } catch {}1584}1585```15861587**Store minimal fields from server responses:**15881589```typescript1590// User object has 20+ fields, only store what UI needs1591function cachePrefs(user: FullUser) {1592 try {1593 localStorage.setItem('prefs:v1', JSON.stringify({1594 theme: user.preferences.theme,1595 notifications: user.preferences.notifications1596 }))1597 } catch {}1598}1599```16001601**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.16021603**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.16041605---16061607## 5. Re-render Optimization16081609**Impact: MEDIUM**16101611Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.16121613### 5.1 Calculate Derived State During Rendering16141615**Impact: MEDIUM (avoids redundant renders and state drift)**16161617If 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.16181619**Incorrect: redundant state and effect**16201621```tsx1622function Form() {1623 const [firstName, setFirstName] = useState('First')1624 const [lastName, setLastName] = useState('Last')1625 const [fullName, setFullName] = useState('')16261627 useEffect(() => {1628 setFullName(firstName + ' ' + lastName)1629 }, [firstName, lastName])16301631 return <p>{fullName}</p>1632}1633```16341635**Correct: derive during render**16361637```tsx1638function Form() {1639 const [firstName, setFirstName] = useState('First')1640 const [lastName, setLastName] = useState('Last')1641 const fullName = firstName + ' ' + lastName16421643 return <p>{fullName}</p>1644}1645```16461647Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)16481649### 5.2 Defer State Reads to Usage Point16501651**Impact: MEDIUM (avoids unnecessary subscriptions)**16521653Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.16541655**Incorrect: subscribes to all searchParams changes**16561657```tsx1658function ShareButton({ chatId }: { chatId: string }) {1659 const searchParams = useSearchParams()16601661 const handleShare = () => {1662 const ref = searchParams.get('ref')1663 shareChat(chatId, { ref })1664 }16651666 return <button onClick={handleShare}>Share</button>1667}1668```16691670**Correct: reads on demand, no subscription**16711672```tsx1673function ShareButton({ chatId }: { chatId: string }) {1674 const handleShare = () => {1675 const params = new URLSearchParams(window.location.search)1676 const ref = params.get('ref')1677 shareChat(chatId, { ref })1678 }16791680 return <button onClick={handleShare}>Share</button>1681}1682```16831684### 5.3 Do not wrap a simple expression with a primitive result type in useMemo16851686**Impact: LOW-MEDIUM (wasted computation on every render)**16871688When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.16891690Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.16911692**Incorrect:**16931694```tsx1695function Header({ user, notifications }: Props) {1696 const isLoading = useMemo(() => {1697 return user.isLoading || notifications.isLoading1698 }, [user.isLoading, notifications.isLoading])16991700 if (isLoading) return <Skeleton />1701 // return some markup1702}1703```17041705**Correct:**17061707```tsx1708function Header({ user, notifications }: Props) {1709 const isLoading = user.isLoading || notifications.isLoading17101711 if (isLoading) return <Skeleton />1712 // return some markup1713}1714```17151716### 5.4 Don't Define Components Inside Components17171718**Impact: HIGH (prevents remount on every render)**17191720Defining 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.17211722A common reason developers do this is to access parent variables without passing props. Always pass props instead.17231724**Incorrect: remounts on every render**17251726```tsx1727function UserProfile({ user, theme }) {1728 // Defined inside to access `theme` - BAD1729 const Avatar = () => (1730 <img1731 src={user.avatarUrl}1732 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}1733 />1734 )17351736 // Defined inside to access `user` - BAD1737 const Stats = () => (1738 <div>1739 <span>{user.followers} followers</span>1740 <span>{user.posts} posts</span>1741 </div>1742 )17431744 return (1745 <div>1746 <Avatar />1747 <Stats />1748 </div>1749 )1750}1751```17521753Every 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.17541755**Correct: pass props instead**17561757```tsx1758function Avatar({ src, theme }: { src: string; theme: string }) {1759 return (1760 <img1761 src={src}1762 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}1763 />1764 )1765}17661767function Stats({ followers, posts }: { followers: number; posts: number }) {1768 return (1769 <div>1770 <span>{followers} followers</span>1771 <span>{posts} posts</span>1772 </div>1773 )1774}17751776function UserProfile({ user, theme }) {1777 return (1778 <div>1779 <Avatar src={user.avatarUrl} theme={theme} />1780 <Stats followers={user.followers} posts={user.posts} />1781 </div>1782 )1783}1784```17851786**Symptoms of this bug:**17871788- Input fields lose focus on every keystroke17891790- Animations restart unexpectedly17911792- `useEffect` cleanup/setup runs on every parent render17931794- Scroll position resets inside the component17951796### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant17971798**Impact: MEDIUM (restores memoization by using a constant for default value)**17991800When 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()`.18011802To address this issue, extract the default value into a constant.18031804**Incorrect: `onClick` has different values on every rerender**18051806```tsx1807const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {1808 // ...1809})18101811// Used without optional onClick1812<UserAvatar />1813```18141815**Correct: stable default value**18161817```tsx1818const NOOP = () => {};18191820const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {1821 // ...1822})18231824// Used without optional onClick1825<UserAvatar />1826```18271828### 5.6 Extract to Memoized Components18291830**Impact: MEDIUM (enables early returns)**18311832Extract expensive work into memoized components to enable early returns before computation.18331834**Incorrect: computes avatar even when loading**18351836```tsx1837function Profile({ user, loading }: Props) {1838 const avatar = useMemo(() => {1839 const id = computeAvatarId(user)1840 return <Avatar id={id} />1841 }, [user])18421843 if (loading) return <Skeleton />1844 return <div>{avatar}</div>1845}1846```18471848**Correct: skips computation when loading**18491850```tsx1851const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1852 const id = useMemo(() => computeAvatarId(user), [user])1853 return <Avatar id={id} />1854})18551856function Profile({ user, loading }: Props) {1857 if (loading) return <Skeleton />1858 return (1859 <div>1860 <UserAvatar user={user} />1861 </div>1862 )1863}1864```18651866**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.18671868### 5.7 Narrow Effect Dependencies18691870**Impact: LOW (minimizes effect re-runs)**18711872Specify primitive dependencies instead of objects to minimize effect re-runs.18731874**Incorrect: re-runs on any user field change**18751876```tsx1877useEffect(() => {1878 console.log(user.id)1879}, [user])1880```18811882**Correct: re-runs only when id changes**18831884```tsx1885useEffect(() => {1886 console.log(user.id)1887}, [user.id])1888```18891890**For derived state, compute outside effect:**18911892```tsx1893// Incorrect: runs on width=767, 766, 765...1894useEffect(() => {1895 if (width < 768) {1896 enableMobileMode()1897 }1898}, [width])18991900// Correct: runs only on boolean transition1901const isMobile = width < 7681902useEffect(() => {1903 if (isMobile) {1904 enableMobileMode()1905 }1906}, [isMobile])1907```19081909### 5.8 Put Interaction Logic in Event Handlers19101911**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**19121913If 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.19141915**Incorrect: event modeled as state + effect**19161917```tsx1918function Form() {1919 const [submitted, setSubmitted] = useState(false)1920 const theme = useContext(ThemeContext)19211922 useEffect(() => {1923 if (submitted) {1924 post('/api/register')1925 showToast('Registered', theme)1926 }1927 }, [submitted, theme])19281929 return <button onClick={() => setSubmitted(true)}>Submit</button>1930}1931```19321933**Correct: do it in the handler**19341935```tsx1936function Form() {1937 const theme = useContext(ThemeContext)19381939 function handleSubmit() {1940 post('/api/register')1941 showToast('Registered', theme)1942 }19431944 return <button onClick={handleSubmit}>Submit</button>1945}1946```19471948Reference: [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)19491950### 5.9 Split Combined Hook Computations19511952**Impact: MEDIUM (avoids recomputing independent steps)**19531954When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.19551956**Incorrect: changing `sortOrder` recomputes filtering**19571958```tsx1959const sortedProducts = useMemo(() => {1960 const filtered = products.filter((p) => p.category === category)1961 const sorted = filtered.toSorted((a, b) =>1962 sortOrder === "asc" ? a.price - b.price : b.price - a.price1963 )1964 return sorted1965}, [products, category, sortOrder])1966```19671968**Correct: filtering only recomputes when products or category change**19691970```tsx1971const filteredProducts = useMemo(1972 () => products.filter((p) => p.category === category),1973 [products, category]1974)19751976const sortedProducts = useMemo(1977 () =>1978 filteredProducts.toSorted((a, b) =>1979 sortOrder === "asc" ? a.price - b.price : b.price - a.price1980 ),1981 [filteredProducts, sortOrder]1982)1983```19841985This pattern also applies to `useEffect` when combining unrelated side effects:19861987**Incorrect: both effects run when either dependency changes**19881989```tsx1990useEffect(() => {1991 analytics.trackPageView(pathname)1992 document.title = `${pageTitle} | My App`1993}, [pathname, pageTitle])1994```19951996**Correct: effects run independently**19971998```tsx1999useEffect(() => {2000 analytics.trackPageView(pathname)2001}, [pathname])20022003useEffect(() => {2004 document.title = `${pageTitle} | My App`2005}, [pageTitle])2006```20072008**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.20092010### 5.10 Subscribe to Derived State20112012**Impact: MEDIUM (reduces re-render frequency)**20132014Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.20152016**Incorrect: re-renders on every pixel change**20172018```tsx2019function Sidebar() {2020 const width = useWindowWidth() // updates continuously2021 const isMobile = width < 7682022 return <nav className={isMobile ? 'mobile' : 'desktop'} />2023}2024```20252026**Correct: re-renders only when boolean changes**20272028```tsx2029function Sidebar() {2030 const isMobile = useMediaQuery('(max-width: 767px)')2031 return <nav className={isMobile ? 'mobile' : 'desktop'} />2032}2033```20342035### 5.11 Use Functional setState Updates20362037**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**20382039When 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.20402041**Incorrect: requires state as dependency**20422043```tsx2044function TodoList() {2045 const [items, setItems] = useState(initialItems)20462047 // Callback must depend on items, recreated on every items change2048 const addItems = useCallback((newItems: Item[]) => {2049 setItems([...items, ...newItems])2050 }, [items]) // ❌ items dependency causes recreations20512052 // Risk of stale closure if dependency is forgotten2053 const removeItem = useCallback((id: string) => {2054 setItems(items.filter(item => item.id !== id))2055 }, []) // ❌ Missing items dependency - will use stale items!20562057 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />2058}2059```20602061The 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.20622063**Correct: stable callbacks, no stale closures**20642065```tsx2066function TodoList() {2067 const [items, setItems] = useState(initialItems)20682069 // Stable callback, never recreated2070 const addItems = useCallback((newItems: Item[]) => {2071 setItems(curr => [...curr, ...newItems])2072 }, []) // ✅ No dependencies needed20732074 // Always uses latest state, no stale closure risk2075 const removeItem = useCallback((id: string) => {2076 setItems(curr => curr.filter(item => item.id !== id))2077 }, []) // ✅ Safe and stable20782079 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />2080}2081```20822083**Benefits:**208420851. **Stable callback references** - Callbacks don't need to be recreated when state changes208620872. **No stale closures** - Always operates on the latest state value208820893. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks209020914. **Prevents bugs** - Eliminates the most common source of React closure bugs20922093**When to use functional updates:**20942095- Any setState that depends on the current state value20962097- Inside useCallback/useMemo when state is needed20982099- Event handlers that reference state21002101- Async operations that update state21022103**When direct updates are fine:**21042105- Setting state to a static value: `setCount(0)`21062107- Setting state from props/arguments only: `setName(newName)`21082109- State doesn't depend on previous value21102111**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.21122113### 5.12 Use Lazy State Initialization21142115**Impact: MEDIUM (wasted computation on every render)**21162117Pass 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.21182119**Incorrect: runs on every render**21202121```tsx2122function FilteredList({ items }: { items: Item[] }) {2123 // buildSearchIndex() runs on EVERY render, even after initialization2124 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))2125 const [query, setQuery] = useState('')21262127 // When query changes, buildSearchIndex runs again unnecessarily2128 return <SearchResults index={searchIndex} query={query} />2129}21302131function UserProfile() {2132 // JSON.parse runs on every render2133 const [settings, setSettings] = useState(2134 JSON.parse(localStorage.getItem('settings') || '{}')2135 )21362137 return <SettingsForm settings={settings} onChange={setSettings} />2138}2139```21402141**Correct: runs only once**21422143```tsx2144function FilteredList({ items }: { items: Item[] }) {2145 // buildSearchIndex() runs ONLY on initial render2146 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))2147 const [query, setQuery] = useState('')21482149 return <SearchResults index={searchIndex} query={query} />2150}21512152function UserProfile() {2153 // JSON.parse runs only on initial render2154 const [settings, setSettings] = useState(() => {2155 const stored = localStorage.getItem('settings')2156 return stored ? JSON.parse(stored) : {}2157 })21582159 return <SettingsForm settings={settings} onChange={setSettings} />2160}2161```21622163Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.21642165For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.21662167### 5.13 Use Transitions for Non-Urgent Updates21682169**Impact: MEDIUM (maintains UI responsiveness)**21702171Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.21722173**Incorrect: blocks UI on every scroll**21742175```tsx2176function ScrollTracker() {2177 const [scrollY, setScrollY] = useState(0)2178 useEffect(() => {2179 const handler = () => setScrollY(window.scrollY)2180 window.addEventListener('scroll', handler, { passive: true })2181 return () => window.removeEventListener('scroll', handler)2182 }, [])2183}2184```21852186**Correct: non-blocking updates**21872188```tsx2189import { startTransition } from 'react'21902191function ScrollTracker() {2192 const [scrollY, setScrollY] = useState(0)2193 useEffect(() => {2194 const handler = () => {2195 startTransition(() => setScrollY(window.scrollY))2196 }2197 window.addEventListener('scroll', handler, { passive: true })2198 return () => window.removeEventListener('scroll', handler)2199 }, [])2200}2201```22022203### 5.14 Use useDeferredValue for Expensive Derived Renders22042205**Impact: MEDIUM (keeps input responsive during heavy computation)**22062207When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.22082209**Incorrect: input feels laggy while filtering**22102211```tsx2212function Search({ items }: { items: Item[] }) {2213 const [query, setQuery] = useState('')2214 const filtered = items.filter(item => fuzzyMatch(item, query))22152216 return (2217 <>2218 <input value={query} onChange={e => setQuery(e.target.value)} />2219 <ResultsList results={filtered} />2220 </>2221 )2222}2223```22242225**Correct: input stays snappy, results render when ready**22262227```tsx2228function Search({ items }: { items: Item[] }) {2229 const [query, setQuery] = useState('')2230 const deferredQuery = useDeferredValue(query)2231 const filtered = useMemo(2232 () => items.filter(item => fuzzyMatch(item, deferredQuery)),2233 [items, deferredQuery]2234 )2235 const isStale = query !== deferredQuery22362237 return (2238 <>2239 <input value={query} onChange={e => setQuery(e.target.value)} />2240 <div style={{ opacity: isStale ? 0.7 : 1 }}>2241 <ResultsList results={filtered} />2242 </div>2243 </>2244 )2245}2246```22472248**When to use:**22492250- Filtering/searching large lists22512252- Expensive visualizations (charts, graphs) reacting to input22532254- Any derived state that causes noticeable render delays22552256**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.22572258Reference: [https://react.dev/reference/react/useDeferredValue](https://react.dev/reference/react/useDeferredValue)22592260### 5.15 Use useRef for Transient Values22612262**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**22632264When 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.22652266**Incorrect: renders every update**22672268```tsx2269function Tracker() {2270 const [lastX, setLastX] = useState(0)22712272 useEffect(() => {2273 const onMove = (e: MouseEvent) => setLastX(e.clientX)2274 window.addEventListener('mousemove', onMove)2275 return () => window.removeEventListener('mousemove', onMove)2276 }, [])22772278 return (2279 <div2280 style={{2281 position: 'fixed',2282 top: 0,2283 left: lastX,2284 width: 8,2285 height: 8,2286 background: 'black',2287 }}2288 />2289 )2290}2291```22922293**Correct: no re-render for tracking**22942295```tsx2296function Tracker() {2297 const lastXRef = useRef(0)2298 const dotRef = useRef<HTMLDivElement>(null)22992300 useEffect(() => {2301 const onMove = (e: MouseEvent) => {2302 lastXRef.current = e.clientX2303 const node = dotRef.current2304 if (node) {2305 node.style.transform = `translateX(${e.clientX}px)`2306 }2307 }2308 window.addEventListener('mousemove', onMove)2309 return () => window.removeEventListener('mousemove', onMove)2310 }, [])23112312 return (2313 <div2314 ref={dotRef}2315 style={{2316 position: 'fixed',2317 top: 0,2318 left: 0,2319 width: 8,2320 height: 8,2321 background: 'black',2322 transform: 'translateX(0px)',2323 }}2324 />2325 )2326}2327```23282329---23302331## 6. Rendering Performance23322333**Impact: MEDIUM**23342335Optimizing the rendering process reduces the work the browser needs to do.23362337### 6.1 Animate SVG Wrapper Instead of SVG Element23382339**Impact: LOW (enables hardware acceleration)**23402341Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.23422343**Incorrect: animating SVG directly - no hardware acceleration**23442345```tsx2346function LoadingSpinner() {2347 return (2348 <svg2349 className="animate-spin"2350 width="24"2351 height="24"2352 viewBox="0 0 24 24"2353 >2354 <circle cx="12" cy="12" r="10" stroke="currentColor" />2355 </svg>2356 )2357}2358```23592360**Correct: animating wrapper div - hardware accelerated**23612362```tsx2363function LoadingSpinner() {2364 return (2365 <div className="animate-spin">2366 <svg2367 width="24"2368 height="24"2369 viewBox="0 0 24 24"2370 >2371 <circle cx="12" cy="12" r="10" stroke="currentColor" />2372 </svg>2373 </div>2374 )2375}2376```23772378This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.23792380### 6.2 CSS content-visibility for Long Lists23812382**Impact: HIGH (faster initial render)**23832384Apply `content-visibility: auto` to defer off-screen rendering.23852386**CSS:**23872388```css2389.message-item {2390 content-visibility: auto;2391 contain-intrinsic-size: 0 80px;2392}2393```23942395**Example:**23962397```tsx2398function MessageList({ messages }: { messages: Message[] }) {2399 return (2400 <div className="overflow-y-auto h-screen">2401 {messages.map(msg => (2402 <div key={msg.id} className="message-item">2403 <Avatar user={msg.author} />2404 <div>{msg.content}</div>2405 </div>2406 ))}2407 </div>2408 )2409}2410```24112412For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).24132414### 6.3 Hoist Static JSX Elements24152416**Impact: LOW (avoids re-creation)**24172418Extract static JSX outside components to avoid re-creation.24192420**Incorrect: recreates element every render**24212422```tsx2423function LoadingSkeleton() {2424 return <div className="animate-pulse h-20 bg-gray-200" />2425}24262427function Container() {2428 return (2429 <div>2430 {loading && <LoadingSkeleton />}2431 </div>2432 )2433}2434```24352436**Correct: reuses same element**24372438```tsx2439const loadingSkeleton = (2440 <div className="animate-pulse h-20 bg-gray-200" />2441)24422443function Container() {2444 return (2445 <div>2446 {loading && loadingSkeleton}2447 </div>2448 )2449}2450```24512452This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.24532454**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.24552456### 6.4 Optimize SVG Precision24572458**Impact: LOW (reduces file size)**24592460Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.24612462**Incorrect: excessive precision**24632464```svg2465<path d="M 10.293847 20.847362 L 30.938472 40.192837" />2466```24672468**Correct: 1 decimal place**24692470```svg2471<path d="M 10.3 20.8 L 30.9 40.2" />2472```24732474**Automate with SVGO:**24752476```bash2477npx svgo --precision=1 --multipass icon.svg2478```24792480### 6.5 Prevent Hydration Mismatch Without Flickering24812482**Impact: MEDIUM (avoids visual flicker and hydration errors)**24832484When 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.24852486**Incorrect: breaks SSR**24872488```tsx2489function ThemeWrapper({ children }: { children: ReactNode }) {2490 // localStorage is not available on server - throws error2491 const theme = localStorage.getItem('theme') || 'light'24922493 return (2494 <div className={theme}>2495 {children}2496 </div>2497 )2498}2499```25002501Server-side rendering will fail because `localStorage` is undefined.25022503**Incorrect: visual flickering**25042505```tsx2506function ThemeWrapper({ children }: { children: ReactNode }) {2507 const [theme, setTheme] = useState('light')25082509 useEffect(() => {2510 // Runs after hydration - causes visible flash2511 const stored = localStorage.getItem('theme')2512 if (stored) {2513 setTheme(stored)2514 }2515 }, [])25162517 return (2518 <div className={theme}>2519 {children}2520 </div>2521 )2522}2523```25242525Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.25262527**Correct: no flicker, no hydration mismatch**25282529```tsx2530function ThemeWrapper({ children }: { children: ReactNode }) {2531 return (2532 <>2533 <div id="theme-wrapper">2534 {children}2535 </div>2536 <script2537 dangerouslySetInnerHTML={{2538 __html: `2539 (function() {2540 try {2541 var theme = localStorage.getItem('theme') || 'light';2542 var el = document.getElementById('theme-wrapper');2543 if (el) el.className = theme;2544 } catch (e) {}2545 })();2546 `,2547 }}2548 />2549 </>2550 )2551}2552```25532554The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.25552556This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.25572558### 6.6 Suppress Expected Hydration Mismatches25592560**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**25612562In 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.25632564**Incorrect: known mismatch warnings**25652566```tsx2567function Timestamp() {2568 return <span>{new Date().toLocaleString()}</span>2569}2570```25712572**Correct: suppress expected mismatch only**25732574```tsx2575function Timestamp() {2576 return (2577 <span suppressHydrationWarning>2578 {new Date().toLocaleString()}2579 </span>2580 )2581}2582```25832584### 6.7 Use Activity Component for Show/Hide25852586**Impact: MEDIUM (preserves state/DOM)**25872588Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.25892590**Usage:**25912592```tsx2593import { Activity } from 'react'25942595function Dropdown({ isOpen }: Props) {2596 return (2597 <Activity mode={isOpen ? 'visible' : 'hidden'}>2598 <ExpensiveMenu />2599 </Activity>2600 )2601}2602```26032604Avoids expensive re-renders and state loss.26052606### 6.8 Use defer or async on Script Tags26072608**Impact: HIGH (eliminates render-blocking)**26092610Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.26112612- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order26132614- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order26152616Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.26172618**Incorrect: blocks rendering**26192620```tsx2621export default function Document() {2622 return (2623 <html>2624 <head>2625 <script src="https://example.com/analytics.js" />2626 <script src="/scripts/utils.js" />2627 </head>2628 <body>{/* content */}</body>2629 </html>2630 )2631}2632```26332634**Correct: non-blocking**26352636```tsx2637import Script from 'next/script'26382639export default function Page() {2640 return (2641 <>2642 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />2643 <Script src="/scripts/utils.js" strategy="beforeInteractive" />2644 </>2645 )2646}2647```26482649**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:26502651Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)26522653### 6.9 Use Explicit Conditional Rendering26542655**Impact: LOW (prevents rendering 0 or NaN)**26562657Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.26582659**Incorrect: renders "0" when count is 0**26602661```tsx2662function Badge({ count }: { count: number }) {2663 return (2664 <div>2665 {count && <span className="badge">{count}</span>}2666 </div>2667 )2668}26692670// When count = 0, renders: <div>0</div>2671// When count = 5, renders: <div><span class="badge">5</span></div>2672```26732674**Correct: renders nothing when count is 0**26752676```tsx2677function Badge({ count }: { count: number }) {2678 return (2679 <div>2680 {count > 0 ? <span className="badge">{count}</span> : null}2681 </div>2682 )2683}26842685// When count = 0, renders: <div></div>2686// When count = 5, renders: <div><span class="badge">5</span></div>2687```26882689### 6.10 Use React DOM Resource Hints26902691**Impact: HIGH (reduces load time for critical resources)**26922693React 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.26942695- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to26962697- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server26982699- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon27002701- **`preloadModule(href)`**: Fetch an ES module you'll use soon27022703- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script27042705- **`preinitModule(href)`**: Fetch and evaluate an ES module27062707**Example: preconnect to third-party APIs**27082709```tsx2710import { preconnect, prefetchDNS } from 'react-dom'27112712export default function App() {2713 prefetchDNS('https://analytics.example.com')2714 preconnect('https://api.example.com')27152716 return <main>{/* content */}</main>2717}2718```27192720**Example: preload critical fonts and styles**27212722```tsx2723import { preload, preinit } from 'react-dom'27242725export default function RootLayout({ children }) {2726 // Preload font file2727 preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })27282729 // Fetch and apply critical stylesheet immediately2730 preinit('/styles/critical.css', { as: 'style' })27312732 return (2733 <html>2734 <body>{children}</body>2735 </html>2736 )2737}2738```27392740**Example: preload modules for code-split routes**27412742```tsx2743import { preloadModule, preinitModule } from 'react-dom'27442745function Navigation() {2746 const preloadDashboard = () => {2747 preloadModule('/dashboard.js', { as: 'script' })2748 }27492750 return (2751 <nav>2752 <a href="/dashboard" onMouseEnter={preloadDashboard}>2753 Dashboard2754 </a>2755 </nav>2756 )2757}2758```27592760**When to use each:**27612762| API | Use case |27632764|-----|----------|27652766| `prefetchDNS` | Third-party domains you'll connect to later |27672768| `preconnect` | APIs or CDNs you'll fetch from immediately |27692770| `preload` | Critical resources needed for current page |27712772| `preloadModule` | JS modules for likely next navigation |27732774| `preinit` | Stylesheets/scripts that must execute early |27752776| `preinitModule` | ES modules that must execute early |27772778Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)27792780### 6.11 Use useTransition Over Manual Loading States27812782**Impact: LOW (reduces re-renders and improves code clarity)**27832784Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.27852786**Incorrect: manual loading state**27872788```tsx2789function SearchResults() {2790 const [query, setQuery] = useState('')2791 const [results, setResults] = useState([])2792 const [isLoading, setIsLoading] = useState(false)27932794 const handleSearch = async (value: string) => {2795 setIsLoading(true)2796 setQuery(value)2797 const data = await fetchResults(value)2798 setResults(data)2799 setIsLoading(false)2800 }28012802 return (2803 <>2804 <input onChange={(e) => handleSearch(e.target.value)} />2805 {isLoading && <Spinner />}2806 <ResultsList results={results} />2807 </>2808 )2809}2810```28112812**Correct: useTransition with built-in pending state**28132814```tsx2815import { useTransition, useState } from 'react'28162817function SearchResults() {2818 const [query, setQuery] = useState('')2819 const [results, setResults] = useState([])2820 const [isPending, startTransition] = useTransition()28212822 const handleSearch = (value: string) => {2823 setQuery(value) // Update input immediately28242825 startTransition(async () => {2826 // Fetch and update results2827 const data = await fetchResults(value)2828 setResults(data)2829 })2830 }28312832 return (2833 <>2834 <input onChange={(e) => handleSearch(e.target.value)} />2835 {isPending && <Spinner />}2836 <ResultsList results={results} />2837 </>2838 )2839}2840```28412842**Benefits:**28432844- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`28452846- **Error resilience**: Pending state correctly resets even if the transition throws28472848- **Better responsiveness**: Keeps the UI responsive during updates28492850- **Interrupt handling**: New transitions automatically cancel pending ones28512852Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)28532854---28552856## 7. JavaScript Performance28572858**Impact: LOW-MEDIUM**28592860Micro-optimizations for hot paths can add up to meaningful improvements.28612862### 7.1 Avoid Layout Thrashing28632864**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**28652866Avoid 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.28672868**This is OK: browser batches style changes**28692870```typescript2871function updateElementStyles(element: HTMLElement) {2872 // Each line invalidates style, but browser batches the recalculation2873 element.style.width = '100px'2874 element.style.height = '200px'2875 element.style.backgroundColor = 'blue'2876 element.style.border = '1px solid black'2877}2878```28792880**Incorrect: interleaved reads and writes force reflows**28812882```typescript2883function layoutThrashing(element: HTMLElement) {2884 element.style.width = '100px'2885 const width = element.offsetWidth // Forces reflow2886 element.style.height = '200px'2887 const height = element.offsetHeight // Forces another reflow2888}2889```28902891**Correct: batch writes, then read once**28922893```typescript2894function updateElementStyles(element: HTMLElement) {2895 // Batch all writes together2896 element.style.width = '100px'2897 element.style.height = '200px'2898 element.style.backgroundColor = 'blue'2899 element.style.border = '1px solid black'29002901 // Read after all writes are done (single reflow)2902 const { width, height } = element.getBoundingClientRect()2903}2904```29052906**Correct: batch reads, then writes**29072908```typescript2909function updateElementStyles(element: HTMLElement) {2910 element.classList.add('highlighted-box')29112912 const { width, height } = element.getBoundingClientRect()2913}2914```29152916**Better: use CSS classes**29172918**React example:**29192920```tsx2921// Incorrect: interleaving style changes with layout queries2922function Box({ isHighlighted }: { isHighlighted: boolean }) {2923 const ref = useRef<HTMLDivElement>(null)29242925 useEffect(() => {2926 if (ref.current && isHighlighted) {2927 ref.current.style.width = '100px'2928 const width = ref.current.offsetWidth // Forces layout2929 ref.current.style.height = '200px'2930 }2931 }, [isHighlighted])29322933 return <div ref={ref}>Content</div>2934}29352936// Correct: toggle class2937function Box({ isHighlighted }: { isHighlighted: boolean }) {2938 return (2939 <div className={isHighlighted ? 'highlighted-box' : ''}>2940 Content2941 </div>2942 )2943}2944```29452946Prefer 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.29472948See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.29492950### 7.2 Build Index Maps for Repeated Lookups29512952**Impact: LOW-MEDIUM (1M ops to 2K ops)**29532954Multiple `.find()` calls by the same key should use a Map.29552956**Incorrect (O(n) per lookup):**29572958```typescript2959function processOrders(orders: Order[], users: User[]) {2960 return orders.map(order => ({2961 ...order,2962 user: users.find(u => u.id === order.userId)2963 }))2964}2965```29662967**Correct (O(1) per lookup):**29682969```typescript2970function processOrders(orders: Order[], users: User[]) {2971 const userById = new Map(users.map(u => [u.id, u]))29722973 return orders.map(order => ({2974 ...order,2975 user: userById.get(order.userId)2976 }))2977}2978```29792980Build map once (O(n)), then all lookups are O(1).29812982For 1000 orders × 1000 users: 1M ops → 2K ops.29832984### 7.3 Cache Property Access in Loops29852986**Impact: LOW-MEDIUM (reduces lookups)**29872988Cache object property lookups in hot paths.29892990**Incorrect: 3 lookups × N iterations**29912992```typescript2993for (let i = 0; i < arr.length; i++) {2994 process(obj.config.settings.value)2995}2996```29972998**Correct: 1 lookup total**29993000```typescript3001const value = obj.config.settings.value3002const len = arr.length3003for (let i = 0; i < len; i++) {3004 process(value)3005}3006```30073008### 7.4 Cache Repeated Function Calls30093010**Impact: MEDIUM (avoid redundant computation)**30113012Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.30133014**Incorrect: redundant computation**30153016```typescript3017function ProjectList({ projects }: { projects: Project[] }) {3018 return (3019 <div>3020 {projects.map(project => {3021 // slugify() called 100+ times for same project names3022 const slug = slugify(project.name)30233024 return <ProjectCard key={project.id} slug={slug} />3025 })}3026 </div>3027 )3028}3029```30303031**Correct: cached results**30323033```typescript3034// Module-level cache3035const slugifyCache = new Map<string, string>()30363037function cachedSlugify(text: string): string {3038 if (slugifyCache.has(text)) {3039 return slugifyCache.get(text)!3040 }3041 const result = slugify(text)3042 slugifyCache.set(text, result)3043 return result3044}30453046function ProjectList({ projects }: { projects: Project[] }) {3047 return (3048 <div>3049 {projects.map(project => {3050 // Computed only once per unique project name3051 const slug = cachedSlugify(project.name)30523053 return <ProjectCard key={project.id} slug={slug} />3054 })}3055 </div>3056 )3057}3058```30593060**Simpler pattern for single-value functions:**30613062```typescript3063let isLoggedInCache: boolean | null = null30643065function isLoggedIn(): boolean {3066 if (isLoggedInCache !== null) {3067 return isLoggedInCache3068 }30693070 isLoggedInCache = document.cookie.includes('auth=')3071 return isLoggedInCache3072}30733074// Clear cache when auth changes3075function onAuthChange() {3076 isLoggedInCache = null3077}3078```30793080Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.30813082Reference: [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)30833084### 7.5 Cache Storage API Calls30853086**Impact: LOW-MEDIUM (reduces expensive I/O)**30873088`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.30893090**Incorrect: reads storage on every call**30913092```typescript3093function getTheme() {3094 return localStorage.getItem('theme') ?? 'light'3095}3096// Called 10 times = 10 storage reads3097```30983099**Correct: Map cache**31003101```typescript3102const storageCache = new Map<string, string | null>()31033104function getLocalStorage(key: string) {3105 if (!storageCache.has(key)) {3106 storageCache.set(key, localStorage.getItem(key))3107 }3108 return storageCache.get(key)3109}31103111function setLocalStorage(key: string, value: string) {3112 localStorage.setItem(key, value)3113 storageCache.set(key, value) // keep cache in sync3114}3115```31163117Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.31183119**Cookie caching:**31203121```typescript3122let cookieCache: Record<string, string> | null = null31233124function getCookie(name: string) {3125 if (!cookieCache) {3126 cookieCache = Object.fromEntries(3127 document.cookie.split('; ').map(c => c.split('='))3128 )3129 }3130 return cookieCache[name]3131}3132```31333134**Important: invalidate on external changes**31353136```typescript3137window.addEventListener('storage', (e) => {3138 if (e.key) storageCache.delete(e.key)3139})31403141document.addEventListener('visibilitychange', () => {3142 if (document.visibilityState === 'visible') {3143 storageCache.clear()3144 }3145})3146```31473148If storage can change externally (another tab, server-set cookies), invalidate cache:31493150### 7.6 Combine Multiple Array Iterations31513152**Impact: LOW-MEDIUM (reduces iterations)**31533154Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.31553156**Incorrect: 3 iterations**31573158```typescript3159const admins = users.filter(u => u.isAdmin)3160const testers = users.filter(u => u.isTester)3161const inactive = users.filter(u => !u.isActive)3162```31633164**Correct: 1 iteration**31653166```typescript3167const admins: User[] = []3168const testers: User[] = []3169const inactive: User[] = []31703171for (const user of users) {3172 if (user.isAdmin) admins.push(user)3173 if (user.isTester) testers.push(user)3174 if (!user.isActive) inactive.push(user)3175}3176```31773178### 7.7 Defer Non-Critical Work with requestIdleCallback31793180**Impact: MEDIUM (keeps UI responsive during background tasks)**31813182Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.31833184**Incorrect: blocks main thread during user interaction**31853186```typescript3187function handleSearch(query: string) {3188 const results = searchItems(query)3189 setResults(results)31903191 // These block the main thread immediately3192 analytics.track('search', { query })3193 saveToRecentSearches(query)3194 prefetchTopResults(results.slice(0, 3))3195}3196```31973198**Correct: defers non-critical work to idle time**31993200```typescript3201function handleSearch(query: string) {3202 const results = searchItems(query)3203 setResults(results)32043205 // Defer non-critical work to idle periods3206 requestIdleCallback(() => {3207 analytics.track('search', { query })3208 })32093210 requestIdleCallback(() => {3211 saveToRecentSearches(query)3212 })32133214 requestIdleCallback(() => {3215 prefetchTopResults(results.slice(0, 3))3216 })3217}3218```32193220**With timeout for required work:**32213222```typescript3223// Ensure analytics fires within 2 seconds even if browser stays busy3224requestIdleCallback(3225 () => analytics.track('page_view', { path: location.pathname }),3226 { timeout: 2000 }3227)3228```32293230**Chunking large tasks:**32313232```typescript3233function processLargeDataset(items: Item[]) {3234 let index = 032353236 function processChunk(deadline: IdleDeadline) {3237 // Process items while we have idle time (aim for <50ms chunks)3238 while (index < items.length && deadline.timeRemaining() > 0) {3239 processItem(items[index])3240 index++3241 }32423243 // Schedule next chunk if more items remain3244 if (index < items.length) {3245 requestIdleCallback(processChunk)3246 }3247 }32483249 requestIdleCallback(processChunk)3250}3251```32523253**With fallback for unsupported browsers:**32543255```typescript3256const scheduleIdleWork = window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1))32573258scheduleIdleWork(() => {3259 // Non-critical work3260})3261```32623263**When to use:**32643265- Analytics and telemetry32663267- Saving state to localStorage/IndexedDB32683269- Prefetching resources for likely next actions32703271- Processing non-urgent data transformations32723273- Lazy initialization of non-critical features32743275**When NOT to use:**32763277- User-initiated actions that need immediate feedback32783279- Rendering updates the user is waiting for32803281- Time-sensitive operations32823283### 7.8 Early Length Check for Array Comparisons32843285**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**32863287When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.32883289In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).32903291**Incorrect: always runs expensive comparison**32923293```typescript3294function hasChanges(current: string[], original: string[]) {3295 // Always sorts and joins, even when lengths differ3296 return current.sort().join() !== original.sort().join()3297}3298```32993300Two 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.33013302**Correct (O(1) length check first):**33033304```typescript3305function hasChanges(current: string[], original: string[]) {3306 // Early return if lengths differ3307 if (current.length !== original.length) {3308 return true3309 }3310 // Only sort when lengths match3311 const currentSorted = current.toSorted()3312 const originalSorted = original.toSorted()3313 for (let i = 0; i < currentSorted.length; i++) {3314 if (currentSorted[i] !== originalSorted[i]) {3315 return true3316 }3317 }3318 return false3319}3320```33213322This new approach is more efficient because:33233324- It avoids the overhead of sorting and joining the arrays when lengths differ33253326- It avoids consuming memory for the joined strings (especially important for large arrays)33273328- It avoids mutating the original arrays33293330- It returns early when a difference is found33313332### 7.9 Early Return from Functions33333334**Impact: LOW-MEDIUM (avoids unnecessary computation)**33353336Return early when result is determined to skip unnecessary processing.33373338**Incorrect: processes all items even after finding answer**33393340```typescript3341function validateUsers(users: User[]) {3342 let hasError = false3343 let errorMessage = ''33443345 for (const user of users) {3346 if (!user.email) {3347 hasError = true3348 errorMessage = 'Email required'3349 }3350 if (!user.name) {3351 hasError = true3352 errorMessage = 'Name required'3353 }3354 // Continues checking all users even after error found3355 }33563357 return hasError ? { valid: false, error: errorMessage } : { valid: true }3358}3359```33603361**Correct: returns immediately on first error**33623363```typescript3364function validateUsers(users: User[]) {3365 for (const user of users) {3366 if (!user.email) {3367 return { valid: false, error: 'Email required' }3368 }3369 if (!user.name) {3370 return { valid: false, error: 'Name required' }3371 }3372 }33733374 return { valid: true }3375}3376```33773378### 7.10 Hoist RegExp Creation33793380**Impact: LOW-MEDIUM (avoids recreation)**33813382Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.33833384**Incorrect: new RegExp every render**33853386```tsx3387function Highlighter({ text, query }: Props) {3388 const regex = new RegExp(`(${query})`, 'gi')3389 const parts = text.split(regex)3390 return <>{parts.map((part, i) => ...)}</>3391}3392```33933394**Correct: memoize or hoist**33953396```tsx3397const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/33983399function Highlighter({ text, query }: Props) {3400 const regex = useMemo(3401 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),3402 [query]3403 )3404 const parts = text.split(regex)3405 return <>{parts.map((part, i) => ...)}</>3406}3407```34083409**Warning: global regex has mutable state**34103411```typescript3412const regex = /foo/g3413regex.test('foo') // true, lastIndex = 33414regex.test('foo') // false, lastIndex = 03415```34163417Global regex (`/g`) has mutable `lastIndex` state:34183419### 7.11 Use flatMap to Map and Filter in One Pass34203421**Impact: LOW-MEDIUM (eliminates intermediate array)**34223423Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.34243425**Incorrect: 2 iterations, intermediate array**34263427```typescript3428const userNames = users3429 .map(user => user.isActive ? user.name : null)3430 .filter(Boolean)3431```34323433**Correct: 1 iteration, no intermediate array**34343435```typescript3436const userNames = users.flatMap(user =>3437 user.isActive ? [user.name] : []3438)3439```34403441**More examples:**34423443```typescript3444// Extract valid emails from responses3445// Before3446const emails = responses3447 .map(r => r.success ? r.data.email : null)3448 .filter(Boolean)34493450// After3451const emails = responses.flatMap(r =>3452 r.success ? [r.data.email] : []3453)34543455// Parse and filter valid numbers3456// Before3457const numbers = strings3458 .map(s => parseInt(s, 10))3459 .filter(n => !isNaN(n))34603461// After3462const numbers = strings.flatMap(s => {3463 const n = parseInt(s, 10)3464 return isNaN(n) ? [] : [n]3465})3466```34673468**When to use:**34693470- Transforming items while filtering some out34713472- Conditional mapping where some inputs produce no output34733474- Parsing/validating where invalid inputs should be skipped34753476### 7.12 Use Loop for Min/Max Instead of Sort34773478**Impact: LOW (O(n) instead of O(n log n))**34793480Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.34813482**Incorrect (O(n log n) - sort to find latest):**34833484```typescript3485interface Project {3486 id: string3487 name: string3488 updatedAt: number3489}34903491function getLatestProject(projects: Project[]) {3492 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)3493 return sorted[0]3494}3495```34963497Sorts the entire array just to find the maximum value.34983499**Incorrect (O(n log n) - sort for oldest and newest):**35003501```typescript3502function getOldestAndNewest(projects: Project[]) {3503 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)3504 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }3505}3506```35073508Still sorts unnecessarily when only min/max are needed.35093510**Correct (O(n) - single loop):**35113512```typescript3513function getLatestProject(projects: Project[]) {3514 if (projects.length === 0) return null35153516 let latest = projects[0]35173518 for (let i = 1; i < projects.length; i++) {3519 if (projects[i].updatedAt > latest.updatedAt) {3520 latest = projects[i]3521 }3522 }35233524 return latest3525}35263527function getOldestAndNewest(projects: Project[]) {3528 if (projects.length === 0) return { oldest: null, newest: null }35293530 let oldest = projects[0]3531 let newest = projects[0]35323533 for (let i = 1; i < projects.length; i++) {3534 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]3535 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]3536 }35373538 return { oldest, newest }3539}3540```35413542Single pass through the array, no copying, no sorting.35433544**Alternative: Math.min/Math.max for small arrays**35453546```typescript3547const numbers = [5, 2, 8, 1, 9]3548const min = Math.min(...numbers)3549const max = Math.max(...numbers)3550```35513552This 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.35533554### 7.13 Use Set/Map for O(1) Lookups35553556**Impact: LOW-MEDIUM (O(n) to O(1))**35573558Convert arrays to Set/Map for repeated membership checks.35593560**Incorrect (O(n) per check):**35613562```typescript3563const allowedIds = ['a', 'b', 'c', ...]3564items.filter(item => allowedIds.includes(item.id))3565```35663567**Correct (O(1) per check):**35683569```typescript3570const allowedIds = new Set(['a', 'b', 'c', ...])3571items.filter(item => allowedIds.has(item.id))3572```35733574### 7.14 Use toSorted() Instead of sort() for Immutability35753576**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**35773578`.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.35793580**Incorrect: mutates original array**35813582```typescript3583function UserList({ users }: { users: User[] }) {3584 // Mutates the users prop array!3585 const sorted = useMemo(3586 () => users.sort((a, b) => a.name.localeCompare(b.name)),3587 [users]3588 )3589 return <div>{sorted.map(renderUser)}</div>3590}3591```35923593**Correct: creates new array**35943595```typescript3596function UserList({ users }: { users: User[] }) {3597 // Creates new sorted array, original unchanged3598 const sorted = useMemo(3599 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),3600 [users]3601 )3602 return <div>{sorted.map(renderUser)}</div>3603}3604```36053606**Why this matters in React:**360736081. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only360936102. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior36113612**Browser support: fallback for older browsers**36133614```typescript3615// Fallback for older browsers3616const sorted = [...items].sort((a, b) => a.value - b.value)3617```36183619`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:36203621**Other immutable array methods:**36223623- `.toSorted()` - immutable sort36243625- `.toReversed()` - immutable reverse36263627- `.toSpliced()` - immutable splice36283629- `.with()` - immutable element replacement36303631---36323633## 8. Advanced Patterns36343635**Impact: LOW**36363637Advanced patterns for specific cases that require careful implementation.36383639### 8.1 Do Not Put Effect Events in Dependency Arrays36403641**Impact: LOW (avoids unnecessary effect re-runs and lint errors)**36423643Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.36443645**Incorrect: Effect Event added as a dependency**36463647```tsx3648import { useEffect, useEffectEvent } from 'react'36493650function ChatRoom({ roomId, onConnected }: {3651 roomId: string3652 onConnected: () => void3653}) {3654 const handleConnected = useEffectEvent(onConnected)36553656 useEffect(() => {3657 const connection = createConnection(roomId)3658 connection.on('connected', handleConnected)3659 connection.connect()36603661 return () => connection.disconnect()3662 }, [roomId, handleConnected])3663}3664```36653666Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.36673668**Correct: depend on reactive values, not the Effect Event**36693670```tsx3671import { useEffect, useEffectEvent } from 'react'36723673function ChatRoom({ roomId, onConnected }: {3674 roomId: string3675 onConnected: () => void3676}) {3677 const handleConnected = useEffectEvent(onConnected)36783679 useEffect(() => {3680 const connection = createConnection(roomId)3681 connection.on('connected', handleConnected)3682 connection.connect()36833684 return () => connection.disconnect()3685 }, [roomId])3686}3687```36883689Reference: [https://react.dev/reference/react/useEffectEvent#effect-event-in-deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)36903691### 8.2 Initialize App Once, Not Per Mount36923693**Impact: LOW-MEDIUM (avoids duplicate init in development)**36943695Do 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.36963697**Incorrect: runs twice in dev, re-runs on remount**36983699```tsx3700function Comp() {3701 useEffect(() => {3702 loadFromStorage()3703 checkAuthToken()3704 }, [])37053706 // ...3707}3708```37093710**Correct: once per app load**37113712```tsx3713let didInit = false37143715function Comp() {3716 useEffect(() => {3717 if (didInit) return3718 didInit = true3719 loadFromStorage()3720 checkAuthToken()3721 }, [])37223723 // ...3724}3725```37263727Reference: [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)37283729### 8.3 Store Event Handlers in Refs37303731**Impact: LOW (stable subscriptions)**37323733Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.37343735**Incorrect: re-subscribes on every render**37363737```tsx3738function useWindowEvent(event: string, handler: (e) => void) {3739 useEffect(() => {3740 window.addEventListener(event, handler)3741 return () => window.removeEventListener(event, handler)3742 }, [event, handler])3743}3744```37453746**Correct: stable subscription**37473748```tsx3749import { useEffectEvent } from 'react'37503751function useWindowEvent(event: string, handler: (e) => void) {3752 const onEvent = useEffectEvent(handler)37533754 useEffect(() => {3755 window.addEventListener(event, onEvent)3756 return () => window.removeEventListener(event, onEvent)3757 }, [event])3758}3759```37603761**Alternative: use `useEffectEvent` if you're on latest React:**37623763`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.37643765### 8.4 useEffectEvent for Stable Callback Refs37663767**Impact: LOW (prevents effect re-runs)**37683769Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.37703771**Incorrect: effect re-runs on every callback change**37723773```tsx3774function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3775 const [query, setQuery] = useState('')37763777 useEffect(() => {3778 const timeout = setTimeout(() => onSearch(query), 300)3779 return () => clearTimeout(timeout)3780 }, [query, onSearch])3781}3782```37833784**Correct: using React's useEffectEvent**37853786```tsx3787import { useEffectEvent } from 'react';37883789function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3790 const [query, setQuery] = useState('')3791 const onSearchEvent = useEffectEvent(onSearch)37923793 useEffect(() => {3794 const timeout = setTimeout(() => onSearchEvent(query), 300)3795 return () => clearTimeout(timeout)3796 }, [query])3797}3798```37993800---38013802## References380338041. [https://react.dev](https://react.dev)38052. [https://nextjs.org](https://nextjs.org)38063. [https://swr.vercel.app](https://swr.vercel.app)38074. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)38085. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)38096. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)38107. [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)3811
Also in itxSaaad/medlens-plus-app
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 |
|---|---|---|---|---|---|
| itxSaaad/medlens-plus-app.cursor/rules/testing.mdc · 0 | Cursor rules | testtesting-strategyapimonorepo | 29/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.claude/skills/web-frontend/references/composition-patterns/AGENTS.md · 0 | AGENTS.md | styleapiuido-not | 57/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.claude/skills/web-frontend/references/react-best-practices/AGENTS.md · 0 | AGENTS.md | buildstyletypessecurity+6 | 65/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/agent-discipline.mdc · 0 | Cursor rules | teststyleagent-behaviour | 53/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/devops-ci.mdc · 0 | Cursor rules | securitydo-not | 23/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/engineering-standards.mdc · 0 | Cursor rules | test | 35/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/fastapi-backend.mdc · 0 | Cursor rules | testlint-formatstyle | 66/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/frontend-quality.mdc · 0 | Cursor rules | securityui | 33/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/github-delivery.mdc · 0 | Cursor rules | git | 16/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/graphify.mdc · 0 | Cursor rules | do-not | 45/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/langgraph-ai-workflows.mdc · 0 | Cursor rules | do-not | 32/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/medical-safety.mdc · 0 | Cursor rules | do-not | 23/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/nextjs-frontend.mdc · 0 | Cursor rules | teststylearch | 80/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/pr-quality-gate.mdc · 0 | Cursor rules | testgit | 43/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/technical-seo.mdc · 0 | Cursor rules | no sections | 24/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/skills/web-frontend/references/composition-patterns/AGENTS.md · 0 | AGENTS.md | styleapiuido-not | 45/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.github/copilot-instructions.md · 0 | Copilot instructions | testgitdo-notagent-behaviour+1 | 70/100 | 3 days ago | |
| itxSaaad/medlens-plus-appAGENTS.md · 0 | AGENTS.md | gitdo-notagent-behaviour | 59/100 | 3 days ago | |
| itxSaaad/medlens-plus-appapps/mobile/CLAUDE.md · 0 | CLAUDE.md | stylearchmonorepoagent-behaviour | 86/100 | 3 days ago | |
| itxSaaad/medlens-plus-appCLAUDE.md · 0 | CLAUDE.md | testgitmonorepodo-not+1 | 76/100 | 3 days ago |
Diff against .cursor/rules/testing.mdc Diff against .claude/skills/web-frontend/references/composition-patterns/AGENTS.md Diff against .claude/skills/web-frontend/references/react-best-practices/AGENTS.md Diff against .cursor/rules/agent-discipline.mdc Diff against .cursor/rules/devops-ci.mdc Diff against .cursor/rules/engineering-standards.mdc Diff against .cursor/rules/fastapi-backend.mdc Diff against .cursor/rules/frontend-quality.mdc Diff against .cursor/rules/github-delivery.mdc Diff against .cursor/rules/graphify.mdc Diff against .cursor/rules/langgraph-ai-workflows.mdc Diff against .cursor/rules/medical-safety.mdc Diff against .cursor/rules/nextjs-frontend.mdc Diff against .cursor/rules/pr-quality-gate.mdc Diff against .cursor/rules/technical-seo.mdc Diff against .cursor/skills/web-frontend/references/composition-patterns/AGENTS.md Diff against .github/copilot-instructions.md Diff against AGENTS.md Diff against apps/mobile/CLAUDE.md Diff against CLAUDE.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 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 | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| aaif-goose/gooseAGENTS.md · 52k | AGENTS.md | setupbuildtestlint-format+6 | 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 |
