AGENTS.md
skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
Quality
64/100
Scores the file, not the repository.Length
9,864 words
69 headings · 136 code blocksRepository
196
— · 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([183 fetchUser(),184 fetchConfig()185])186const profile = await fetchProfile(user.id)187```188189**Correct: config and profile run in parallel**190191```typescript192import { all } from 'better-all'193194const { user, config, profile } = await all({195 async user() { return fetchUser() },196 async config() { return fetchConfig() },197 async profile() {198 return fetchProfile((await this.$.user).id)199 }200})201```202203**Alternative without extra dependencies:**204205```typescript206const userPromise = fetchUser()207const profilePromise = userPromise.then(user => fetchProfile(user.id))208209const [user, config, profile] = await Promise.all([210 userPromise,211 fetchConfig(),212 profilePromise213])214```215216We can also create all the promises first, and do `Promise.all()` at the end.217218Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)219220### 1.3 Prevent Waterfall Chains in API Routes221222**Impact: CRITICAL (2-10× improvement)**223224In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.225226**Incorrect: config waits for auth, data waits for both**227228```typescript229export async function GET(request: Request) {230 const session = await auth()231 const config = await fetchConfig()232 const data = await fetchData(session.user.id)233 return Response.json({ data, config })234}235```236237**Correct: auth and config start immediately**238239```typescript240export async function GET(request: Request) {241 const sessionPromise = auth()242 const configPromise = fetchConfig()243 const session = await sessionPromise244 const [config, data] = await Promise.all([245 configPromise,246 fetchData(session.user.id)247 ])248 return Response.json({ data, config })249}250```251252For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).253254### 1.4 Promise.all() for Independent Operations255256**Impact: CRITICAL (2-10× improvement)**257258When async operations have no interdependencies, execute them concurrently using `Promise.all()`.259260**Incorrect: sequential execution, 3 round trips**261262```typescript263const user = await fetchUser()264const posts = await fetchPosts()265const comments = await fetchComments()266```267268**Correct: parallel execution, 1 round trip**269270```typescript271const [user, posts, comments] = await Promise.all([272 fetchUser(),273 fetchPosts(),274 fetchComments()275])276```277278### 1.5 Strategic Suspense Boundaries279280**Impact: HIGH (faster initial paint)**281282Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.283284**Incorrect: wrapper blocked by data fetching**285286```tsx287async function Page() {288 const data = await fetchData() // Blocks entire page289290 return (291 <div>292 <div>Sidebar</div>293 <div>Header</div>294 <div>295 <DataDisplay data={data} />296 </div>297 <div>Footer</div>298 </div>299 )300}301```302303The entire layout waits for data even though only the middle section needs it.304305**Correct: wrapper shows immediately, data streams in**306307```tsx308function Page() {309 return (310 <div>311 <div>Sidebar</div>312 <div>Header</div>313 <div>314 <Suspense fallback={<Skeleton />}>315 <DataDisplay />316 </Suspense>317 </div>318 <div>Footer</div>319 </div>320 )321}322323async function DataDisplay() {324 const data = await fetchData() // Only blocks this component325 return <div>{data.content}</div>326}327```328329Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.330331**Alternative: share promise across components**332333```tsx334function Page() {335 // Start fetch immediately, but don't await336 const dataPromise = fetchData()337338 return (339 <div>340 <div>Sidebar</div>341 <div>Header</div>342 <Suspense fallback={<Skeleton />}>343 <DataDisplay dataPromise={dataPromise} />344 <DataSummary dataPromise={dataPromise} />345 </Suspense>346 <div>Footer</div>347 </div>348 )349}350351function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {352 const data = use(dataPromise) // Unwraps the promise353 return <div>{data.content}</div>354}355356function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {357 const data = use(dataPromise) // Reuses the same promise358 return <div>{data.summary}</div>359}360```361362Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.363364**When NOT to use this pattern:**365366- Critical data needed for layout decisions (affects positioning)367368- SEO-critical content above the fold369370- Small, fast queries where suspense overhead isn't worth it371372- When you want to avoid layout shift (loading → content jump)373374**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.375376---377378## 2. Bundle Size Optimization379380**Impact: CRITICAL**381382Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.383384### 2.1 Avoid Barrel File Imports385386**Impact: CRITICAL (200-800ms import cost, slow builds)**387388Import 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'`).389390Popular 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.391392**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.393394**Incorrect: imports entire library**395396```tsx397import { Check, X, Menu } from 'lucide-react'398// Loads 1,583 modules, takes ~2.8s extra in dev399// Runtime cost: 200-800ms on every cold start400401import { Button, TextField } from '@mui/material'402// Loads 2,225 modules, takes ~4.2s extra in dev403```404405**Correct: imports only what you need**406407```tsx408import Check from 'lucide-react/dist/esm/icons/check'409import X from 'lucide-react/dist/esm/icons/x'410import Menu from 'lucide-react/dist/esm/icons/menu'411// Loads only 3 modules (~2KB vs ~1MB)412413import Button from '@mui/material/Button'414import TextField from '@mui/material/TextField'415// Loads only what you use416```417418**Alternative: Next.js 13.5+**419420```js421// next.config.js - use optimizePackageImports422module.exports = {423 experimental: {424 optimizePackageImports: ['lucide-react', '@mui/material']425 }426}427428// Then you can keep the ergonomic barrel imports:429import { Check, X, Menu } from 'lucide-react'430// Automatically transformed to direct imports at build time431```432433Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.434435Libraries 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`.436437Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)438439### 2.2 Conditional Module Loading440441**Impact: HIGH (loads large data only when needed)**442443Load large data or modules only when a feature is activated.444445**Example: lazy-load animation frames**446447```tsx448function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {449 const [frames, setFrames] = useState<Frame[] | null>(null)450451 useEffect(() => {452 if (enabled && !frames && typeof window !== 'undefined') {453 import('./animation-frames.js')454 .then(mod => setFrames(mod.frames))455 .catch(() => setEnabled(false))456 }457 }, [enabled, frames, setEnabled])458459 if (!frames) return <Skeleton />460 return <Canvas frames={frames} />461}462```463464The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.465466### 2.3 Defer Non-Critical Third-Party Libraries467468**Impact: MEDIUM (loads after hydration)**469470Analytics, logging, and error tracking don't block user interaction. Load them after hydration.471472**Incorrect: blocks initial bundle**473474```tsx475import { Analytics } from '@vercel/analytics/react'476477export default function RootLayout({ children }) {478 return (479 <html>480 <body>481 {children}482 <Analytics />483 </body>484 </html>485 )486}487```488489**Correct: loads after hydration**490491```tsx492import dynamic from 'next/dynamic'493494const Analytics = dynamic(495 () => import('@vercel/analytics/react').then(m => m.Analytics),496 { ssr: false }497)498499export default function RootLayout({ children }) {500 return (501 <html>502 <body>503 {children}504 <Analytics />505 </body>506 </html>507 )508}509```510511### 2.4 Dynamic Imports for Heavy Components512513**Impact: CRITICAL (directly affects TTI and LCP)**514515Use `next/dynamic` to lazy-load large components not needed on initial render.516517**Incorrect: Monaco bundles with main chunk ~300KB**518519```tsx520import { MonacoEditor } from './monaco-editor'521522function CodePanel({ code }: { code: string }) {523 return <MonacoEditor value={code} />524}525```526527**Correct: Monaco loads on demand**528529```tsx530import dynamic from 'next/dynamic'531532const MonacoEditor = dynamic(533 () => import('./monaco-editor').then(m => m.MonacoEditor),534 { ssr: false }535)536537function CodePanel({ code }: { code: string }) {538 return <MonacoEditor value={code} />539}540```541542### 2.5 Preload Based on User Intent543544**Impact: MEDIUM (reduces perceived latency)**545546Preload heavy bundles before they're needed to reduce perceived latency.547548**Example: preload on hover/focus**549550```tsx551function EditorButton({ onClick }: { onClick: () => void }) {552 const preload = () => {553 if (typeof window !== 'undefined') {554 void import('./monaco-editor')555 }556 }557558 return (559 <button560 onMouseEnter={preload}561 onFocus={preload}562 onClick={onClick}563 >564 Open Editor565 </button>566 )567}568```569570**Example: preload when feature flag is enabled**571572```tsx573function FlagsProvider({ children, flags }: Props) {574 useEffect(() => {575 if (flags.editorEnabled && typeof window !== 'undefined') {576 void import('./monaco-editor').then(mod => mod.init())577 }578 }, [flags.editorEnabled])579580 return <FlagsContext.Provider value={flags}>581 {children}582 </FlagsContext.Provider>583}584```585586The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.587588---589590## 3. Server-Side Performance591592**Impact: HIGH**593594Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.595596### 3.1 Authenticate Server Actions Like API Routes597598**Impact: CRITICAL (prevents unauthorized access to server mutations)**599600Server 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.601602Next.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."603604**Incorrect: no authentication check**605606```typescript607'use server'608609export async function deleteUser(userId: string) {610 // Anyone can call this! No auth check611 await db.user.delete({ where: { id: userId } })612 return { success: true }613}614```615616**Correct: authentication inside the action**617618```typescript619'use server'620621import { verifySession } from '@/lib/auth'622import { unauthorized } from '@/lib/errors'623624export async function deleteUser(userId: string) {625 // Always check auth inside the action626 const session = await verifySession()627628 if (!session) {629 throw unauthorized('Must be logged in')630 }631632 // Check authorization too633 if (session.user.role !== 'admin' && session.user.id !== userId) {634 throw unauthorized('Cannot delete other users')635 }636637 await db.user.delete({ where: { id: userId } })638 return { success: true }639}640```641642**With input validation:**643644```typescript645'use server'646647import { verifySession } from '@/lib/auth'648import { z } from 'zod'649650const updateProfileSchema = z.object({651 userId: z.string().uuid(),652 name: z.string().min(1).max(100),653 email: z.string().email()654})655656export async function updateProfile(data: unknown) {657 // Validate input first658 const validated = updateProfileSchema.parse(data)659660 // Then authenticate661 const session = await verifySession()662 if (!session) {663 throw new Error('Unauthorized')664 }665666 // Then authorize667 if (session.user.id !== validated.userId) {668 throw new Error('Can only update own profile')669 }670671 // Finally perform the mutation672 await db.user.update({673 where: { id: validated.userId },674 data: {675 name: validated.name,676 email: validated.email677 }678 })679680 return { success: true }681}682```683684Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)685686### 3.2 Avoid Duplicate Serialization in RSC Props687688**Impact: LOW (reduces network payload by avoiding duplicate serialization)**689690RSC→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.691692**Incorrect: duplicates array**693694```tsx695// RSC: sends 6 strings (2 arrays × 3 items)696<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />697```698699**Correct: sends 3 strings**700701```tsx702// RSC: send once703<ClientList usernames={usernames} />704705// Client: transform there706'use client'707const sorted = useMemo(() => [...usernames].sort(), [usernames])708```709710**Nested deduplication behavior:**711712```tsx713// string[] - duplicates everything714usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings715716// object[] - duplicates array structure only717users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)718```719720Deduplication works recursively. Impact varies by data type:721722- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated723724- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference725726**Operations breaking deduplication: create new references**727728- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`729730- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`731732**More examples:**733734```tsx735// ❌ Bad736<C users={users} active={users.filter(u => u.active)} />737<C product={product} productName={product.name} />738739// ✅ Good740<C users={users} />741<C product={product} />742// Do filtering/destructuring in client743```744745**Exception:** Pass derived data when transformation is expensive or client doesn't need original.746747### 3.3 Cross-Request LRU Caching748749**Impact: HIGH (caches across requests)**750751`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.752753**Implementation:**754755```typescript756import { LRUCache } from 'lru-cache'757758const cache = new LRUCache<string, any>({759 max: 1000,760 ttl: 5 * 60 * 1000 // 5 minutes761})762763export async function getUser(id: string) {764 const cached = cache.get(id)765 if (cached) return cached766767 const user = await db.user.findUnique({ where: { id } })768 cache.set(id, user)769 return user770}771772// Request 1: DB query, result cached773// Request 2: cache hit, no DB query774```775776Use when sequential user actions hit multiple endpoints needing the same data within seconds.777778**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.779780**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.781782Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)783784### 3.4 Minimize Serialization at RSC Boundaries785786**Impact: HIGH (reduces data transfer size)**787788The 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.789790**Incorrect: serializes all 50 fields**791792```tsx793async function Page() {794 const user = await fetchUser() // 50 fields795 return <Profile user={user} />796}797798'use client'799function Profile({ user }: { user: User }) {800 return <div>{user.name}</div> // uses 1 field801}802```803804**Correct: serializes only 1 field**805806```tsx807async function Page() {808 const user = await fetchUser()809 return <Profile name={user.name} />810}811812'use client'813function Profile({ name }: { name: string }) {814 return <div>{name}</div>815}816```817818### 3.5 Parallel Data Fetching with Component Composition819820**Impact: CRITICAL (eliminates server-side waterfalls)**821822React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.823824**Incorrect: Sidebar waits for Page's fetch to complete**825826```tsx827export default async function Page() {828 const header = await fetchHeader()829 return (830 <div>831 <div>{header}</div>832 <Sidebar />833 </div>834 )835}836837async function Sidebar() {838 const items = await fetchSidebarItems()839 return <nav>{items.map(renderItem)}</nav>840}841```842843**Correct: both fetch simultaneously**844845```tsx846async function Header() {847 const data = await fetchHeader()848 return <div>{data}</div>849}850851async function Sidebar() {852 const items = await fetchSidebarItems()853 return <nav>{items.map(renderItem)}</nav>854}855856export default function Page() {857 return (858 <div>859 <Header />860 <Sidebar />861 </div>862 )863}864```865866**Alternative with children prop:**867868```tsx869async function Header() {870 const data = await fetchHeader()871 return <div>{data}</div>872}873874async function Sidebar() {875 const items = await fetchSidebarItems()876 return <nav>{items.map(renderItem)}</nav>877}878879function Layout({ children }: { children: ReactNode }) {880 return (881 <div>882 <Header />883 {children}884 </div>885 )886}887888export default function Page() {889 return (890 <Layout>891 <Sidebar />892 </Layout>893 )894}895```896897### 3.6 Per-Request Deduplication with React.cache()898899**Impact: MEDIUM (deduplicates within request)**900901Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.902903**Usage:**904905```typescript906import { cache } from 'react'907908export const getCurrentUser = cache(async () => {909 const session = await auth()910 if (!session?.user?.id) return null911 return await db.user.findUnique({912 where: { id: session.user.id }913 })914})915```916917Within a single request, multiple calls to `getCurrentUser()` execute the query only once.918919**Avoid inline objects as arguments:**920921`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.922923**Incorrect: always cache miss**924925```typescript926const getUser = cache(async (params: { uid: number }) => {927 return await db.user.findUnique({ where: { id: params.uid } })928})929930// Each call creates new object, never hits cache931getUser({ uid: 1 })932getUser({ uid: 1 }) // Cache miss, runs query again933```934935**Correct: cache hit**936937```typescript938const params = { uid: 1 }939getUser(params) // Query runs940getUser(params) // Cache hit (same reference)941```942943If you must pass objects, pass the same reference:944945**Next.js-Specific Note:**946947In 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:948949- Database queries (Prisma, Drizzle, etc.)950951- Heavy computations952953- Authentication checks954955- File system operations956957- Any non-fetch async work958959Use `React.cache()` to deduplicate these operations across your component tree.960961Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)962963### 3.7 Use after() for Non-Blocking Operations964965**Impact: MEDIUM (faster response times)**966967Use 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.968969**Incorrect: blocks response**970971```tsx972import { logUserAction } from '@/app/utils'973974export async function POST(request: Request) {975 // Perform mutation976 await updateDatabase(request)977978 // Logging blocks the response979 const userAgent = request.headers.get('user-agent') || 'unknown'980 await logUserAction({ userAgent })981982 return new Response(JSON.stringify({ status: 'success' }), {983 status: 200,984 headers: { 'Content-Type': 'application/json' }985 })986}987```988989**Correct: non-blocking**990991```tsx992import { after } from 'next/server'993import { headers, cookies } from 'next/headers'994import { logUserAction } from '@/app/utils'995996export async function POST(request: Request) {997 // Perform mutation998 await updateDatabase(request)9991000 // Log after response is sent1001 after(async () => {1002 const userAgent = (await headers()).get('user-agent') || 'unknown'1003 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'10041005 logUserAction({ sessionCookie, userAgent })1006 })10071008 return new Response(JSON.stringify({ status: 'success' }), {1009 status: 200,1010 headers: { 'Content-Type': 'application/json' }1011 })1012}1013```10141015The response is sent immediately while logging happens in the background.10161017**Common use cases:**10181019- Analytics tracking10201021- Audit logging10221023- Sending notifications10241025- Cache invalidation10261027- Cleanup tasks10281029**Important notes:**10301031- `after()` runs even if the response fails or redirects10321033- Works in Server Actions, Route Handlers, and Server Components10341035Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)10361037---10381039## 4. Client-Side Data Fetching10401041**Impact: MEDIUM-HIGH**10421043Automatic deduplication and efficient data fetching patterns reduce redundant network requests.10441045### 4.1 Deduplicate Global Event Listeners10461047**Impact: LOW (single listener for N components)**10481049Use `useSWRSubscription()` to share global event listeners across component instances.10501051**Incorrect: N instances = N listeners**10521053```tsx1054function useKeyboardShortcut(key: string, callback: () => void) {1055 useEffect(() => {1056 const handler = (e: KeyboardEvent) => {1057 if (e.metaKey && e.key === key) {1058 callback()1059 }1060 }1061 window.addEventListener('keydown', handler)1062 return () => window.removeEventListener('keydown', handler)1063 }, [key, callback])1064}1065```10661067When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.10681069**Correct: N instances = 1 listener**10701071```tsx1072import useSWRSubscription from 'swr/subscription'10731074// Module-level Map to track callbacks per key1075const keyCallbacks = new Map<string, Set<() => void>>()10761077function useKeyboardShortcut(key: string, callback: () => void) {1078 // Register this callback in the Map1079 useEffect(() => {1080 if (!keyCallbacks.has(key)) {1081 keyCallbacks.set(key, new Set())1082 }1083 keyCallbacks.get(key)!.add(callback)10841085 return () => {1086 const set = keyCallbacks.get(key)1087 if (set) {1088 set.delete(callback)1089 if (set.size === 0) {1090 keyCallbacks.delete(key)1091 }1092 }1093 }1094 }, [key, callback])10951096 useSWRSubscription('global-keydown', () => {1097 const handler = (e: KeyboardEvent) => {1098 if (e.metaKey && keyCallbacks.has(e.key)) {1099 keyCallbacks.get(e.key)!.forEach(cb => cb())1100 }1101 }1102 window.addEventListener('keydown', handler)1103 return () => window.removeEventListener('keydown', handler)1104 })1105}11061107function Profile() {1108 // Multiple shortcuts will share the same listener1109 useKeyboardShortcut('p', () => { /* ... */ })1110 useKeyboardShortcut('k', () => { /* ... */ })1111 // ...1112}1113```11141115### 4.2 Use Passive Event Listeners for Scrolling Performance11161117**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**11181119Add `{ 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.11201121**Incorrect:**11221123```typescript1124useEffect(() => {1125 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1126 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11271128 document.addEventListener('touchstart', handleTouch)1129 document.addEventListener('wheel', handleWheel)11301131 return () => {1132 document.removeEventListener('touchstart', handleTouch)1133 document.removeEventListener('wheel', handleWheel)1134 }1135}, [])1136```11371138**Correct:**11391140```typescript1141useEffect(() => {1142 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)1143 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)11441145 document.addEventListener('touchstart', handleTouch, { passive: true })1146 document.addEventListener('wheel', handleWheel, { passive: true })11471148 return () => {1149 document.removeEventListener('touchstart', handleTouch)1150 document.removeEventListener('wheel', handleWheel)1151 }1152}, [])1153```11541155**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.11561157**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.11581159### 4.3 Use SWR for Automatic Deduplication11601161**Impact: MEDIUM-HIGH (automatic deduplication)**11621163SWR enables request deduplication, caching, and revalidation across component instances.11641165**Incorrect: no deduplication, each instance fetches**11661167```tsx1168function UserList() {1169 const [users, setUsers] = useState([])1170 useEffect(() => {1171 fetch('/api/users')1172 .then(r => r.json())1173 .then(setUsers)1174 }, [])1175}1176```11771178**Correct: multiple instances share one request**11791180```tsx1181import useSWR from 'swr'11821183function UserList() {1184 const { data: users } = useSWR('/api/users', fetcher)1185}1186```11871188**For immutable data:**11891190```tsx1191import { useImmutableSWR } from '@/lib/swr'11921193function StaticContent() {1194 const { data } = useImmutableSWR('/api/config', fetcher)1195}1196```11971198**For mutations:**11991200```tsx1201import { useSWRMutation } from 'swr/mutation'12021203function UpdateButton() {1204 const { trigger } = useSWRMutation('/api/user', updateUser)1205 return <button onClick={() => trigger()}>Update</button>1206}1207```12081209Reference: [https://swr.vercel.app](https://swr.vercel.app)12101211### 4.4 Version and Minimize localStorage Data12121213**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**12141215Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.12161217**Incorrect:**12181219```typescript1220// No version, stores everything, no error handling1221localStorage.setItem('userConfig', JSON.stringify(fullUserObject))1222const data = localStorage.getItem('userConfig')1223```12241225**Correct:**12261227```typescript1228const VERSION = 'v2'12291230function saveConfig(config: { theme: string; language: string }) {1231 try {1232 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))1233 } catch {1234 // Throws in incognito/private browsing, quota exceeded, or disabled1235 }1236}12371238function loadConfig() {1239 try {1240 const data = localStorage.getItem(`userConfig:${VERSION}`)1241 return data ? JSON.parse(data) : null1242 } catch {1243 return null1244 }1245}12461247// Migration from v1 to v21248function migrate() {1249 try {1250 const v1 = localStorage.getItem('userConfig:v1')1251 if (v1) {1252 const old = JSON.parse(v1)1253 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })1254 localStorage.removeItem('userConfig:v1')1255 }1256 } catch {}1257}1258```12591260**Store minimal fields from server responses:**12611262```typescript1263// User object has 20+ fields, only store what UI needs1264function cachePrefs(user: FullUser) {1265 try {1266 localStorage.setItem('prefs:v1', JSON.stringify({1267 theme: user.preferences.theme,1268 notifications: user.preferences.notifications1269 }))1270 } catch {}1271}1272```12731274**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.12751276**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.12771278---12791280## 5. Re-render Optimization12811282**Impact: MEDIUM**12831284Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.12851286### 5.1 Calculate Derived State During Rendering12871288**Impact: MEDIUM (avoids redundant renders and state drift)**12891290If 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.12911292**Incorrect: redundant state and effect**12931294```tsx1295function Form() {1296 const [firstName, setFirstName] = useState('First')1297 const [lastName, setLastName] = useState('Last')1298 const [fullName, setFullName] = useState('')12991300 useEffect(() => {1301 setFullName(firstName + ' ' + lastName)1302 }, [firstName, lastName])13031304 return <p>{fullName}</p>1305}1306```13071308**Correct: derive during render**13091310```tsx1311function Form() {1312 const [firstName, setFirstName] = useState('First')1313 const [lastName, setLastName] = useState('Last')1314 const fullName = firstName + ' ' + lastName13151316 return <p>{fullName}</p>1317}1318```13191320Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)13211322### 5.2 Defer State Reads to Usage Point13231324**Impact: MEDIUM (avoids unnecessary subscriptions)**13251326Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.13271328**Incorrect: subscribes to all searchParams changes**13291330```tsx1331function ShareButton({ chatId }: { chatId: string }) {1332 const searchParams = useSearchParams()13331334 const handleShare = () => {1335 const ref = searchParams.get('ref')1336 shareChat(chatId, { ref })1337 }13381339 return <button onClick={handleShare}>Share</button>1340}1341```13421343**Correct: reads on demand, no subscription**13441345```tsx1346function ShareButton({ chatId }: { chatId: string }) {1347 const handleShare = () => {1348 const params = new URLSearchParams(window.location.search)1349 const ref = params.get('ref')1350 shareChat(chatId, { ref })1351 }13521353 return <button onClick={handleShare}>Share</button>1354}1355```13561357### 5.3 Do not wrap a simple expression with a primitive result type in useMemo13581359**Impact: LOW-MEDIUM (wasted computation on every render)**13601361When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.13621363Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.13641365**Incorrect:**13661367```tsx1368function Header({ user, notifications }: Props) {1369 const isLoading = useMemo(() => {1370 return user.isLoading || notifications.isLoading1371 }, [user.isLoading, notifications.isLoading])13721373 if (isLoading) return <Skeleton />1374 // return some markup1375}1376```13771378**Correct:**13791380```tsx1381function Header({ user, notifications }: Props) {1382 const isLoading = user.isLoading || notifications.isLoading13831384 if (isLoading) return <Skeleton />1385 // return some markup1386}1387```13881389### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant13901391**Impact: MEDIUM (restores memoization by using a constant for default value)**13921393When 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()`.13941395To address this issue, extract the default value into a constant.13961397**Incorrect: `onClick` has different values on every rerender**13981399```tsx1400const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {1401 // ...1402})14031404// Used without optional onClick1405<UserAvatar />1406```14071408**Correct: stable default value**14091410```tsx1411const NOOP = () => {};14121413const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {1414 // ...1415})14161417// Used without optional onClick1418<UserAvatar />1419```14201421### 5.5 Extract to Memoized Components14221423**Impact: MEDIUM (enables early returns)**14241425Extract expensive work into memoized components to enable early returns before computation.14261427**Incorrect: computes avatar even when loading**14281429```tsx1430function Profile({ user, loading }: Props) {1431 const avatar = useMemo(() => {1432 const id = computeAvatarId(user)1433 return <Avatar id={id} />1434 }, [user])14351436 if (loading) return <Skeleton />1437 return <div>{avatar}</div>1438}1439```14401441**Correct: skips computation when loading**14421443```tsx1444const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1445 const id = useMemo(() => computeAvatarId(user), [user])1446 return <Avatar id={id} />1447})14481449function Profile({ user, loading }: Props) {1450 if (loading) return <Skeleton />1451 return (1452 <div>1453 <UserAvatar user={user} />1454 </div>1455 )1456}1457```14581459**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.14601461### 5.6 Narrow Effect Dependencies14621463**Impact: LOW (minimizes effect re-runs)**14641465Specify primitive dependencies instead of objects to minimize effect re-runs.14661467**Incorrect: re-runs on any user field change**14681469```tsx1470useEffect(() => {1471 console.log(user.id)1472}, [user])1473```14741475**Correct: re-runs only when id changes**14761477```tsx1478useEffect(() => {1479 console.log(user.id)1480}, [user.id])1481```14821483**For derived state, compute outside effect:**14841485```tsx1486// Incorrect: runs on width=767, 766, 765...1487useEffect(() => {1488 if (width < 768) {1489 enableMobileMode()1490 }1491}, [width])14921493// Correct: runs only on boolean transition1494const isMobile = width < 7681495useEffect(() => {1496 if (isMobile) {1497 enableMobileMode()1498 }1499}, [isMobile])1500```15011502### 5.7 Put Interaction Logic in Event Handlers15031504**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**15051506If 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.15071508**Incorrect: event modeled as state + effect**15091510```tsx1511function Form() {1512 const [submitted, setSubmitted] = useState(false)1513 const theme = useContext(ThemeContext)15141515 useEffect(() => {1516 if (submitted) {1517 post('/api/register')1518 showToast('Registered', theme)1519 }1520 }, [submitted, theme])15211522 return <button onClick={() => setSubmitted(true)}>Submit</button>1523}1524```15251526**Correct: do it in the handler**15271528```tsx1529function Form() {1530 const theme = useContext(ThemeContext)15311532 function handleSubmit() {1533 post('/api/register')1534 showToast('Registered', theme)1535 }15361537 return <button onClick={handleSubmit}>Submit</button>1538}1539```15401541Reference: [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)15421543### 5.8 Subscribe to Derived State15441545**Impact: MEDIUM (reduces re-render frequency)**15461547Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.15481549**Incorrect: re-renders on every pixel change**15501551```tsx1552function Sidebar() {1553 const width = useWindowWidth() // updates continuously1554 const isMobile = width < 7681555 return <nav className={isMobile ? 'mobile' : 'desktop'} />1556}1557```15581559**Correct: re-renders only when boolean changes**15601561```tsx1562function Sidebar() {1563 const isMobile = useMediaQuery('(max-width: 767px)')1564 return <nav className={isMobile ? 'mobile' : 'desktop'} />1565}1566```15671568### 5.9 Use Functional setState Updates15691570**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**15711572When 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.15731574**Incorrect: requires state as dependency**15751576```tsx1577function TodoList() {1578 const [items, setItems] = useState(initialItems)15791580 // Callback must depend on items, recreated on every items change1581 const addItems = useCallback((newItems: Item[]) => {1582 setItems([...items, ...newItems])1583 }, [items]) // ❌ items dependency causes recreations15841585 // Risk of stale closure if dependency is forgotten1586 const removeItem = useCallback((id: string) => {1587 setItems(items.filter(item => item.id !== id))1588 }, []) // ❌ Missing items dependency - will use stale items!15891590 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1591}1592```15931594The 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.15951596**Correct: stable callbacks, no stale closures**15971598```tsx1599function TodoList() {1600 const [items, setItems] = useState(initialItems)16011602 // Stable callback, never recreated1603 const addItems = useCallback((newItems: Item[]) => {1604 setItems(curr => [...curr, ...newItems])1605 }, []) // ✅ No dependencies needed16061607 // Always uses latest state, no stale closure risk1608 const removeItem = useCallback((id: string) => {1609 setItems(curr => curr.filter(item => item.id !== id))1610 }, []) // ✅ Safe and stable16111612 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1613}1614```16151616**Benefits:**161716181. **Stable callback references** - Callbacks don't need to be recreated when state changes161916202. **No stale closures** - Always operates on the latest state value162116223. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks162316244. **Prevents bugs** - Eliminates the most common source of React closure bugs16251626**When to use functional updates:**16271628- Any setState that depends on the current state value16291630- Inside useCallback/useMemo when state is needed16311632- Event handlers that reference state16331634- Async operations that update state16351636**When direct updates are fine:**16371638- Setting state to a static value: `setCount(0)`16391640- Setting state from props/arguments only: `setName(newName)`16411642- State doesn't depend on previous value16431644**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.16451646### 5.10 Use Lazy State Initialization16471648**Impact: MEDIUM (wasted computation on every render)**16491650Pass 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.16511652**Incorrect: runs on every render**16531654```tsx1655function FilteredList({ items }: { items: Item[] }) {1656 // buildSearchIndex() runs on EVERY render, even after initialization1657 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))1658 const [query, setQuery] = useState('')16591660 // When query changes, buildSearchIndex runs again unnecessarily1661 return <SearchResults index={searchIndex} query={query} />1662}16631664function UserProfile() {1665 // JSON.parse runs on every render1666 const [settings, setSettings] = useState(1667 JSON.parse(localStorage.getItem('settings') || '{}')1668 )16691670 return <SettingsForm settings={settings} onChange={setSettings} />1671}1672```16731674**Correct: runs only once**16751676```tsx1677function FilteredList({ items }: { items: Item[] }) {1678 // buildSearchIndex() runs ONLY on initial render1679 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))1680 const [query, setQuery] = useState('')16811682 return <SearchResults index={searchIndex} query={query} />1683}16841685function UserProfile() {1686 // JSON.parse runs only on initial render1687 const [settings, setSettings] = useState(() => {1688 const stored = localStorage.getItem('settings')1689 return stored ? JSON.parse(stored) : {}1690 })16911692 return <SettingsForm settings={settings} onChange={setSettings} />1693}1694```16951696Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.16971698For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.16991700### 5.11 Use Transitions for Non-Urgent Updates17011702**Impact: MEDIUM (maintains UI responsiveness)**17031704Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.17051706**Incorrect: blocks UI on every scroll**17071708```tsx1709function ScrollTracker() {1710 const [scrollY, setScrollY] = useState(0)1711 useEffect(() => {1712 const handler = () => setScrollY(window.scrollY)1713 window.addEventListener('scroll', handler, { passive: true })1714 return () => window.removeEventListener('scroll', handler)1715 }, [])1716}1717```17181719**Correct: non-blocking updates**17201721```tsx1722import { startTransition } from 'react'17231724function ScrollTracker() {1725 const [scrollY, setScrollY] = useState(0)1726 useEffect(() => {1727 const handler = () => {1728 startTransition(() => setScrollY(window.scrollY))1729 }1730 window.addEventListener('scroll', handler, { passive: true })1731 return () => window.removeEventListener('scroll', handler)1732 }, [])1733}1734```17351736### 5.12 Use useRef for Transient Values17371738**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**17391740When 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.17411742**Incorrect: renders every update**17431744```tsx1745function Tracker() {1746 const [lastX, setLastX] = useState(0)17471748 useEffect(() => {1749 const onMove = (e: MouseEvent) => setLastX(e.clientX)1750 window.addEventListener('mousemove', onMove)1751 return () => window.removeEventListener('mousemove', onMove)1752 }, [])17531754 return (1755 <div1756 style={{1757 position: 'fixed',1758 top: 0,1759 left: lastX,1760 width: 8,1761 height: 8,1762 background: 'black',1763 }}1764 />1765 )1766}1767```17681769**Correct: no re-render for tracking**17701771```tsx1772function Tracker() {1773 const lastXRef = useRef(0)1774 const dotRef = useRef<HTMLDivElement>(null)17751776 useEffect(() => {1777 const onMove = (e: MouseEvent) => {1778 lastXRef.current = e.clientX1779 const node = dotRef.current1780 if (node) {1781 node.style.transform = `translateX(${e.clientX}px)`1782 }1783 }1784 window.addEventListener('mousemove', onMove)1785 return () => window.removeEventListener('mousemove', onMove)1786 }, [])17871788 return (1789 <div1790 ref={dotRef}1791 style={{1792 position: 'fixed',1793 top: 0,1794 left: 0,1795 width: 8,1796 height: 8,1797 background: 'black',1798 transform: 'translateX(0px)',1799 }}1800 />1801 )1802}1803```18041805---18061807## 6. Rendering Performance18081809**Impact: MEDIUM**18101811Optimizing the rendering process reduces the work the browser needs to do.18121813### 6.1 Animate SVG Wrapper Instead of SVG Element18141815**Impact: LOW (enables hardware acceleration)**18161817Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.18181819**Incorrect: animating SVG directly - no hardware acceleration**18201821```tsx1822function LoadingSpinner() {1823 return (1824 <svg1825 className="animate-spin"1826 width="24"1827 height="24"1828 viewBox="0 0 24 24"1829 >1830 <circle cx="12" cy="12" r="10" stroke="currentColor" />1831 </svg>1832 )1833}1834```18351836**Correct: animating wrapper div - hardware accelerated**18371838```tsx1839function LoadingSpinner() {1840 return (1841 <div className="animate-spin">1842 <svg1843 width="24"1844 height="24"1845 viewBox="0 0 24 24"1846 >1847 <circle cx="12" cy="12" r="10" stroke="currentColor" />1848 </svg>1849 </div>1850 )1851}1852```18531854This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.18551856### 6.2 CSS content-visibility for Long Lists18571858**Impact: HIGH (faster initial render)**18591860Apply `content-visibility: auto` to defer off-screen rendering.18611862**CSS:**18631864```css1865.message-item {1866 content-visibility: auto;1867 contain-intrinsic-size: 0 80px;1868}1869```18701871**Example:**18721873```tsx1874function MessageList({ messages }: { messages: Message[] }) {1875 return (1876 <div className="overflow-y-auto h-screen">1877 {messages.map(msg => (1878 <div key={msg.id} className="message-item">1879 <Avatar user={msg.author} />1880 <div>{msg.content}</div>1881 </div>1882 ))}1883 </div>1884 )1885}1886```18871888For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).18891890### 6.3 Hoist Static JSX Elements18911892**Impact: LOW (avoids re-creation)**18931894Extract static JSX outside components to avoid re-creation.18951896**Incorrect: recreates element every render**18971898```tsx1899function LoadingSkeleton() {1900 return <div className="animate-pulse h-20 bg-gray-200" />1901}19021903function Container() {1904 return (1905 <div>1906 {loading && <LoadingSkeleton />}1907 </div>1908 )1909}1910```19111912**Correct: reuses same element**19131914```tsx1915const loadingSkeleton = (1916 <div className="animate-pulse h-20 bg-gray-200" />1917)19181919function Container() {1920 return (1921 <div>1922 {loading && loadingSkeleton}1923 </div>1924 )1925}1926```19271928This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.19291930**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.19311932### 6.4 Optimize SVG Precision19331934**Impact: LOW (reduces file size)**19351936Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.19371938**Incorrect: excessive precision**19391940```svg1941<path d="M 10.293847 20.847362 L 30.938472 40.192837" />1942```19431944**Correct: 1 decimal place**19451946```svg1947<path d="M 10.3 20.8 L 30.9 40.2" />1948```19491950**Automate with SVGO:**19511952```bash1953npx svgo --precision=1 --multipass icon.svg1954```19551956### 6.5 Prevent Hydration Mismatch Without Flickering19571958**Impact: MEDIUM (avoids visual flicker and hydration errors)**19591960When 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.19611962**Incorrect: breaks SSR**19631964```tsx1965function ThemeWrapper({ children }: { children: ReactNode }) {1966 // localStorage is not available on server - throws error1967 const theme = localStorage.getItem('theme') || 'light'19681969 return (1970 <div className={theme}>1971 {children}1972 </div>1973 )1974}1975```19761977Server-side rendering will fail because `localStorage` is undefined.19781979**Incorrect: visual flickering**19801981```tsx1982function ThemeWrapper({ children }: { children: ReactNode }) {1983 const [theme, setTheme] = useState('light')19841985 useEffect(() => {1986 // Runs after hydration - causes visible flash1987 const stored = localStorage.getItem('theme')1988 if (stored) {1989 setTheme(stored)1990 }1991 }, [])19921993 return (1994 <div className={theme}>1995 {children}1996 </div>1997 )1998}1999```20002001Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.20022003**Correct: no flicker, no hydration mismatch**20042005```tsx2006function ThemeWrapper({ children }: { children: ReactNode }) {2007 return (2008 <>2009 <div id="theme-wrapper">2010 {children}2011 </div>2012 <script2013 dangerouslySetInnerHTML={{2014 __html: `2015 (function() {2016 try {2017 var theme = localStorage.getItem('theme') || 'light';2018 var el = document.getElementById('theme-wrapper');2019 if (el) el.className = theme;2020 } catch (e) {}2021 })();2022 `,2023 }}2024 />2025 </>2026 )2027}2028```20292030The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.20312032This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.20332034### 6.6 Suppress Expected Hydration Mismatches20352036**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**20372038In 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.20392040**Incorrect: known mismatch warnings**20412042```tsx2043function Timestamp() {2044 return <span>{new Date().toLocaleString()}</span>2045}2046```20472048**Correct: suppress expected mismatch only**20492050```tsx2051function Timestamp() {2052 return (2053 <span suppressHydrationWarning>2054 {new Date().toLocaleString()}2055 </span>2056 )2057}2058```20592060### 6.7 Use Activity Component for Show/Hide20612062**Impact: MEDIUM (preserves state/DOM)**20632064Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.20652066**Usage:**20672068```tsx2069import { Activity } from 'react'20702071function Dropdown({ isOpen }: Props) {2072 return (2073 <Activity mode={isOpen ? 'visible' : 'hidden'}>2074 <ExpensiveMenu />2075 </Activity>2076 )2077}2078```20792080Avoids expensive re-renders and state loss.20812082### 6.8 Use Explicit Conditional Rendering20832084**Impact: LOW (prevents rendering 0 or NaN)**20852086Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.20872088**Incorrect: renders "0" when count is 0**20892090```tsx2091function Badge({ count }: { count: number }) {2092 return (2093 <div>2094 {count && <span className="badge">{count}</span>}2095 </div>2096 )2097}20982099// When count = 0, renders: <div>0</div>2100// When count = 5, renders: <div><span class="badge">5</span></div>2101```21022103**Correct: renders nothing when count is 0**21042105```tsx2106function Badge({ count }: { count: number }) {2107 return (2108 <div>2109 {count > 0 ? <span className="badge">{count}</span> : null}2110 </div>2111 )2112}21132114// When count = 0, renders: <div></div>2115// When count = 5, renders: <div><span class="badge">5</span></div>2116```21172118### 6.9 Use useTransition Over Manual Loading States21192120**Impact: LOW (reduces re-renders and improves code clarity)**21212122Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.21232124**Incorrect: manual loading state**21252126```tsx2127function SearchResults() {2128 const [query, setQuery] = useState('')2129 const [results, setResults] = useState([])2130 const [isLoading, setIsLoading] = useState(false)21312132 const handleSearch = async (value: string) => {2133 setIsLoading(true)2134 setQuery(value)2135 const data = await fetchResults(value)2136 setResults(data)2137 setIsLoading(false)2138 }21392140 return (2141 <>2142 <input onChange={(e) => handleSearch(e.target.value)} />2143 {isLoading && <Spinner />}2144 <ResultsList results={results} />2145 </>2146 )2147}2148```21492150**Correct: useTransition with built-in pending state**21512152```tsx2153import { useTransition, useState } from 'react'21542155function SearchResults() {2156 const [query, setQuery] = useState('')2157 const [results, setResults] = useState([])2158 const [isPending, startTransition] = useTransition()21592160 const handleSearch = (value: string) => {2161 setQuery(value) // Update input immediately21622163 startTransition(async () => {2164 // Fetch and update results2165 const data = await fetchResults(value)2166 setResults(data)2167 })2168 }21692170 return (2171 <>2172 <input onChange={(e) => handleSearch(e.target.value)} />2173 {isPending && <Spinner />}2174 <ResultsList results={results} />2175 </>2176 )2177}2178```21792180**Benefits:**21812182- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`21832184- **Error resilience**: Pending state correctly resets even if the transition throws21852186- **Better responsiveness**: Keeps the UI responsive during updates21872188- **Interrupt handling**: New transitions automatically cancel pending ones21892190Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)21912192---21932194## 7. JavaScript Performance21952196**Impact: LOW-MEDIUM**21972198Micro-optimizations for hot paths can add up to meaningful improvements.21992200### 7.1 Avoid Layout Thrashing22012202**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**22032204Avoid 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.22052206**This is OK: browser batches style changes**22072208```typescript2209function updateElementStyles(element: HTMLElement) {2210 // Each line invalidates style, but browser batches the recalculation2211 element.style.width = '100px'2212 element.style.height = '200px'2213 element.style.backgroundColor = 'blue'2214 element.style.border = '1px solid black'2215}2216```22172218**Incorrect: interleaved reads and writes force reflows**22192220```typescript2221function layoutThrashing(element: HTMLElement) {2222 element.style.width = '100px'2223 const width = element.offsetWidth // Forces reflow2224 element.style.height = '200px'2225 const height = element.offsetHeight // Forces another reflow2226}2227```22282229**Correct: batch writes, then read once**22302231```typescript2232function updateElementStyles(element: HTMLElement) {2233 // Batch all writes together2234 element.style.width = '100px'2235 element.style.height = '200px'2236 element.style.backgroundColor = 'blue'2237 element.style.border = '1px solid black'22382239 // Read after all writes are done (single reflow)2240 const { width, height } = element.getBoundingClientRect()2241}2242```22432244**Correct: batch reads, then writes**22452246```typescript2247function updateElementStyles(element: HTMLElement) {2248 element.classList.add('highlighted-box')22492250 const { width, height } = element.getBoundingClientRect()2251}2252```22532254**Better: use CSS classes**22552256**React example:**22572258```tsx2259// Incorrect: interleaving style changes with layout queries2260function Box({ isHighlighted }: { isHighlighted: boolean }) {2261 const ref = useRef<HTMLDivElement>(null)22622263 useEffect(() => {2264 if (ref.current && isHighlighted) {2265 ref.current.style.width = '100px'2266 const width = ref.current.offsetWidth // Forces layout2267 ref.current.style.height = '200px'2268 }2269 }, [isHighlighted])22702271 return <div ref={ref}>Content</div>2272}22732274// Correct: toggle class2275function Box({ isHighlighted }: { isHighlighted: boolean }) {2276 return (2277 <div className={isHighlighted ? 'highlighted-box' : ''}>2278 Content2279 </div>2280 )2281}2282```22832284Prefer 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.22852286See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.22872288### 7.2 Build Index Maps for Repeated Lookups22892290**Impact: LOW-MEDIUM (1M ops to 2K ops)**22912292Multiple `.find()` calls by the same key should use a Map.22932294**Incorrect (O(n) per lookup):**22952296```typescript2297function processOrders(orders: Order[], users: User[]) {2298 return orders.map(order => ({2299 ...order,2300 user: users.find(u => u.id === order.userId)2301 }))2302}2303```23042305**Correct (O(1) per lookup):**23062307```typescript2308function processOrders(orders: Order[], users: User[]) {2309 const userById = new Map(users.map(u => [u.id, u]))23102311 return orders.map(order => ({2312 ...order,2313 user: userById.get(order.userId)2314 }))2315}2316```23172318Build map once (O(n)), then all lookups are O(1).23192320For 1000 orders × 1000 users: 1M ops → 2K ops.23212322### 7.3 Cache Property Access in Loops23232324**Impact: LOW-MEDIUM (reduces lookups)**23252326Cache object property lookups in hot paths.23272328**Incorrect: 3 lookups × N iterations**23292330```typescript2331for (let i = 0; i < arr.length; i++) {2332 process(obj.config.settings.value)2333}2334```23352336**Correct: 1 lookup total**23372338```typescript2339const value = obj.config.settings.value2340const len = arr.length2341for (let i = 0; i < len; i++) {2342 process(value)2343}2344```23452346### 7.4 Cache Repeated Function Calls23472348**Impact: MEDIUM (avoid redundant computation)**23492350Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.23512352**Incorrect: redundant computation**23532354```typescript2355function ProjectList({ projects }: { projects: Project[] }) {2356 return (2357 <div>2358 {projects.map(project => {2359 // slugify() called 100+ times for same project names2360 const slug = slugify(project.name)23612362 return <ProjectCard key={project.id} slug={slug} />2363 })}2364 </div>2365 )2366}2367```23682369**Correct: cached results**23702371```typescript2372// Module-level cache2373const slugifyCache = new Map<string, string>()23742375function cachedSlugify(text: string): string {2376 if (slugifyCache.has(text)) {2377 return slugifyCache.get(text)!2378 }2379 const result = slugify(text)2380 slugifyCache.set(text, result)2381 return result2382}23832384function ProjectList({ projects }: { projects: Project[] }) {2385 return (2386 <div>2387 {projects.map(project => {2388 // Computed only once per unique project name2389 const slug = cachedSlugify(project.name)23902391 return <ProjectCard key={project.id} slug={slug} />2392 })}2393 </div>2394 )2395}2396```23972398**Simpler pattern for single-value functions:**23992400```typescript2401let isLoggedInCache: boolean | null = null24022403function isLoggedIn(): boolean {2404 if (isLoggedInCache !== null) {2405 return isLoggedInCache2406 }24072408 isLoggedInCache = document.cookie.includes('auth=')2409 return isLoggedInCache2410}24112412// Clear cache when auth changes2413function onAuthChange() {2414 isLoggedInCache = null2415}2416```24172418Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.24192420Reference: [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)24212422### 7.5 Cache Storage API Calls24232424**Impact: LOW-MEDIUM (reduces expensive I/O)**24252426`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.24272428**Incorrect: reads storage on every call**24292430```typescript2431function getTheme() {2432 return localStorage.getItem('theme') ?? 'light'2433}2434// Called 10 times = 10 storage reads2435```24362437**Correct: Map cache**24382439```typescript2440const storageCache = new Map<string, string | null>()24412442function getLocalStorage(key: string) {2443 if (!storageCache.has(key)) {2444 storageCache.set(key, localStorage.getItem(key))2445 }2446 return storageCache.get(key)2447}24482449function setLocalStorage(key: string, value: string) {2450 localStorage.setItem(key, value)2451 storageCache.set(key, value) // keep cache in sync2452}2453```24542455Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.24562457**Cookie caching:**24582459```typescript2460let cookieCache: Record<string, string> | null = null24612462function getCookie(name: string) {2463 if (!cookieCache) {2464 cookieCache = Object.fromEntries(2465 document.cookie.split('; ').map(c => c.split('='))2466 )2467 }2468 return cookieCache[name]2469}2470```24712472**Important: invalidate on external changes**24732474```typescript2475window.addEventListener('storage', (e) => {2476 if (e.key) storageCache.delete(e.key)2477})24782479document.addEventListener('visibilitychange', () => {2480 if (document.visibilityState === 'visible') {2481 storageCache.clear()2482 }2483})2484```24852486If storage can change externally (another tab, server-set cookies), invalidate cache:24872488### 7.6 Combine Multiple Array Iterations24892490**Impact: LOW-MEDIUM (reduces iterations)**24912492Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.24932494**Incorrect: 3 iterations**24952496```typescript2497const admins = users.filter(u => u.isAdmin)2498const testers = users.filter(u => u.isTester)2499const inactive = users.filter(u => !u.isActive)2500```25012502**Correct: 1 iteration**25032504```typescript2505const admins: User[] = []2506const testers: User[] = []2507const inactive: User[] = []25082509for (const user of users) {2510 if (user.isAdmin) admins.push(user)2511 if (user.isTester) testers.push(user)2512 if (!user.isActive) inactive.push(user)2513}2514```25152516### 7.7 Early Length Check for Array Comparisons25172518**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**25192520When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.25212522In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).25232524**Incorrect: always runs expensive comparison**25252526```typescript2527function hasChanges(current: string[], original: string[]) {2528 // Always sorts and joins, even when lengths differ2529 return current.sort().join() !== original.sort().join()2530}2531```25322533Two 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.25342535**Correct (O(1) length check first):**25362537```typescript2538function hasChanges(current: string[], original: string[]) {2539 // Early return if lengths differ2540 if (current.length !== original.length) {2541 return true2542 }2543 // Only sort when lengths match2544 const currentSorted = current.toSorted()2545 const originalSorted = original.toSorted()2546 for (let i = 0; i < currentSorted.length; i++) {2547 if (currentSorted[i] !== originalSorted[i]) {2548 return true2549 }2550 }2551 return false2552}2553```25542555This new approach is more efficient because:25562557- It avoids the overhead of sorting and joining the arrays when lengths differ25582559- It avoids consuming memory for the joined strings (especially important for large arrays)25602561- It avoids mutating the original arrays25622563- It returns early when a difference is found25642565### 7.8 Early Return from Functions25662567**Impact: LOW-MEDIUM (avoids unnecessary computation)**25682569Return early when result is determined to skip unnecessary processing.25702571**Incorrect: processes all items even after finding answer**25722573```typescript2574function validateUsers(users: User[]) {2575 let hasError = false2576 let errorMessage = ''25772578 for (const user of users) {2579 if (!user.email) {2580 hasError = true2581 errorMessage = 'Email required'2582 }2583 if (!user.name) {2584 hasError = true2585 errorMessage = 'Name required'2586 }2587 // Continues checking all users even after error found2588 }25892590 return hasError ? { valid: false, error: errorMessage } : { valid: true }2591}2592```25932594**Correct: returns immediately on first error**25952596```typescript2597function validateUsers(users: User[]) {2598 for (const user of users) {2599 if (!user.email) {2600 return { valid: false, error: 'Email required' }2601 }2602 if (!user.name) {2603 return { valid: false, error: 'Name required' }2604 }2605 }26062607 return { valid: true }2608}2609```26102611### 7.9 Hoist RegExp Creation26122613**Impact: LOW-MEDIUM (avoids recreation)**26142615Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.26162617**Incorrect: new RegExp every render**26182619```tsx2620function Highlighter({ text, query }: Props) {2621 const regex = new RegExp(`(${query})`, 'gi')2622 const parts = text.split(regex)2623 return <>{parts.map((part, i) => ...)}</>2624}2625```26262627**Correct: memoize or hoist**26282629```tsx2630const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/26312632function Highlighter({ text, query }: Props) {2633 const regex = useMemo(2634 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),2635 [query]2636 )2637 const parts = text.split(regex)2638 return <>{parts.map((part, i) => ...)}</>2639}2640```26412642**Warning: global regex has mutable state**26432644```typescript2645const regex = /foo/g2646regex.test('foo') // true, lastIndex = 32647regex.test('foo') // false, lastIndex = 02648```26492650Global regex (`/g`) has mutable `lastIndex` state:26512652### 7.10 Use Loop for Min/Max Instead of Sort26532654**Impact: LOW (O(n) instead of O(n log n))**26552656Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.26572658**Incorrect (O(n log n) - sort to find latest):**26592660```typescript2661interface Project {2662 id: string2663 name: string2664 updatedAt: number2665}26662667function getLatestProject(projects: Project[]) {2668 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)2669 return sorted[0]2670}2671```26722673Sorts the entire array just to find the maximum value.26742675**Incorrect (O(n log n) - sort for oldest and newest):**26762677```typescript2678function getOldestAndNewest(projects: Project[]) {2679 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)2680 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }2681}2682```26832684Still sorts unnecessarily when only min/max are needed.26852686**Correct (O(n) - single loop):**26872688```typescript2689function getLatestProject(projects: Project[]) {2690 if (projects.length === 0) return null26912692 let latest = projects[0]26932694 for (let i = 1; i < projects.length; i++) {2695 if (projects[i].updatedAt > latest.updatedAt) {2696 latest = projects[i]2697 }2698 }26992700 return latest2701}27022703function getOldestAndNewest(projects: Project[]) {2704 if (projects.length === 0) return { oldest: null, newest: null }27052706 let oldest = projects[0]2707 let newest = projects[0]27082709 for (let i = 1; i < projects.length; i++) {2710 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]2711 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]2712 }27132714 return { oldest, newest }2715}2716```27172718Single pass through the array, no copying, no sorting.27192720**Alternative: Math.min/Math.max for small arrays**27212722```typescript2723const numbers = [5, 2, 8, 1, 9]2724const min = Math.min(...numbers)2725const max = Math.max(...numbers)2726```27272728This 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.27292730### 7.11 Use Set/Map for O(1) Lookups27312732**Impact: LOW-MEDIUM (O(n) to O(1))**27332734Convert arrays to Set/Map for repeated membership checks.27352736**Incorrect (O(n) per check):**27372738```typescript2739const allowedIds = ['a', 'b', 'c', ...]2740items.filter(item => allowedIds.includes(item.id))2741```27422743**Correct (O(1) per check):**27442745```typescript2746const allowedIds = new Set(['a', 'b', 'c', ...])2747items.filter(item => allowedIds.has(item.id))2748```27492750### 7.12 Use toSorted() Instead of sort() for Immutability27512752**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**27532754`.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.27552756**Incorrect: mutates original array**27572758```typescript2759function UserList({ users }: { users: User[] }) {2760 // Mutates the users prop array!2761 const sorted = useMemo(2762 () => users.sort((a, b) => a.name.localeCompare(b.name)),2763 [users]2764 )2765 return <div>{sorted.map(renderUser)}</div>2766}2767```27682769**Correct: creates new array**27702771```typescript2772function UserList({ users }: { users: User[] }) {2773 // Creates new sorted array, original unchanged2774 const sorted = useMemo(2775 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),2776 [users]2777 )2778 return <div>{sorted.map(renderUser)}</div>2779}2780```27812782**Why this matters in React:**278327841. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only278527862. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior27872788**Browser support: fallback for older browsers**27892790```typescript2791// Fallback for older browsers2792const sorted = [...items].sort((a, b) => a.value - b.value)2793```27942795`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:27962797**Other immutable array methods:**27982799- `.toSorted()` - immutable sort28002801- `.toReversed()` - immutable reverse28022803- `.toSpliced()` - immutable splice28042805- `.with()` - immutable element replacement28062807---28082809## 8. Advanced Patterns28102811**Impact: LOW**28122813Advanced patterns for specific cases that require careful implementation.28142815### 8.1 Initialize App Once, Not Per Mount28162817**Impact: LOW-MEDIUM (avoids duplicate init in development)**28182819Do 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.28202821**Incorrect: runs twice in dev, re-runs on remount**28222823```tsx2824function Comp() {2825 useEffect(() => {2826 loadFromStorage()2827 checkAuthToken()2828 }, [])28292830 // ...2831}2832```28332834**Correct: once per app load**28352836```tsx2837let didInit = false28382839function Comp() {2840 useEffect(() => {2841 if (didInit) return2842 didInit = true2843 loadFromStorage()2844 checkAuthToken()2845 }, [])28462847 // ...2848}2849```28502851Reference: [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)28522853### 8.2 Store Event Handlers in Refs28542855**Impact: LOW (stable subscriptions)**28562857Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.28582859**Incorrect: re-subscribes on every render**28602861```tsx2862function useWindowEvent(event: string, handler: (e) => void) {2863 useEffect(() => {2864 window.addEventListener(event, handler)2865 return () => window.removeEventListener(event, handler)2866 }, [event, handler])2867}2868```28692870**Correct: stable subscription**28712872```tsx2873import { useEffectEvent } from 'react'28742875function useWindowEvent(event: string, handler: (e) => void) {2876 const onEvent = useEffectEvent(handler)28772878 useEffect(() => {2879 window.addEventListener(event, onEvent)2880 return () => window.removeEventListener(event, onEvent)2881 }, [event])2882}2883```28842885**Alternative: use `useEffectEvent` if you're on latest React:**28862887`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.28882889### 8.3 useEffectEvent for Stable Callback Refs28902891**Impact: LOW (prevents effect re-runs)**28922893Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.28942895**Incorrect: effect re-runs on every callback change**28962897```tsx2898function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2899 const [query, setQuery] = useState('')29002901 useEffect(() => {2902 const timeout = setTimeout(() => onSearch(query), 300)2903 return () => clearTimeout(timeout)2904 }, [query, onSearch])2905}2906```29072908**Correct: using React's useEffectEvent**29092910```tsx2911import { useEffectEvent } from 'react';29122913function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2914 const [query, setQuery] = useState('')2915 const onSearchEvent = useEffectEvent(onSearch)29162917 useEffect(() => {2918 const timeout = setTimeout(() => onSearchEvent(query), 300)2919 return () => clearTimeout(timeout)2920 }, [query])2921}2922```29232924---29252926## References292729281. [https://react.dev](https://react.dev)29292. [https://nextjs.org](https://nextjs.org)29303. [https://swr.vercel.app](https://swr.vercel.app)29314. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)29325. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)29336. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)29347. [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)2935
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago | |
| wpscanteam/wpscanAGENTS.md · 9.7k | AGENTS.md | setupbuildteststyle+6 | 100/100 | 2 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 | |
| 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 |
