AGENTS.md
skills/react-best-practices/AGENTS.mdAGENTS.md
Quality
64/100
Scores the file, not the repository.Length
9,811 words
69 headings · 136 code blocksRepository
4.5k
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# React Best Practices23**Version 1.0.0**4Vercel Engineering5January 202667> **Note:**8> This document is mainly for agents and LLMs to follow when maintaining,9> generating, or refactoring React and Next.js codebases. Humans10> may also find it useful, but guidance here is optimized for automation11> and consistency by AI-assisted workflows.1213---1415## Abstract1617Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.1819---2021## Table of Contents22231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**24 - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)25 - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)26 - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)27 - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)28 - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)292. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**30 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)31 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)32 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)33 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)34 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)353. [Server-Side Performance](#3-server-side-performance) — **HIGH**36 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)37 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)38 - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)39 - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)40 - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)41 - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)42 - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)434. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**44 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)45 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)46 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)47 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)485. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**49 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)50 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)51 - 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)52 - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)53 - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)54 - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)55 - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)56 - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)57 - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates)58 - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization)59 - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates)60 - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values)616. [Rendering Performance](#6-rendering-performance) — **MEDIUM**62 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)63 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)64 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)65 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)66 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)67 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)68 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)69 - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering)70 - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states)717. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**72 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)73 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)74 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)75 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)76 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)77 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)78 - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)79 - 7.8 [Early Return from Functions](#78-early-return-from-functions)80 - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)81 - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)82 - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)83 - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)848. [Advanced Patterns](#8-advanced-patterns) — **LOW**85 - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)86 - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)87 - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)8889---9091## 1. Eliminating Waterfalls9293**Impact: CRITICAL**9495Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.9697### 1.1 Defer Await Until Needed9899**Impact: HIGH (avoids blocking unused code paths)**100101Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.102103**Incorrect: blocks both branches**104105```typescript106async function handleRequest(userId: string, skipProcessing: boolean) {107 const userData = await fetchUserData(userId)108109 if (skipProcessing) {110 // Returns immediately but still waited for userData111 return { skipped: true }112 }113114 // Only this branch uses userData115 return processUserData(userData)116}117```118119**Correct: only blocks when needed**120121```typescript122async function handleRequest(userId: string, skipProcessing: boolean) {123 if (skipProcessing) {124 // Returns immediately without waiting125 return { skipped: true }126 }127128 // Fetch only when needed129 const userData = await fetchUserData(userId)130 return processUserData(userData)131}132```133134**Another example: early return optimization**135136```typescript137// Incorrect: always fetches permissions138async function updateResource(resourceId: string, userId: string) {139 const permissions = await fetchPermissions(userId)140 const resource = await getResource(resourceId)141142 if (!resource) {143 return { error: "Not found" }144 }145146 if (!permissions.canEdit) {147 return { error: "Forbidden" }148 }149150 return await updateResourceData(resource, permissions)151}152153// Correct: fetches only when needed154async function updateResource(resourceId: string, userId: string) {155 const resource = await getResource(resourceId)156157 if (!resource) {158 return { error: "Not found" }159 }160161 const permissions = await fetchPermissions(userId)162163 if (!permissions.canEdit) {164 return { error: "Forbidden" }165 }166167 return await updateResourceData(resource, permissions)168}169```170171This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.172173### 1.2 Dependency-Based Parallelization174175**Impact: CRITICAL (2-10× improvement)**176177For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.178179**Incorrect: profile waits for config unnecessarily**180181```typescript182const [user, config] = await Promise.all([fetchUser(), fetchConfig()])183const profile = await fetchProfile(user.id)184```185186**Correct: config and profile run in parallel**187188```typescript189import { all } from "better-all"190191const { user, config, profile } = await all({192 async user() {193 return fetchUser()194 },195 async config() {196 return fetchConfig()197 },198 async profile() {199 return fetchProfile((await this.$.user).id)200 },201})202```203204**Alternative without extra dependencies:**205206```typescript207const userPromise = fetchUser()208const profilePromise = userPromise.then((user) => fetchProfile(user.id))209210const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise])211```212213We can also create all the promises first, and do `Promise.all()` at the end.214215Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)216217### 1.3 Prevent Waterfall Chains in API Routes218219**Impact: CRITICAL (2-10× improvement)**220221In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.222223**Incorrect: config waits for auth, data waits for both**224225```typescript226export async function GET(request: Request) {227 const session = await auth()228 const config = await fetchConfig()229 const data = await fetchData(session.user.id)230 return Response.json({ data, config })231}232```233234**Correct: auth and config start immediately**235236```typescript237export async function GET(request: Request) {238 const sessionPromise = auth()239 const configPromise = fetchConfig()240 const session = await sessionPromise241 const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)])242 return Response.json({ data, config })243}244```245246For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).247248### 1.4 Promise.all() for Independent Operations249250**Impact: CRITICAL (2-10× improvement)**251252When async operations have no interdependencies, execute them concurrently using `Promise.all()`.253254**Incorrect: sequential execution, 3 round trips**255256```typescript257const user = await fetchUser()258const posts = await fetchPosts()259const comments = await fetchComments()260```261262**Correct: parallel execution, 1 round trip**263264```typescript265const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()])266```267268### 1.5 Strategic Suspense Boundaries269270**Impact: HIGH (faster initial paint)**271272Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.273274**Incorrect: wrapper blocked by data fetching**275276```tsx277async function Page() {278 const data = await fetchData() // Blocks entire page279280 return (281 <div>282 <div>Sidebar</div>283 <div>Header</div>284 <div>285 <DataDisplay data={data} />286 </div>287 <div>Footer</div>288 </div>289 )290}291```292293The entire layout waits for data even though only the middle section needs it.294295**Correct: wrapper shows immediately, data streams in**296297```tsx298function Page() {299 return (300 <div>301 <div>Sidebar</div>302 <div>Header</div>303 <div>304 <Suspense fallback={<Skeleton />}>305 <DataDisplay />306 </Suspense>307 </div>308 <div>Footer</div>309 </div>310 )311}312313async function DataDisplay() {314 const data = await fetchData() // Only blocks this component315 return <div>{data.content}</div>316}317```318319Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.320321**Alternative: share promise across components**322323```tsx324function Page() {325 // Start fetch immediately, but don't await326 const dataPromise = fetchData()327328 return (329 <div>330 <div>Sidebar</div>331 <div>Header</div>332 <Suspense fallback={<Skeleton />}>333 <DataDisplay dataPromise={dataPromise} />334 <DataSummary dataPromise={dataPromise} />335 </Suspense>336 <div>Footer</div>337 </div>338 )339}340341function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {342 const data = use(dataPromise) // Unwraps the promise343 return <div>{data.content}</div>344}345346function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {347 const data = use(dataPromise) // Reuses the same promise348 return <div>{data.summary}</div>349}350```351352Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.353354**When NOT to use this pattern:**355356- Critical data needed for layout decisions (affects positioning)357358- SEO-critical content above the fold359360- Small, fast queries where suspense overhead isn't worth it361362- When you want to avoid layout shift (loading → content jump)363364**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.365366---367368## 2. Bundle Size Optimization369370**Impact: CRITICAL**371372Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.373374### 2.1 Avoid Barrel File Imports375376**Impact: CRITICAL (200-800ms import cost, slow builds)**377378Import 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'`).379380Popular 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.381382**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.383384**Incorrect: imports entire library**385386```tsx387import { Check, X, Menu } from "lucide-react"388// Loads 1,583 modules, takes ~2.8s extra in dev389// Runtime cost: 200-800ms on every cold start390391import { Button, TextField } from "@mui/material"392// Loads 2,225 modules, takes ~4.2s extra in dev393```394395**Correct: imports only what you need**396397```tsx398import Check from "lucide-react/dist/esm/icons/check"399import X from "lucide-react/dist/esm/icons/x"400import Menu from "lucide-react/dist/esm/icons/menu"401// Loads only 3 modules (~2KB vs ~1MB)402403import Button from "@mui/material/Button"404import TextField from "@mui/material/TextField"405// Loads only what you use406```407408**Alternative: Next.js 13.5+**409410```js411// next.config.js - use optimizePackageImports412module.exports = {413 experimental: {414 optimizePackageImports: ["lucide-react", "@mui/material"],415 },416}417418// Then you can keep the ergonomic barrel imports:419import { Check, X, Menu } from "lucide-react"420// Automatically transformed to direct imports at build time421```422423Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.424425Libraries 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`.426427Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)428429### 2.2 Conditional Module Loading430431**Impact: HIGH (loads large data only when needed)**432433Load large data or modules only when a feature is activated.434435**Example: lazy-load animation frames**436437```tsx438function AnimationPlayer({439 enabled,440 setEnabled,441}: {442 enabled: boolean443 setEnabled: React.Dispatch<React.SetStateAction<boolean>>444}) {445 const [frames, setFrames] = useState<Frame[] | null>(null)446447 useEffect(() => {448 if (enabled && !frames && typeof window !== "undefined") {449 import("./animation-frames.js")450 .then((mod) => setFrames(mod.frames))451 .catch(() => setEnabled(false))452 }453 }, [enabled, frames, setEnabled])454455 if (!frames) return <Skeleton />456 return <Canvas frames={frames} />457}458```459460The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.461462### 2.3 Defer Non-Critical Third-Party Libraries463464**Impact: MEDIUM (loads after hydration)**465466Analytics, logging, and error tracking don't block user interaction. Load them after hydration.467468**Incorrect: blocks initial bundle**469470```tsx471import { Analytics } from "@vercel/analytics/react"472473export default function RootLayout({ children }) {474 return (475 <html>476 <body>477 {children}478 <Analytics />479 </body>480 </html>481 )482}483```484485**Correct: loads after hydration**486487```tsx488import dynamic from "next/dynamic"489490const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {491 ssr: false,492})493494export default function RootLayout({ children }) {495 return (496 <html>497 <body>498 {children}499 <Analytics />500 </body>501 </html>502 )503}504```505506### 2.4 Dynamic Imports for Heavy Components507508**Impact: CRITICAL (directly affects TTI and LCP)**509510Use `next/dynamic` to lazy-load large components not needed on initial render.511512**Incorrect: Monaco bundles with main chunk ~300KB**513514```tsx515import { MonacoEditor } from "./monaco-editor"516517function CodePanel({ code }: { code: string }) {518 return <MonacoEditor value={code} />519}520```521522**Correct: Monaco loads on demand**523524```tsx525import dynamic from "next/dynamic"526527const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {528 ssr: false,529})530531function CodePanel({ code }: { code: string }) {532 return <MonacoEditor value={code} />533}534```535536### 2.5 Preload Based on User Intent537538**Impact: MEDIUM (reduces perceived latency)**539540Preload heavy bundles before they're needed to reduce perceived latency.541542**Example: preload on hover/focus**543544```tsx545function EditorButton({ onClick }: { onClick: () => void }) {546 const preload = () => {547 if (typeof window !== "undefined") {548 void import("./monaco-editor")549 }550 }551552 return (553 <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>554 Open Editor555 </button>556 )557}558```559560**Example: preload when feature flag is enabled**561562```tsx563function FlagsProvider({ children, flags }: Props) {564 useEffect(() => {565 if (flags.editorEnabled && typeof window !== "undefined") {566 void import("./monaco-editor").then((mod) => mod.init())567 }568 }, [flags.editorEnabled])569570 return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>571}572```573574The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.575576---577578## 3. Server-Side Performance579580**Impact: HIGH**581582Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.583584### 3.1 Authenticate Server Actions Like API Routes585586**Impact: CRITICAL (prevents unauthorized access to server mutations)**587588Server 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.589590Next.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."591592**Incorrect: no authentication check**593594```typescript595"use server"596597export async function deleteUser(userId: string) {598 // Anyone can call this! No auth check599 await db.user.delete({ where: { id: userId } })600 return { success: true }601}602```603604**Correct: authentication inside the action**605606```typescript607"use server"608609import { verifySession } from "@/lib/auth"610import { unauthorized } from "@/lib/errors"611612export async function deleteUser(userId: string) {613 // Always check auth inside the action614 const session = await verifySession()615616 if (!session) {617 throw unauthorized("Must be logged in")618 }619620 // Check authorization too621 if (session.user.role !== "admin" && session.user.id !== userId) {622 throw unauthorized("Cannot delete other users")623 }624625 await db.user.delete({ where: { id: userId } })626 return { success: true }627}628```629630**With input validation:**631632```typescript633"use server"634635import { verifySession } from "@/lib/auth"636import { z } from "zod"637638const updateProfileSchema = z.object({639 userId: z.string().uuid(),640 name: z.string().min(1).max(100),641 email: z.string().email(),642})643644export async function updateProfile(data: unknown) {645 // Validate input first646 const validated = updateProfileSchema.parse(data)647648 // Then authenticate649 const session = await verifySession()650 if (!session) {651 throw new Error("Unauthorized")652 }653654 // Then authorize655 if (session.user.id !== validated.userId) {656 throw new Error("Can only update own profile")657 }658659 // Finally perform the mutation660 await db.user.update({661 where: { id: validated.userId },662 data: {663 name: validated.name,664 email: validated.email,665 },666 })667668 return { success: true }669}670```671672Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)673674### 3.2 Avoid Duplicate Serialization in RSC Props675676**Impact: LOW (reduces network payload by avoiding duplicate serialization)**677678RSC→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.679680**Incorrect: duplicates array**681682```tsx683// RSC: sends 6 strings (2 arrays × 3 items)684<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />685```686687**Correct: sends 3 strings**688689```tsx690// RSC: send once691;<ClientList usernames={usernames} />692693// Client: transform there694;("use client")695const sorted = useMemo(() => [...usernames].sort(), [usernames])696```697698**Nested deduplication behavior:**699700```tsx701// string[] - duplicates everything702usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings703704// object[] - duplicates array structure only705users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)706```707708Deduplication works recursively. Impact varies by data type:709710- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated711712- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference713714**Operations breaking deduplication: create new references**715716- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`717718- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`719720**More examples:**721722```tsx723// ❌ Bad724<C users={users} active={users.filter(u => u.active)} />725<C product={product} productName={product.name} />726727// ✅ Good728<C users={users} />729<C product={product} />730// Do filtering/destructuring in client731```732733**Exception:** Pass derived data when transformation is expensive or client doesn't need original.734735### 3.3 Cross-Request LRU Caching736737**Impact: HIGH (caches across requests)**738739`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.740741**Implementation:**742743```typescript744import { LRUCache } from "lru-cache"745746const cache = new LRUCache<string, any>({747 max: 1000,748 ttl: 5 * 60 * 1000, // 5 minutes749})750751export async function getUser(id: string) {752 const cached = cache.get(id)753 if (cached) return cached754755 const user = await db.user.findUnique({ where: { id } })756 cache.set(id, user)757 return user758}759760// Request 1: DB query, result cached761// Request 2: cache hit, no DB query762```763764Use when sequential user actions hit multiple endpoints needing the same data within seconds.765766**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.767768**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.769770Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)771772### 3.4 Minimize Serialization at RSC Boundaries773774**Impact: HIGH (reduces data transfer size)**775776The 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.777778**Incorrect: serializes all 50 fields**779780```tsx781async function Page() {782 const user = await fetchUser() // 50 fields783 return <Profile user={user} />784}785786;("use client")787function Profile({ user }: { user: User }) {788 return <div>{user.name}</div> // uses 1 field789}790```791792**Correct: serializes only 1 field**793794```tsx795async function Page() {796 const user = await fetchUser()797 return <Profile name={user.name} />798}799800;("use client")801function Profile({ name }: { name: string }) {802 return <div>{name}</div>803}804```805806### 3.5 Parallel Data Fetching with Component Composition807808**Impact: CRITICAL (eliminates server-side waterfalls)**809810React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.811812**Incorrect: Sidebar waits for Page's fetch to complete**813814```tsx815export default async function Page() {816 const header = await fetchHeader()817 return (818 <div>819 <div>{header}</div>820 <Sidebar />821 </div>822 )823}824825async function Sidebar() {826 const items = await fetchSidebarItems()827 return <nav>{items.map(renderItem)}</nav>828}829```830831**Correct: both fetch simultaneously**832833```tsx834async function Header() {835 const data = await fetchHeader()836 return <div>{data}</div>837}838839async function Sidebar() {840 const items = await fetchSidebarItems()841 return <nav>{items.map(renderItem)}</nav>842}843844export default function Page() {845 return (846 <div>847 <Header />848 <Sidebar />849 </div>850 )851}852```853854**Alternative with children prop:**855856```tsx857async function Header() {858 const data = await fetchHeader()859 return <div>{data}</div>860}861862async function Sidebar() {863 const items = await fetchSidebarItems()864 return <nav>{items.map(renderItem)}</nav>865}866867function Layout({ children }: { children: ReactNode }) {868 return (869 <div>870 <Header />871 {children}872 </div>873 )874}875876export default function Page() {877 return (878 <Layout>879 <Sidebar />880 </Layout>881 )882}883```884885### 3.6 Per-Request Deduplication with React.cache()886887**Impact: MEDIUM (deduplicates within request)**888889Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.890891**Usage:**892893```typescript894import { cache } from "react"895896export const getCurrentUser = cache(async () => {897 const session = await auth()898 if (!session?.user?.id) return null899 return await db.user.findUnique({900 where: { id: session.user.id },901 })902})903```904905Within a single request, multiple calls to `getCurrentUser()` execute the query only once.906907**Avoid inline objects as arguments:**908909`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.910911**Incorrect: always cache miss**912913```typescript914const getUser = cache(async (params: { uid: number }) => {915 return await db.user.findUnique({ where: { id: params.uid } })916})917918// Each call creates new object, never hits cache919getUser({ uid: 1 })920getUser({ uid: 1 }) // Cache miss, runs query again921```922923**Correct: cache hit**924925```typescript926const params = { uid: 1 }927getUser(params) // Query runs928getUser(params) // Cache hit (same reference)929```930931If you must pass objects, pass the same reference:932933**Next.js-Specific Note:**934935In 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:936937- Database queries (Prisma, Drizzle, etc.)938939- Heavy computations940941- Authentication checks942943- File system operations944945- Any non-fetch async work946947Use `React.cache()` to deduplicate these operations across your component tree.948949Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)950951### 3.7 Use after() for Non-Blocking Operations952953**Impact: MEDIUM (faster response times)**954955Use 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.956957**Incorrect: blocks response**958959```tsx960import { logUserAction } from "@/app/utils"961962export async function POST(request: Request) {963 // Perform mutation964 await updateDatabase(request)965966 // Logging blocks the response967 const userAgent = request.headers.get("user-agent") || "unknown"968 await logUserAction({ userAgent })969970 return new Response(JSON.stringify({ status: "success" }), {971 status: 200,972 headers: { "Content-Type": "application/json" },973 })974}975```976977**Correct: non-blocking**978979```tsx980import { after } from "next/server"981import { headers, cookies } from "next/headers"982import { logUserAction } from "@/app/utils"983984export async function POST(request: Request) {985 // Perform mutation986 await updateDatabase(request)987988 // Log after response is sent989 after(async () => {990 const userAgent = (await headers()).get("user-agent") || "unknown"991 const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"992993 logUserAction({ sessionCookie, userAgent })994 })995996 return new Response(JSON.stringify({ status: "success" }), {997 status: 200,998 headers: { "Content-Type": "application/json" },999 })1000}1001```10021003The response is sent immediately while logging happens in the background.10041005**Common use cases:**10061007- Analytics tracking10081009- Audit logging10101011- Sending notifications10121013- Cache invalidation10141015- Cleanup tasks10161017**Important notes:**10181019- `after()` runs even if the response fails or redirects10201021- Works in Server Actions, Route Handlers, and Server Components10221023Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)10241025---10261027## 4. Client-Side Data Fetching10281029**Impact: MEDIUM-HIGH**10301031Automatic deduplication and efficient data fetching patterns reduce redundant network requests.10321033### 4.1 Deduplicate Global Event Listeners10341035**Impact: LOW (single listener for N components)**10361037Use `useSWRSubscription()` to share global event listeners across component instances.10381039**Incorrect: N instances = N listeners**10401041```tsx1042function useKeyboardShortcut(key: string, callback: () => void) {1043 useEffect(() => {1044 const handler = (e: KeyboardEvent) => {1045 if (e.metaKey && e.key === key) {1046 callback()1047 }1048 }1049 window.addEventListener("keydown", handler)1050 return () => window.removeEventListener("keydown", handler)1051 }, [key, callback])1052}1053```10541055When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.10561057**Correct: N instances = 1 listener**10581059```tsx1060import useSWRSubscription from "swr/subscription"10611062// Module-level Map to track callbacks per key1063const keyCallbacks = new Map<string, Set<() => void>>()10641065function useKeyboardShortcut(key: string, callback: () => void) {1066 // Register this callback in the Map1067 useEffect(() => {1068 if (!keyCallbacks.has(key)) {1069 keyCallbacks.set(key, new Set())1070 }1071 keyCallbacks.get(key)!.add(callback)10721073 return () => {1074 const set = keyCallbacks.get(key)1075 if (set) {1076 set.delete(callback)1077 if (set.size === 0) {1078 keyCallbacks.delete(key)1079 }1080 }1081 }1082 }, [key, callback])10831084 useSWRSubscription("global-keydown", () => {1085 const handler = (e: KeyboardEvent) => {1086 if (e.metaKey && keyCallbacks.has(e.key)) {1087 keyCallbacks.get(e.key)!.forEach((cb) => cb())1088 }1089 }1090 window.addEventListener("keydown", handler)1091 return () => window.removeEventListener("keydown", handler)1092 })1093}10941095function Profile() {1096 // Multiple shortcuts will share the same listener1097 useKeyboardShortcut("p", () => {1098 /* ... */1099 })1100 useKeyboardShortcut("k", () => {1101 /* ... */1102 })1103 // ...1104}1105```11061107### 4.2 Use Passive Event Listeners for Scrolling Performance11081109**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**11101111Add `{ 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.11121113**Incorrect:**11141115```typescript1116useEffect(() => {1117 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1118 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11191120 document.addEventListener("touchstart", handleTouch)1121 document.addEventListener("wheel", handleWheel)11221123 return () => {1124 document.removeEventListener("touchstart", handleTouch)1125 document.removeEventListener("wheel", handleWheel)1126 }1127}, [])1128```11291130**Correct:**11311132```typescript1133useEffect(() => {1134 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1135 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11361137 document.addEventListener("touchstart", handleTouch, { passive: true })1138 document.addEventListener("wheel", handleWheel, { passive: true })11391140 return () => {1141 document.removeEventListener("touchstart", handleTouch)1142 document.removeEventListener("wheel", handleWheel)1143 }1144}, [])1145```11461147**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.11481149**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.11501151### 4.3 Use SWR for Automatic Deduplication11521153**Impact: MEDIUM-HIGH (automatic deduplication)**11541155SWR enables request deduplication, caching, and revalidation across component instances.11561157**Incorrect: no deduplication, each instance fetches**11581159```tsx1160function UserList() {1161 const [users, setUsers] = useState([])1162 useEffect(() => {1163 fetch("/api/users")1164 .then((r) => r.json())1165 .then(setUsers)1166 }, [])1167}1168```11691170**Correct: multiple instances share one request**11711172```tsx1173import useSWR from "swr"11741175function UserList() {1176 const { data: users } = useSWR("/api/users", fetcher)1177}1178```11791180**For immutable data:**11811182```tsx1183import { useImmutableSWR } from "@/lib/swr"11841185function StaticContent() {1186 const { data } = useImmutableSWR("/api/config", fetcher)1187}1188```11891190**For mutations:**11911192```tsx1193import { useSWRMutation } from "swr/mutation"11941195function UpdateButton() {1196 const { trigger } = useSWRMutation("/api/user", updateUser)1197 return <button onClick={() => trigger()}>Update</button>1198}1199```12001201Reference: [https://swr.vercel.app](https://swr.vercel.app)12021203### 4.4 Version and Minimize localStorage Data12041205**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**12061207Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.12081209**Incorrect:**12101211```typescript1212// No version, stores everything, no error handling1213localStorage.setItem("userConfig", JSON.stringify(fullUserObject))1214const data = localStorage.getItem("userConfig")1215```12161217**Correct:**12181219```typescript1220const VERSION = "v2"12211222function saveConfig(config: { theme: string; language: string }) {1223 try {1224 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))1225 } catch {1226 // Throws in incognito/private browsing, quota exceeded, or disabled1227 }1228}12291230function loadConfig() {1231 try {1232 const data = localStorage.getItem(`userConfig:${VERSION}`)1233 return data ? JSON.parse(data) : null1234 } catch {1235 return null1236 }1237}12381239// Migration from v1 to v21240function migrate() {1241 try {1242 const v1 = localStorage.getItem("userConfig:v1")1243 if (v1) {1244 const old = JSON.parse(v1)1245 saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang })1246 localStorage.removeItem("userConfig:v1")1247 }1248 } catch {}1249}1250```12511252**Store minimal fields from server responses:**12531254```typescript1255// User object has 20+ fields, only store what UI needs1256function cachePrefs(user: FullUser) {1257 try {1258 localStorage.setItem(1259 "prefs:v1",1260 JSON.stringify({1261 theme: user.preferences.theme,1262 notifications: user.preferences.notifications,1263 })1264 )1265 } catch {}1266}1267```12681269**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.12701271**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.12721273---12741275## 5. Re-render Optimization12761277**Impact: MEDIUM**12781279Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.12801281### 5.1 Calculate Derived State During Rendering12821283**Impact: MEDIUM (avoids redundant renders and state drift)**12841285If 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.12861287**Incorrect: redundant state and effect**12881289```tsx1290function Form() {1291 const [firstName, setFirstName] = useState("First")1292 const [lastName, setLastName] = useState("Last")1293 const [fullName, setFullName] = useState("")12941295 useEffect(() => {1296 setFullName(firstName + " " + lastName)1297 }, [firstName, lastName])12981299 return <p>{fullName}</p>1300}1301```13021303**Correct: derive during render**13041305```tsx1306function Form() {1307 const [firstName, setFirstName] = useState("First")1308 const [lastName, setLastName] = useState("Last")1309 const fullName = firstName + " " + lastName13101311 return <p>{fullName}</p>1312}1313```13141315Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)13161317### 5.2 Defer State Reads to Usage Point13181319**Impact: MEDIUM (avoids unnecessary subscriptions)**13201321Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.13221323**Incorrect: subscribes to all searchParams changes**13241325```tsx1326function ShareButton({ chatId }: { chatId: string }) {1327 const searchParams = useSearchParams()13281329 const handleShare = () => {1330 const ref = searchParams.get("ref")1331 shareChat(chatId, { ref })1332 }13331334 return <button onClick={handleShare}>Share</button>1335}1336```13371338**Correct: reads on demand, no subscription**13391340```tsx1341function ShareButton({ chatId }: { chatId: string }) {1342 const handleShare = () => {1343 const params = new URLSearchParams(window.location.search)1344 const ref = params.get("ref")1345 shareChat(chatId, { ref })1346 }13471348 return <button onClick={handleShare}>Share</button>1349}1350```13511352### 5.3 Do not wrap a simple expression with a primitive result type in useMemo13531354**Impact: LOW-MEDIUM (wasted computation on every render)**13551356When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.13571358Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.13591360**Incorrect:**13611362```tsx1363function Header({ user, notifications }: Props) {1364 const isLoading = useMemo(() => {1365 return user.isLoading || notifications.isLoading1366 }, [user.isLoading, notifications.isLoading])13671368 if (isLoading) return <Skeleton />1369 // return some markup1370}1371```13721373**Correct:**13741375```tsx1376function Header({ user, notifications }: Props) {1377 const isLoading = user.isLoading || notifications.isLoading13781379 if (isLoading) return <Skeleton />1380 // return some markup1381}1382```13831384### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant13851386**Impact: MEDIUM (restores memoization by using a constant for default value)**13871388When 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()`.13891390To address this issue, extract the default value into a constant.13911392**Incorrect: `onClick` has different values on every rerender**13931394```tsx1395const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {1396 // ...1397})13981399// Used without optional onClick1400<UserAvatar />1401```14021403**Correct: stable default value**14041405```tsx1406const NOOP = () => {};14071408const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {1409 // ...1410})14111412// Used without optional onClick1413<UserAvatar />1414```14151416### 5.5 Extract to Memoized Components14171418**Impact: MEDIUM (enables early returns)**14191420Extract expensive work into memoized components to enable early returns before computation.14211422**Incorrect: computes avatar even when loading**14231424```tsx1425function Profile({ user, loading }: Props) {1426 const avatar = useMemo(() => {1427 const id = computeAvatarId(user)1428 return <Avatar id={id} />1429 }, [user])14301431 if (loading) return <Skeleton />1432 return <div>{avatar}</div>1433}1434```14351436**Correct: skips computation when loading**14371438```tsx1439const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1440 const id = useMemo(() => computeAvatarId(user), [user])1441 return <Avatar id={id} />1442})14431444function Profile({ user, loading }: Props) {1445 if (loading) return <Skeleton />1446 return (1447 <div>1448 <UserAvatar user={user} />1449 </div>1450 )1451}1452```14531454**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.14551456### 5.6 Narrow Effect Dependencies14571458**Impact: LOW (minimizes effect re-runs)**14591460Specify primitive dependencies instead of objects to minimize effect re-runs.14611462**Incorrect: re-runs on any user field change**14631464```tsx1465useEffect(() => {1466 console.log(user.id)1467}, [user])1468```14691470**Correct: re-runs only when id changes**14711472```tsx1473useEffect(() => {1474 console.log(user.id)1475}, [user.id])1476```14771478**For derived state, compute outside effect:**14791480```tsx1481// Incorrect: runs on width=767, 766, 765...1482useEffect(() => {1483 if (width < 768) {1484 enableMobileMode()1485 }1486}, [width])14871488// Correct: runs only on boolean transition1489const isMobile = width < 7681490useEffect(() => {1491 if (isMobile) {1492 enableMobileMode()1493 }1494}, [isMobile])1495```14961497### 5.7 Put Interaction Logic in Event Handlers14981499**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**15001501If 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.15021503**Incorrect: event modeled as state + effect**15041505```tsx1506function Form() {1507 const [submitted, setSubmitted] = useState(false)1508 const theme = useContext(ThemeContext)15091510 useEffect(() => {1511 if (submitted) {1512 post("/api/register")1513 showToast("Registered", theme)1514 }1515 }, [submitted, theme])15161517 return <button onClick={() => setSubmitted(true)}>Submit</button>1518}1519```15201521**Correct: do it in the handler**15221523```tsx1524function Form() {1525 const theme = useContext(ThemeContext)15261527 function handleSubmit() {1528 post("/api/register")1529 showToast("Registered", theme)1530 }15311532 return <button onClick={handleSubmit}>Submit</button>1533}1534```15351536Reference: [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)15371538### 5.8 Subscribe to Derived State15391540**Impact: MEDIUM (reduces re-render frequency)**15411542Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.15431544**Incorrect: re-renders on every pixel change**15451546```tsx1547function Sidebar() {1548 const width = useWindowWidth() // updates continuously1549 const isMobile = width < 7681550 return <nav className={isMobile ? "mobile" : "desktop"} />1551}1552```15531554**Correct: re-renders only when boolean changes**15551556```tsx1557function Sidebar() {1558 const isMobile = useMediaQuery("(max-width: 767px)")1559 return <nav className={isMobile ? "mobile" : "desktop"} />1560}1561```15621563### 5.9 Use Functional setState Updates15641565**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**15661567When 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.15681569**Incorrect: requires state as dependency**15701571```tsx1572function TodoList() {1573 const [items, setItems] = useState(initialItems)15741575 // Callback must depend on items, recreated on every items change1576 const addItems = useCallback(1577 (newItems: Item[]) => {1578 setItems([...items, ...newItems])1579 },1580 [items]1581 ) // ❌ items dependency causes recreations15821583 // Risk of stale closure if dependency is forgotten1584 const removeItem = useCallback((id: string) => {1585 setItems(items.filter((item) => item.id !== id))1586 }, []) // ❌ Missing items dependency - will use stale items!15871588 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1589}1590```15911592The 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.15931594**Correct: stable callbacks, no stale closures**15951596```tsx1597function TodoList() {1598 const [items, setItems] = useState(initialItems)15991600 // Stable callback, never recreated1601 const addItems = useCallback((newItems: Item[]) => {1602 setItems((curr) => [...curr, ...newItems])1603 }, []) // ✅ No dependencies needed16041605 // Always uses latest state, no stale closure risk1606 const removeItem = useCallback((id: string) => {1607 setItems((curr) => curr.filter((item) => item.id !== id))1608 }, []) // ✅ Safe and stable16091610 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1611}1612```16131614**Benefits:**161516161. **Stable callback references** - Callbacks don't need to be recreated when state changes161716182. **No stale closures** - Always operates on the latest state value161916203. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks162116224. **Prevents bugs** - Eliminates the most common source of React closure bugs16231624**When to use functional updates:**16251626- Any setState that depends on the current state value16271628- Inside useCallback/useMemo when state is needed16291630- Event handlers that reference state16311632- Async operations that update state16331634**When direct updates are fine:**16351636- Setting state to a static value: `setCount(0)`16371638- Setting state from props/arguments only: `setName(newName)`16391640- State doesn't depend on previous value16411642**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.16431644### 5.10 Use Lazy State Initialization16451646**Impact: MEDIUM (wasted computation on every render)**16471648Pass 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.16491650**Incorrect: runs on every render**16511652```tsx1653function FilteredList({ items }: { items: Item[] }) {1654 // buildSearchIndex() runs on EVERY render, even after initialization1655 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))1656 const [query, setQuery] = useState("")16571658 // When query changes, buildSearchIndex runs again unnecessarily1659 return <SearchResults index={searchIndex} query={query} />1660}16611662function UserProfile() {1663 // JSON.parse runs on every render1664 const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"))16651666 return <SettingsForm settings={settings} onChange={setSettings} />1667}1668```16691670**Correct: runs only once**16711672```tsx1673function FilteredList({ items }: { items: Item[] }) {1674 // buildSearchIndex() runs ONLY on initial render1675 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))1676 const [query, setQuery] = useState("")16771678 return <SearchResults index={searchIndex} query={query} />1679}16801681function UserProfile() {1682 // JSON.parse runs only on initial render1683 const [settings, setSettings] = useState(() => {1684 const stored = localStorage.getItem("settings")1685 return stored ? JSON.parse(stored) : {}1686 })16871688 return <SettingsForm settings={settings} onChange={setSettings} />1689}1690```16911692Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.16931694For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.16951696### 5.11 Use Transitions for Non-Urgent Updates16971698**Impact: MEDIUM (maintains UI responsiveness)**16991700Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.17011702**Incorrect: blocks UI on every scroll**17031704```tsx1705function ScrollTracker() {1706 const [scrollY, setScrollY] = useState(0)1707 useEffect(() => {1708 const handler = () => setScrollY(window.scrollY)1709 window.addEventListener("scroll", handler, { passive: true })1710 return () => window.removeEventListener("scroll", handler)1711 }, [])1712}1713```17141715**Correct: non-blocking updates**17161717```tsx1718import { startTransition } from "react"17191720function ScrollTracker() {1721 const [scrollY, setScrollY] = useState(0)1722 useEffect(() => {1723 const handler = () => {1724 startTransition(() => setScrollY(window.scrollY))1725 }1726 window.addEventListener("scroll", handler, { passive: true })1727 return () => window.removeEventListener("scroll", handler)1728 }, [])1729}1730```17311732### 5.12 Use useRef for Transient Values17331734**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**17351736When 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.17371738**Incorrect: renders every update**17391740```tsx1741function Tracker() {1742 const [lastX, setLastX] = useState(0)17431744 useEffect(() => {1745 const onMove = (e: MouseEvent) => setLastX(e.clientX)1746 window.addEventListener("mousemove", onMove)1747 return () => window.removeEventListener("mousemove", onMove)1748 }, [])17491750 return (1751 <div1752 style={{1753 position: "fixed",1754 top: 0,1755 left: lastX,1756 width: 8,1757 height: 8,1758 background: "black",1759 }}1760 />1761 )1762}1763```17641765**Correct: no re-render for tracking**17661767```tsx1768function Tracker() {1769 const lastXRef = useRef(0)1770 const dotRef = useRef<HTMLDivElement>(null)17711772 useEffect(() => {1773 const onMove = (e: MouseEvent) => {1774 lastXRef.current = e.clientX1775 const node = dotRef.current1776 if (node) {1777 node.style.transform = `translateX(${e.clientX}px)`1778 }1779 }1780 window.addEventListener("mousemove", onMove)1781 return () => window.removeEventListener("mousemove", onMove)1782 }, [])17831784 return (1785 <div1786 ref={dotRef}1787 style={{1788 position: "fixed",1789 top: 0,1790 left: 0,1791 width: 8,1792 height: 8,1793 background: "black",1794 transform: "translateX(0px)",1795 }}1796 />1797 )1798}1799```18001801---18021803## 6. Rendering Performance18041805**Impact: MEDIUM**18061807Optimizing the rendering process reduces the work the browser needs to do.18081809### 6.1 Animate SVG Wrapper Instead of SVG Element18101811**Impact: LOW (enables hardware acceleration)**18121813Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.18141815**Incorrect: animating SVG directly - no hardware acceleration**18161817```tsx1818function LoadingSpinner() {1819 return (1820 <svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">1821 <circle cx="12" cy="12" r="10" stroke="currentColor" />1822 </svg>1823 )1824}1825```18261827**Correct: animating wrapper div - hardware accelerated**18281829```tsx1830function LoadingSpinner() {1831 return (1832 <div className="animate-spin">1833 <svg width="24" height="24" viewBox="0 0 24 24">1834 <circle cx="12" cy="12" r="10" stroke="currentColor" />1835 </svg>1836 </div>1837 )1838}1839```18401841This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.18421843### 6.2 CSS content-visibility for Long Lists18441845**Impact: HIGH (faster initial render)**18461847Apply `content-visibility: auto` to defer off-screen rendering.18481849**CSS:**18501851```css1852.message-item {1853 content-visibility: auto;1854 contain-intrinsic-size: 0 80px;1855}1856```18571858**Example:**18591860```tsx1861function MessageList({ messages }: { messages: Message[] }) {1862 return (1863 <div className="h-screen overflow-y-auto">1864 {messages.map((msg) => (1865 <div key={msg.id} className="message-item">1866 <Avatar user={msg.author} />1867 <div>{msg.content}</div>1868 </div>1869 ))}1870 </div>1871 )1872}1873```18741875For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).18761877### 6.3 Hoist Static JSX Elements18781879**Impact: LOW (avoids re-creation)**18801881Extract static JSX outside components to avoid re-creation.18821883**Incorrect: recreates element every render**18841885```tsx1886function LoadingSkeleton() {1887 return <div className="h-20 animate-pulse bg-gray-200" />1888}18891890function Container() {1891 return <div>{loading && <LoadingSkeleton />}</div>1892}1893```18941895**Correct: reuses same element**18961897```tsx1898const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />18991900function Container() {1901 return <div>{loading && loadingSkeleton}</div>1902}1903```19041905This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.19061907**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.19081909### 6.4 Optimize SVG Precision19101911**Impact: LOW (reduces file size)**19121913Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.19141915**Incorrect: excessive precision**19161917```svg1918<path d="M 10.293847 20.847362 L 30.938472 40.192837" />1919```19201921**Correct: 1 decimal place**19221923```svg1924<path d="M 10.3 20.8 L 30.9 40.2" />1925```19261927**Automate with SVGO:**19281929```bash1930npx svgo --precision=1 --multipass icon.svg1931```19321933### 6.5 Prevent Hydration Mismatch Without Flickering19341935**Impact: MEDIUM (avoids visual flicker and hydration errors)**19361937When 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.19381939**Incorrect: breaks SSR**19401941```tsx1942function ThemeWrapper({ children }: { children: ReactNode }) {1943 // localStorage is not available on server - throws error1944 const theme = localStorage.getItem("theme") || "light"19451946 return <div className={theme}>{children}</div>1947}1948```19491950Server-side rendering will fail because `localStorage` is undefined.19511952**Incorrect: visual flickering**19531954```tsx1955function ThemeWrapper({ children }: { children: ReactNode }) {1956 const [theme, setTheme] = useState("light")19571958 useEffect(() => {1959 // Runs after hydration - causes visible flash1960 const stored = localStorage.getItem("theme")1961 if (stored) {1962 setTheme(stored)1963 }1964 }, [])19651966 return <div className={theme}>{children}</div>1967}1968```19691970Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.19711972**Correct: no flicker, no hydration mismatch**19731974```tsx1975function ThemeWrapper({ children }: { children: ReactNode }) {1976 return (1977 <>1978 <div id="theme-wrapper">{children}</div>1979 <script1980 dangerouslySetInnerHTML={{1981 __html: `1982 (function() {1983 try {1984 var theme = localStorage.getItem('theme') || 'light';1985 var el = document.getElementById('theme-wrapper');1986 if (el) el.className = theme;1987 } catch (e) {}1988 })();1989 `,1990 }}1991 />1992 </>1993 )1994}1995```19961997The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.19981999This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.20002001### 6.6 Suppress Expected Hydration Mismatches20022003**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**20042005In 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.20062007**Incorrect: known mismatch warnings**20082009```tsx2010function Timestamp() {2011 return <span>{new Date().toLocaleString()}</span>2012}2013```20142015**Correct: suppress expected mismatch only**20162017```tsx2018function Timestamp() {2019 return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>2020}2021```20222023### 6.7 Use Activity Component for Show/Hide20242025**Impact: MEDIUM (preserves state/DOM)**20262027Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.20282029**Usage:**20302031```tsx2032import { Activity } from "react"20332034function Dropdown({ isOpen }: Props) {2035 return (2036 <Activity mode={isOpen ? "visible" : "hidden"}>2037 <ExpensiveMenu />2038 </Activity>2039 )2040}2041```20422043Avoids expensive re-renders and state loss.20442045### 6.8 Use Explicit Conditional Rendering20462047**Impact: LOW (prevents rendering 0 or NaN)**20482049Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.20502051**Incorrect: renders "0" when count is 0**20522053```tsx2054function Badge({ count }: { count: number }) {2055 return <div>{count && <span className="badge">{count}</span>}</div>2056}20572058// When count = 0, renders: <div>0</div>2059// When count = 5, renders: <div><span class="badge">5</span></div>2060```20612062**Correct: renders nothing when count is 0**20632064```tsx2065function Badge({ count }: { count: number }) {2066 return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>2067}20682069// When count = 0, renders: <div></div>2070// When count = 5, renders: <div><span class="badge">5</span></div>2071```20722073### 6.9 Use useTransition Over Manual Loading States20742075**Impact: LOW (reduces re-renders and improves code clarity)**20762077Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.20782079**Incorrect: manual loading state**20802081```tsx2082function SearchResults() {2083 const [query, setQuery] = useState("")2084 const [results, setResults] = useState([])2085 const [isLoading, setIsLoading] = useState(false)20862087 const handleSearch = async (value: string) => {2088 setIsLoading(true)2089 setQuery(value)2090 const data = await fetchResults(value)2091 setResults(data)2092 setIsLoading(false)2093 }20942095 return (2096 <>2097 <input onChange={(e) => handleSearch(e.target.value)} />2098 {isLoading && <Spinner />}2099 <ResultsList results={results} />2100 </>2101 )2102}2103```21042105**Correct: useTransition with built-in pending state**21062107```tsx2108import { useTransition, useState } from "react"21092110function SearchResults() {2111 const [query, setQuery] = useState("")2112 const [results, setResults] = useState([])2113 const [isPending, startTransition] = useTransition()21142115 const handleSearch = (value: string) => {2116 setQuery(value) // Update input immediately21172118 startTransition(async () => {2119 // Fetch and update results2120 const data = await fetchResults(value)2121 setResults(data)2122 })2123 }21242125 return (2126 <>2127 <input onChange={(e) => handleSearch(e.target.value)} />2128 {isPending && <Spinner />}2129 <ResultsList results={results} />2130 </>2131 )2132}2133```21342135**Benefits:**21362137- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`21382139- **Error resilience**: Pending state correctly resets even if the transition throws21402141- **Better responsiveness**: Keeps the UI responsive during updates21422143- **Interrupt handling**: New transitions automatically cancel pending ones21442145Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)21462147---21482149## 7. JavaScript Performance21502151**Impact: LOW-MEDIUM**21522153Micro-optimizations for hot paths can add up to meaningful improvements.21542155### 7.1 Avoid Layout Thrashing21562157**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**21582159Avoid 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.21602161**This is OK: browser batches style changes**21622163```typescript2164function updateElementStyles(element: HTMLElement) {2165 // Each line invalidates style, but browser batches the recalculation2166 element.style.width = "100px"2167 element.style.height = "200px"2168 element.style.backgroundColor = "blue"2169 element.style.border = "1px solid black"2170}2171```21722173**Incorrect: interleaved reads and writes force reflows**21742175```typescript2176function layoutThrashing(element: HTMLElement) {2177 element.style.width = "100px"2178 const width = element.offsetWidth // Forces reflow2179 element.style.height = "200px"2180 const height = element.offsetHeight // Forces another reflow2181}2182```21832184**Correct: batch writes, then read once**21852186```typescript2187function updateElementStyles(element: HTMLElement) {2188 // Batch all writes together2189 element.style.width = "100px"2190 element.style.height = "200px"2191 element.style.backgroundColor = "blue"2192 element.style.border = "1px solid black"21932194 // Read after all writes are done (single reflow)2195 const { width, height } = element.getBoundingClientRect()2196}2197```21982199**Correct: batch reads, then writes**22002201```typescript2202function updateElementStyles(element: HTMLElement) {2203 element.classList.add("highlighted-box")22042205 const { width, height } = element.getBoundingClientRect()2206}2207```22082209**Better: use CSS classes**22102211**React example:**22122213```tsx2214// Incorrect: interleaving style changes with layout queries2215function Box({ isHighlighted }: { isHighlighted: boolean }) {2216 const ref = useRef<HTMLDivElement>(null)22172218 useEffect(() => {2219 if (ref.current && isHighlighted) {2220 ref.current.style.width = "100px"2221 const width = ref.current.offsetWidth // Forces layout2222 ref.current.style.height = "200px"2223 }2224 }, [isHighlighted])22252226 return <div ref={ref}>Content</div>2227}22282229// Correct: toggle class2230function Box({ isHighlighted }: { isHighlighted: boolean }) {2231 return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>2232}2233```22342235Prefer 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.22362237See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.22382239### 7.2 Build Index Maps for Repeated Lookups22402241**Impact: LOW-MEDIUM (1M ops to 2K ops)**22422243Multiple `.find()` calls by the same key should use a Map.22442245**Incorrect (O(n) per lookup):**22462247```typescript2248function processOrders(orders: Order[], users: User[]) {2249 return orders.map((order) => ({2250 ...order,2251 user: users.find((u) => u.id === order.userId),2252 }))2253}2254```22552256**Correct (O(1) per lookup):**22572258```typescript2259function processOrders(orders: Order[], users: User[]) {2260 const userById = new Map(users.map((u) => [u.id, u]))22612262 return orders.map((order) => ({2263 ...order,2264 user: userById.get(order.userId),2265 }))2266}2267```22682269Build map once (O(n)), then all lookups are O(1).22702271For 1000 orders × 1000 users: 1M ops → 2K ops.22722273### 7.3 Cache Property Access in Loops22742275**Impact: LOW-MEDIUM (reduces lookups)**22762277Cache object property lookups in hot paths.22782279**Incorrect: 3 lookups × N iterations**22802281```typescript2282for (let i = 0; i < arr.length; i++) {2283 process(obj.config.settings.value)2284}2285```22862287**Correct: 1 lookup total**22882289```typescript2290const value = obj.config.settings.value2291const len = arr.length2292for (let i = 0; i < len; i++) {2293 process(value)2294}2295```22962297### 7.4 Cache Repeated Function Calls22982299**Impact: MEDIUM (avoid redundant computation)**23002301Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.23022303**Incorrect: redundant computation**23042305```typescript2306function ProjectList({ projects }: { projects: Project[] }) {2307 return (2308 <div>2309 {projects.map(project => {2310 // slugify() called 100+ times for same project names2311 const slug = slugify(project.name)23122313 return <ProjectCard key={project.id} slug={slug} />2314 })}2315 </div>2316 )2317}2318```23192320**Correct: cached results**23212322```typescript2323// Module-level cache2324const slugifyCache = new Map<string, string>()23252326function cachedSlugify(text: string): string {2327 if (slugifyCache.has(text)) {2328 return slugifyCache.get(text)!2329 }2330 const result = slugify(text)2331 slugifyCache.set(text, result)2332 return result2333}23342335function ProjectList({ projects }: { projects: Project[] }) {2336 return (2337 <div>2338 {projects.map(project => {2339 // Computed only once per unique project name2340 const slug = cachedSlugify(project.name)23412342 return <ProjectCard key={project.id} slug={slug} />2343 })}2344 </div>2345 )2346}2347```23482349**Simpler pattern for single-value functions:**23502351```typescript2352let isLoggedInCache: boolean | null = null23532354function isLoggedIn(): boolean {2355 if (isLoggedInCache !== null) {2356 return isLoggedInCache2357 }23582359 isLoggedInCache = document.cookie.includes("auth=")2360 return isLoggedInCache2361}23622363// Clear cache when auth changes2364function onAuthChange() {2365 isLoggedInCache = null2366}2367```23682369Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.23702371Reference: [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)23722373### 7.5 Cache Storage API Calls23742375**Impact: LOW-MEDIUM (reduces expensive I/O)**23762377`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.23782379**Incorrect: reads storage on every call**23802381```typescript2382function getTheme() {2383 return localStorage.getItem("theme") ?? "light"2384}2385// Called 10 times = 10 storage reads2386```23872388**Correct: Map cache**23892390```typescript2391const storageCache = new Map<string, string | null>()23922393function getLocalStorage(key: string) {2394 if (!storageCache.has(key)) {2395 storageCache.set(key, localStorage.getItem(key))2396 }2397 return storageCache.get(key)2398}23992400function setLocalStorage(key: string, value: string) {2401 localStorage.setItem(key, value)2402 storageCache.set(key, value) // keep cache in sync2403}2404```24052406Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.24072408**Cookie caching:**24092410```typescript2411let cookieCache: Record<string, string> | null = null24122413function getCookie(name: string) {2414 if (!cookieCache) {2415 cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")))2416 }2417 return cookieCache[name]2418}2419```24202421**Important: invalidate on external changes**24222423```typescript2424window.addEventListener("storage", (e) => {2425 if (e.key) storageCache.delete(e.key)2426})24272428document.addEventListener("visibilitychange", () => {2429 if (document.visibilityState === "visible") {2430 storageCache.clear()2431 }2432})2433```24342435If storage can change externally (another tab, server-set cookies), invalidate cache:24362437### 7.6 Combine Multiple Array Iterations24382439**Impact: LOW-MEDIUM (reduces iterations)**24402441Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.24422443**Incorrect: 3 iterations**24442445```typescript2446const admins = users.filter((u) => u.isAdmin)2447const testers = users.filter((u) => u.isTester)2448const inactive = users.filter((u) => !u.isActive)2449```24502451**Correct: 1 iteration**24522453```typescript2454const admins: User[] = []2455const testers: User[] = []2456const inactive: User[] = []24572458for (const user of users) {2459 if (user.isAdmin) admins.push(user)2460 if (user.isTester) testers.push(user)2461 if (!user.isActive) inactive.push(user)2462}2463```24642465### 7.7 Early Length Check for Array Comparisons24662467**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**24682469When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.24702471In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).24722473**Incorrect: always runs expensive comparison**24742475```typescript2476function hasChanges(current: string[], original: string[]) {2477 // Always sorts and joins, even when lengths differ2478 return current.sort().join() !== original.sort().join()2479}2480```24812482Two 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.24832484**Correct (O(1) length check first):**24852486```typescript2487function hasChanges(current: string[], original: string[]) {2488 // Early return if lengths differ2489 if (current.length !== original.length) {2490 return true2491 }2492 // Only sort when lengths match2493 const currentSorted = current.toSorted()2494 const originalSorted = original.toSorted()2495 for (let i = 0; i < currentSorted.length; i++) {2496 if (currentSorted[i] !== originalSorted[i]) {2497 return true2498 }2499 }2500 return false2501}2502```25032504This new approach is more efficient because:25052506- It avoids the overhead of sorting and joining the arrays when lengths differ25072508- It avoids consuming memory for the joined strings (especially important for large arrays)25092510- It avoids mutating the original arrays25112512- It returns early when a difference is found25132514### 7.8 Early Return from Functions25152516**Impact: LOW-MEDIUM (avoids unnecessary computation)**25172518Return early when result is determined to skip unnecessary processing.25192520**Incorrect: processes all items even after finding answer**25212522```typescript2523function validateUsers(users: User[]) {2524 let hasError = false2525 let errorMessage = ""25262527 for (const user of users) {2528 if (!user.email) {2529 hasError = true2530 errorMessage = "Email required"2531 }2532 if (!user.name) {2533 hasError = true2534 errorMessage = "Name required"2535 }2536 // Continues checking all users even after error found2537 }25382539 return hasError ? { valid: false, error: errorMessage } : { valid: true }2540}2541```25422543**Correct: returns immediately on first error**25442545```typescript2546function validateUsers(users: User[]) {2547 for (const user of users) {2548 if (!user.email) {2549 return { valid: false, error: "Email required" }2550 }2551 if (!user.name) {2552 return { valid: false, error: "Name required" }2553 }2554 }25552556 return { valid: true }2557}2558```25592560### 7.9 Hoist RegExp Creation25612562**Impact: LOW-MEDIUM (avoids recreation)**25632564Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.25652566**Incorrect: new RegExp every render**25672568```tsx2569function Highlighter({ text, query }: Props) {2570 const regex = new RegExp(`(${query})`, 'gi')2571 const parts = text.split(regex)2572 return <>{parts.map((part, i) => ...)}</>2573}2574```25752576**Correct: memoize or hoist**25772578```tsx2579const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/25802581function Highlighter({ text, query }: Props) {2582 const regex = useMemo(2583 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),2584 [query]2585 )2586 const parts = text.split(regex)2587 return <>{parts.map((part, i) => ...)}</>2588}2589```25902591**Warning: global regex has mutable state**25922593```typescript2594const regex = /foo/g2595regex.test("foo") // true, lastIndex = 32596regex.test("foo") // false, lastIndex = 02597```25982599Global regex (`/g`) has mutable `lastIndex` state:26002601### 7.10 Use Loop for Min/Max Instead of Sort26022603**Impact: LOW (O(n) instead of O(n log n))**26042605Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.26062607**Incorrect (O(n log n) - sort to find latest):**26082609```typescript2610interface Project {2611 id: string2612 name: string2613 updatedAt: number2614}26152616function getLatestProject(projects: Project[]) {2617 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)2618 return sorted[0]2619}2620```26212622Sorts the entire array just to find the maximum value.26232624**Incorrect (O(n log n) - sort for oldest and newest):**26252626```typescript2627function getOldestAndNewest(projects: Project[]) {2628 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)2629 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }2630}2631```26322633Still sorts unnecessarily when only min/max are needed.26342635**Correct (O(n) - single loop):**26362637```typescript2638function getLatestProject(projects: Project[]) {2639 if (projects.length === 0) return null26402641 let latest = projects[0]26422643 for (let i = 1; i < projects.length; i++) {2644 if (projects[i].updatedAt > latest.updatedAt) {2645 latest = projects[i]2646 }2647 }26482649 return latest2650}26512652function getOldestAndNewest(projects: Project[]) {2653 if (projects.length === 0) return { oldest: null, newest: null }26542655 let oldest = projects[0]2656 let newest = projects[0]26572658 for (let i = 1; i < projects.length; i++) {2659 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]2660 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]2661 }26622663 return { oldest, newest }2664}2665```26662667Single pass through the array, no copying, no sorting.26682669**Alternative: Math.min/Math.max for small arrays**26702671```typescript2672const numbers = [5, 2, 8, 1, 9]2673const min = Math.min(...numbers)2674const max = Math.max(...numbers)2675```26762677This 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.26782679### 7.11 Use Set/Map for O(1) Lookups26802681**Impact: LOW-MEDIUM (O(n) to O(1))**26822683Convert arrays to Set/Map for repeated membership checks.26842685**Incorrect (O(n) per check):**26862687```typescript2688const allowedIds = ['a', 'b', 'c', ...]2689items.filter(item => allowedIds.includes(item.id))2690```26912692**Correct (O(1) per check):**26932694```typescript2695const allowedIds = new Set(['a', 'b', 'c', ...])2696items.filter(item => allowedIds.has(item.id))2697```26982699### 7.12 Use toSorted() Instead of sort() for Immutability27002701**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**27022703`.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.27042705**Incorrect: mutates original array**27062707```typescript2708function UserList({ users }: { users: User[] }) {2709 // Mutates the users prop array!2710 const sorted = useMemo(2711 () => users.sort((a, b) => a.name.localeCompare(b.name)),2712 [users]2713 )2714 return <div>{sorted.map(renderUser)}</div>2715}2716```27172718**Correct: creates new array**27192720```typescript2721function UserList({ users }: { users: User[] }) {2722 // Creates new sorted array, original unchanged2723 const sorted = useMemo(2724 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),2725 [users]2726 )2727 return <div>{sorted.map(renderUser)}</div>2728}2729```27302731**Why this matters in React:**273227331. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only273427352. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior27362737**Browser support: fallback for older browsers**27382739```typescript2740// Fallback for older browsers2741const sorted = [...items].sort((a, b) => a.value - b.value)2742```27432744`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:27452746**Other immutable array methods:**27472748- `.toSorted()` - immutable sort27492750- `.toReversed()` - immutable reverse27512752- `.toSpliced()` - immutable splice27532754- `.with()` - immutable element replacement27552756---27572758## 8. Advanced Patterns27592760**Impact: LOW**27612762Advanced patterns for specific cases that require careful implementation.27632764### 8.1 Initialize App Once, Not Per Mount27652766**Impact: LOW-MEDIUM (avoids duplicate init in development)**27672768Do 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.27692770**Incorrect: runs twice in dev, re-runs on remount**27712772```tsx2773function Comp() {2774 useEffect(() => {2775 loadFromStorage()2776 checkAuthToken()2777 }, [])27782779 // ...2780}2781```27822783**Correct: once per app load**27842785```tsx2786let didInit = false27872788function Comp() {2789 useEffect(() => {2790 if (didInit) return2791 didInit = true2792 loadFromStorage()2793 checkAuthToken()2794 }, [])27952796 // ...2797}2798```27992800Reference: [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)28012802### 8.2 Store Event Handlers in Refs28032804**Impact: LOW (stable subscriptions)**28052806Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.28072808**Incorrect: re-subscribes on every render**28092810```tsx2811function useWindowEvent(event: string, handler: (e) => void) {2812 useEffect(() => {2813 window.addEventListener(event, handler)2814 return () => window.removeEventListener(event, handler)2815 }, [event, handler])2816}2817```28182819**Correct: stable subscription**28202821```tsx2822import { useEffectEvent } from "react"28232824function useWindowEvent(event: string, handler: (e) => void) {2825 const onEvent = useEffectEvent(handler)28262827 useEffect(() => {2828 window.addEventListener(event, onEvent)2829 return () => window.removeEventListener(event, onEvent)2830 }, [event])2831}2832```28332834**Alternative: use `useEffectEvent` if you're on latest React:**28352836`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.28372838### 8.3 useEffectEvent for Stable Callback Refs28392840**Impact: LOW (prevents effect re-runs)**28412842Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.28432844**Incorrect: effect re-runs on every callback change**28452846```tsx2847function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2848 const [query, setQuery] = useState("")28492850 useEffect(() => {2851 const timeout = setTimeout(() => onSearch(query), 300)2852 return () => clearTimeout(timeout)2853 }, [query, onSearch])2854}2855```28562857**Correct: using React's useEffectEvent**28582859```tsx2860import { useEffectEvent } from "react"28612862function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2863 const [query, setQuery] = useState("")2864 const onSearchEvent = useEffectEvent(onSearch)28652866 useEffect(() => {2867 const timeout = setTimeout(() => onSearchEvent(query), 300)2868 return () => clearTimeout(timeout)2869 }, [query])2870}2871```28722873---28742875## References287628771. [https://react.dev](https://react.dev)28782. [https://nextjs.org](https://nextjs.org)28793. [https://swr.vercel.app](https://swr.vercel.app)28804. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)28815. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)28826. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)28837. [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)2884
Also in zebbern/claude-code-guide
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 |
|---|---|---|---|---|---|
| zebbern/claude-code-guideskills/composition-patterns/AGENTS.md · 4.5k | AGENTS.md | styleapiuido-not | 57/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| vllm-project/vllmAGENTS.md · 88k | AGENTS.md | setuptestlint-formatstyle+5 | 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 | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| OnlyTerp/prompt-cache-skillsAGENTS.md · 112 | AGENTS.md | setupbuildtestlint-format+5 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago |
