返回 AiToEarn
1 # React Best Practices
2
3 **Version 1.0.0**
4 Vercel Engineering
5 January 2026
6
7 > **Note:**
8 > This document is mainly for agents and LLMs to follow when maintaining,
9 > generating, or refactoring React and Next.js codebases. Humans
10 > may also find it useful, but guidance here is optimized for automation
11 > and consistency by AI-assisted workflows.
12
13 ---
14
15 ## Abstract
16
17 Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
18
19 ---
20
21 ## Table of Contents
22
23 1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
24 - 1.1 [Check Cheap Conditions Before Async Flags](#11-check-cheap-conditions-before-async-flags)
25 - 1.2 [Defer Await Until Needed](#12-defer-await-until-needed)
26 - 1.3 [Dependency-Based Parallelization](#13-dependency-based-parallelization)
27 - 1.4 [Prevent Waterfall Chains in API Routes](#14-prevent-waterfall-chains-in-api-routes)
28 - 1.5 [Promise.all() for Independent Operations](#15-promiseall-for-independent-operations)
29 - 1.6 [Strategic Suspense Boundaries](#16-strategic-suspense-boundaries)
30 2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
31 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
32 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
33 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
34 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
35 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
36 3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
37 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
38 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
39 - 3.3 [Avoid Shared Module State for Request Data](#33-avoid-shared-module-state-for-request-data)
40 - 3.4 [Cross-Request LRU Caching](#34-cross-request-lru-caching)
41 - 3.5 [Hoist Static I/O to Module Level](#35-hoist-static-io-to-module-level)
42 - 3.6 [Minimize Serialization at RSC Boundaries](#36-minimize-serialization-at-rsc-boundaries)
43 - 3.7 [Parallel Data Fetching with Component Composition](#37-parallel-data-fetching-with-component-composition)
44 - 3.8 [Parallel Nested Data Fetching](#38-parallel-nested-data-fetching)
45 - 3.9 [Per-Request Deduplication with React.cache()](#39-per-request-deduplication-with-reactcache)
46 - 3.10 [Use after() for Non-Blocking Operations](#310-use-after-for-non-blocking-operations)
47 4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
48 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
49 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
50 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
51 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
52 5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
53 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
54 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
55 - 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)
56 - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)
57 - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
58 - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)
59 - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)
60 - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)
61 - 5.9 [Split Combined Hook Computations](#59-split-combined-hook-computations)
62 - 5.10 [Subscribe to Derived State](#510-subscribe-to-derived-state)
63 - 5.11 [Use Functional setState Updates](#511-use-functional-setstate-updates)
64 - 5.12 [Use Lazy State Initialization](#512-use-lazy-state-initialization)
65 - 5.13 [Use Transitions for Non-Urgent Updates](#513-use-transitions-for-non-urgent-updates)
66 - 5.14 [Use useDeferredValue for Expensive Derived Renders](#514-use-usedeferredvalue-for-expensive-derived-renders)
67 - 5.15 [Use useRef for Transient Values](#515-use-useref-for-transient-values)
68 6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
69 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
70 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
71 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
72 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
73 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
74 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
75 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
76 - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)
77 - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)
78 - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)
79 - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)
80 7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
81 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
82 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
83 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
84 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
85 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
86 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
87 - 7.7 [Defer Non-Critical Work with requestIdleCallback](#77-defer-non-critical-work-with-requestidlecallback)
88 - 7.8 [Early Length Check for Array Comparisons](#78-early-length-check-for-array-comparisons)
89 - 7.9 [Early Return from Functions](#79-early-return-from-functions)
90 - 7.10 [Hoist RegExp Creation](#710-hoist-regexp-creation)
91 - 7.11 [Use flatMap to Map and Filter in One Pass](#711-use-flatmap-to-map-and-filter-in-one-pass)
92 - 7.12 [Use Loop for Min/Max Instead of Sort](#712-use-loop-for-minmax-instead-of-sort)
93 - 7.13 [Use Set/Map for O(1) Lookups](#713-use-setmap-for-o1-lookups)
94 - 7.14 [Use toSorted() Instead of sort() for Immutability](#714-use-tosorted-instead-of-sort-for-immutability)
95 8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
96 - 8.1 [Do Not Put Effect Events in Dependency Arrays](#81-do-not-put-effect-events-in-dependency-arrays)
97 - 8.2 [Initialize App Once, Not Per Mount](#82-initialize-app-once-not-per-mount)
98 - 8.3 [Store Event Handlers in Refs](#83-store-event-handlers-in-refs)
99 - 8.4 [useEffectEvent for Stable Callback Refs](#84-useeffectevent-for-stable-callback-refs)
100
101 ---
102
103 ## 1. Eliminating Waterfalls
104
105 **Impact: CRITICAL**
106
107 Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
108
109 ### 1.1 Check Cheap Conditions Before Async Flags
110
111 **Impact: HIGH (avoids unnecessary async work when a synchronous guard already fails)**
112
113 When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.
114
115 This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.
116
117 **Incorrect:**
118
119 ```typescript
120 const someFlag = await getFlag()
121
122 if (someFlag && someCondition) {
123 // ...
124 }
125 ```
126
127 **Correct:**
128
129 ```typescript
130 if (someCondition) {
131 const someFlag = await getFlag()
132 if (someFlag) {
133 // ...
134 }
135 }
136 ```
137
138 This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.
139
140 Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.
141
142 ### 1.2 Defer Await Until Needed
143
144 **Impact: HIGH (avoids blocking unused code paths)**
145
146 Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
147
148 **Incorrect: blocks both branches**
149
150 ```typescript
151 async function handleRequest(userId: string, skipProcessing: boolean) {
152 const userData = await fetchUserData(userId)
153
154 if (skipProcessing) {
155 // Returns immediately but still waited for userData
156 return { skipped: true }
157 }
158
159 // Only this branch uses userData
160 return processUserData(userData)
161 }
162 ```
163
164 **Correct: only blocks when needed**
165
166 ```typescript
167 async function handleRequest(userId: string, skipProcessing: boolean) {
168 if (skipProcessing) {
169 // Returns immediately without waiting
170 return { skipped: true }
171 }
172
173 // Fetch only when needed
174 const userData = await fetchUserData(userId)
175 return processUserData(userData)
176 }
177 ```
178
179 **Another example: early return optimization**
180
181 ```typescript
182 // Incorrect: always fetches permissions
183 async function updateResource(resourceId: string, userId: string) {
184 const permissions = await fetchPermissions(userId)
185 const resource = await getResource(resourceId)
186
187 if (!resource) {
188 return { error: 'Not found' }
189 }
190
191 if (!permissions.canEdit) {
192 return { error: 'Forbidden' }
193 }
194
195 return await updateResourceData(resource, permissions)
196 }
197
198 // Correct: fetches only when needed
199 async function updateResource(resourceId: string, userId: string) {
200 const resource = await getResource(resourceId)
201
202 if (!resource) {
203 return { error: 'Not found' }
204 }
205
206 const permissions = await fetchPermissions(userId)
207
208 if (!permissions.canEdit) {
209 return { error: 'Forbidden' }
210 }
211
212 return await updateResourceData(resource, permissions)
213 }
214 ```
215
216 This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
217
218 For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).
219
220 ### 1.3 Dependency-Based Parallelization
221
222 **Impact: CRITICAL (2-10× improvement)**
223
224 For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
225
226 **Incorrect: profile waits for config unnecessarily**
227
228 ```typescript
229 const [user, config] = await Promise.all([
230 fetchUser(),
231 fetchConfig()
232 ])
233 const profile = await fetchProfile(user.id)
234 ```
235
236 **Correct: config and profile run in parallel**
237
238 ```typescript
239 import { all } from 'better-all'
240
241 const { user, config, profile } = await all({
242 async user() { return fetchUser() },
243 async config() { return fetchConfig() },
244 async profile() {
245 return fetchProfile((await this.$.user).id)
246 }
247 })
248 ```
249
250 **Alternative without extra dependencies:**
251
252 ```typescript
253 const userPromise = fetchUser()
254 const profilePromise = userPromise.then(user => fetchProfile(user.id))
255
256 const [user, config, profile] = await Promise.all([
257 userPromise,
258 fetchConfig(),
259 profilePromise
260 ])
261 ```
262
263 We can also create all the promises first, and do `Promise.all()` at the end.
264
265 Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
266
267 ### 1.4 Prevent Waterfall Chains in API Routes
268
269 **Impact: CRITICAL (2-10× improvement)**
270
271 In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
272
273 **Incorrect: config waits for auth, data waits for both**
274
275 ```typescript
276 export async function GET(request: Request) {
277 const session = await auth()
278 const config = await fetchConfig()
279 const data = await fetchData(session.user.id)
280 return Response.json({ data, config })
281 }
282 ```
283
284 **Correct: auth and config start immediately**
285
286 ```typescript
287 export async function GET(request: Request) {
288 const sessionPromise = auth()
289 const configPromise = fetchConfig()
290 const session = await sessionPromise
291 const [config, data] = await Promise.all([
292 configPromise,
293 fetchData(session.user.id)
294 ])
295 return Response.json({ data, config })
296 }
297 ```
298
299 For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
300
301 ### 1.5 Promise.all() for Independent Operations
302
303 **Impact: CRITICAL (2-10× improvement)**
304
305 When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
306
307 **Incorrect: sequential execution, 3 round trips**
308
309 ```typescript
310 const user = await fetchUser()
311 const posts = await fetchPosts()
312 const comments = await fetchComments()
313 ```
314
315 **Correct: parallel execution, 1 round trip**
316
317 ```typescript
318 const [user, posts, comments] = await Promise.all([
319 fetchUser(),
320 fetchPosts(),
321 fetchComments()
322 ])
323 ```
324
325 ### 1.6 Strategic Suspense Boundaries
326
327 **Impact: HIGH (faster initial paint)**
328
329 Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
330
331 **Incorrect: wrapper blocked by data fetching**
332
333 ```tsx
334 async function Page() {
335 const data = await fetchData() // Blocks entire page
336
337 return (
338 <div>
339 <div>Sidebar</div>
340 <div>Header</div>
341 <div>
342 <DataDisplay data={data} />
343 </div>
344 <div>Footer</div>
345 </div>
346 )
347 }
348 ```
349
350 The entire layout waits for data even though only the middle section needs it.
351
352 **Correct: wrapper shows immediately, data streams in**
353
354 ```tsx
355 function Page() {
356 return (
357 <div>
358 <div>Sidebar</div>
359 <div>Header</div>
360 <div>
361 <Suspense fallback={<Skeleton />}>
362 <DataDisplay />
363 </Suspense>
364 </div>
365 <div>Footer</div>
366 </div>
367 )
368 }
369
370 async function DataDisplay() {
371 const data = await fetchData() // Only blocks this component
372 return <div>{data.content}</div>
373 }
374 ```
375
376 Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
377
378 **Alternative: share promise across components**
379
380 ```tsx
381 function Page() {
382 // Start fetch immediately, but don't await
383 const dataPromise = fetchData()
384
385 return (
386 <div>
387 <div>Sidebar</div>
388 <div>Header</div>
389 <Suspense fallback={<Skeleton />}>
390 <DataDisplay dataPromise={dataPromise} />
391 <DataSummary dataPromise={dataPromise} />
392 </Suspense>
393 <div>Footer</div>
394 </div>
395 )
396 }
397
398 function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
399 const data = use(dataPromise) // Unwraps the promise
400 return <div>{data.content}</div>
401 }
402
403 function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
404 const data = use(dataPromise) // Reuses the same promise
405 return <div>{data.summary}</div>
406 }
407 ```
408
409 Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
410
411 **When NOT to use this pattern:**
412
413 - Critical data needed for layout decisions (affects positioning)
414
415 - SEO-critical content above the fold
416
417 - Small, fast queries where suspense overhead isn't worth it
418
419 - When you want to avoid layout shift (loading → content jump)
420
421 **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
422
423 ---
424
425 ## 2. Bundle Size Optimization
426
427 **Impact: CRITICAL**
428
429 Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
430
431 ### 2.1 Avoid Barrel File Imports
432
433 **Impact: CRITICAL (200-800ms import cost, slow builds)**
434
435 Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
436
437 Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
438
439 **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.
440
441 **Incorrect: imports entire library**
442
443 ```tsx
444 import { Check, X, Menu } from 'lucide-react'
445 // Loads 1,583 modules, takes ~2.8s extra in dev
446 // Runtime cost: 200-800ms on every cold start
447
448 import { Button, TextField } from '@mui/material'
449 // Loads 2,225 modules, takes ~4.2s extra in dev
450 ```
451
452 **Correct - Next.js 13.5+ (recommended):**
453
454 ```tsx
455 // Keep the standard imports - Next.js transforms them to direct imports
456 import { Check, X, Menu } from 'lucide-react'
457 // Full TypeScript support, no manual path wrangling
458 ```
459
460 This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.
461
462 **Correct - Direct imports (non-Next.js projects):**
463
464 ```tsx
465 import Button from '@mui/material/Button'
466 import TextField from '@mui/material/TextField'
467 // Loads only what you use
468 ```
469
470 > **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.
471
472 These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
473
474 Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
475
476 Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
477
478 ### 2.2 Conditional Module Loading
479
480 **Impact: HIGH (loads large data only when needed)**
481
482 Load large data or modules only when a feature is activated.
483
484 **Example: lazy-load animation frames**
485
486 ```tsx
487 function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
488 const [frames, setFrames] = useState<Frame[] | null>(null)
489
490 useEffect(() => {
491 if (enabled && !frames && typeof window !== 'undefined') {
492 import('./animation-frames.js')
493 .then(mod => setFrames(mod.frames))
494 .catch(() => setEnabled(false))
495 }
496 }, [enabled, frames, setEnabled])
497
498 if (!frames) return <Skeleton />
499 return <Canvas frames={frames} />
500 }
501 ```
502
503 The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
504
505 ### 2.3 Defer Non-Critical Third-Party Libraries
506
507 **Impact: MEDIUM (loads after hydration)**
508
509 Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
510
511 **Incorrect: blocks initial bundle**
512
513 ```tsx
514 import { Analytics } from '@vercel/analytics/react'
515
516 export default function RootLayout({ children }) {
517 return (
518 <html>
519 <body>
520 {children}
521 <Analytics />
522 </body>
523 </html>
524 )
525 }
526 ```
527
528 **Correct: loads after hydration**
529
530 ```tsx
531 import dynamic from 'next/dynamic'
532
533 const Analytics = dynamic(
534 () => import('@vercel/analytics/react').then(m => m.Analytics),
535 { ssr: false }
536 )
537
538 export default function RootLayout({ children }) {
539 return (
540 <html>
541 <body>
542 {children}
543 <Analytics />
544 </body>
545 </html>
546 )
547 }
548 ```
549
550 ### 2.4 Dynamic Imports for Heavy Components
551
552 **Impact: CRITICAL (directly affects TTI and LCP)**
553
554 Use `next/dynamic` to lazy-load large components not needed on initial render.
555
556 **Incorrect: Monaco bundles with main chunk ~300KB**
557
558 ```tsx
559 import { MonacoEditor } from './monaco-editor'
560
561 function CodePanel({ code }: { code: string }) {
562 return <MonacoEditor value={code} />
563 }
564 ```
565
566 **Correct: Monaco loads on demand**
567
568 ```tsx
569 import dynamic from 'next/dynamic'
570
571 const MonacoEditor = dynamic(
572 () => import('./monaco-editor').then(m => m.MonacoEditor),
573 { ssr: false }
574 )
575
576 function CodePanel({ code }: { code: string }) {
577 return <MonacoEditor value={code} />
578 }
579 ```
580
581 ### 2.5 Preload Based on User Intent
582
583 **Impact: MEDIUM (reduces perceived latency)**
584
585 Preload heavy bundles before they're needed to reduce perceived latency.
586
587 **Example: preload on hover/focus**
588
589 ```tsx
590 function EditorButton({ onClick }: { onClick: () => void }) {
591 const preload = () => {
592 if (typeof window !== 'undefined') {
593 void import('./monaco-editor')
594 }
595 }
596
597 return (
598 <button
599 onMouseEnter={preload}
600 onFocus={preload}
601 onClick={onClick}
602 >
603 Open Editor
604 </button>
605 )
606 }
607 ```
608
609 **Example: preload when feature flag is enabled**
610
611 ```tsx
612 function FlagsProvider({ children, flags }: Props) {
613 useEffect(() => {
614 if (flags.editorEnabled && typeof window !== 'undefined') {
615 void import('./monaco-editor').then(mod => mod.init())
616 }
617 }, [flags.editorEnabled])
618
619 return <FlagsContext.Provider value={flags}>
620 {children}
621 </FlagsContext.Provider>
622 }
623 ```
624
625 The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
626
627 ---
628
629 ## 3. Server-Side Performance
630
631 **Impact: HIGH**
632
633 Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
634
635 ### 3.1 Authenticate Server Actions Like API Routes
636
637 **Impact: CRITICAL (prevents unauthorized access to server mutations)**
638
639 Server 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.
640
641 Next.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."
642
643 **Incorrect: no authentication check**
644
645 ```typescript
646 'use server'
647
648 export async function deleteUser(userId: string) {
649 // Anyone can call this! No auth check
650 await db.user.delete({ where: { id: userId } })
651 return { success: true }
652 }
653 ```
654
655 **Correct: authentication inside the action**
656
657 ```typescript
658 'use server'
659
660 import { verifySession } from '@/lib/auth'
661 import { unauthorized } from '@/lib/errors'
662
663 export async function deleteUser(userId: string) {
664 // Always check auth inside the action
665 const session = await verifySession()
666
667 if (!session) {
668 throw unauthorized('Must be logged in')
669 }
670
671 // Check authorization too
672 if (session.user.role !== 'admin' && session.user.id !== userId) {
673 throw unauthorized('Cannot delete other users')
674 }
675
676 await db.user.delete({ where: { id: userId } })
677 return { success: true }
678 }
679 ```
680
681 **With input validation:**
682
683 ```typescript
684 'use server'
685
686 import { verifySession } from '@/lib/auth'
687 import { z } from 'zod'
688
689 const updateProfileSchema = z.object({
690 userId: z.string().uuid(),
691 name: z.string().min(1).max(100),
692 email: z.string().email()
693 })
694
695 export async function updateProfile(data: unknown) {
696 // Validate input first
697 const validated = updateProfileSchema.parse(data)
698
699 // Then authenticate
700 const session = await verifySession()
701 if (!session) {
702 throw new Error('Unauthorized')
703 }
704
705 // Then authorize
706 if (session.user.id !== validated.userId) {
707 throw new Error('Can only update own profile')
708 }
709
710 // Finally perform the mutation
711 await db.user.update({
712 where: { id: validated.userId },
713 data: {
714 name: validated.name,
715 email: validated.email
716 }
717 })
718
719 return { success: true }
720 }
721 ```
722
723 Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
724
725 ### 3.2 Avoid Duplicate Serialization in RSC Props
726
727 **Impact: LOW (reduces network payload by avoiding duplicate serialization)**
728
729 RSC→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.
730
731 **Incorrect: duplicates array**
732
733 ```tsx
734 // RSC: sends 6 strings (2 arrays × 3 items)
735 <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
736 ```
737
738 **Correct: sends 3 strings**
739
740 ```tsx
741 // RSC: send once
742 <ClientList usernames={usernames} />
743
744 // Client: transform there
745 'use client'
746 const sorted = useMemo(() => [...usernames].sort(), [usernames])
747 ```
748
749 **Nested deduplication behavior:**
750
751 ```tsx
752 // string[] - duplicates everything
753 usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
754
755 // object[] - duplicates array structure only
756 users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
757 ```
758
759 Deduplication works recursively. Impact varies by data type:
760
761 - `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
762
763 - `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
764
765 **Operations breaking deduplication: create new references**
766
767 - Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
768
769 - Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
770
771 **More examples:**
772
773 ```tsx
774 // ❌ Bad
775 <C users={users} active={users.filter(u => u.active)} />
776 <C product={product} productName={product.name} />
777
778 // ✅ Good
779 <C users={users} />
780 <C product={product} />
781 // Do filtering/destructuring in client
782 ```
783
784 **Exception:** Pass derived data when transformation is expensive or client doesn't need original.
785
786 ### 3.3 Avoid Shared Module State for Request Data
787
788 **Impact: HIGH (prevents concurrency bugs and request data leaks)**
789
790 For React Server Components and client components rendered during SSR, avoid using mutable module-level variables to share request-scoped data. Server renders can run concurrently in the same process. If one render writes to shared module state and another render reads it, you can get race conditions, cross-request contamination, and security bugs where one user's data appears in another user's response.
791
792 Treat module scope on the server as process-wide shared memory, not request-local state.
793
794 **Incorrect: request data leaks across concurrent renders**
795
796 ```tsx
797 let currentUser: User | null = null
798
799 export default async function Page() {
800 currentUser = await auth()
801 return <Dashboard />
802 }
803
804 async function Dashboard() {
805 return <div>{currentUser?.name}</div>
806 }
807 ```
808
809 If two requests overlap, request A can set `currentUser`, then request B overwrites it before request A finishes rendering `Dashboard`.
810
811 **Correct: keep request data local to the render tree**
812
813 ```tsx
814 export default async function Page() {
815 const user = await auth()
816 return <Dashboard user={user} />
817 }
818
819 function Dashboard({ user }: { user: User | null }) {
820 return <div>{user?.name}</div>
821 }
822 ```
823
824 Safe exceptions:
825
826 - Immutable static assets or config loaded once at module scope
827
828 - Shared caches intentionally designed for cross-request reuse and keyed correctly
829
830 - Process-wide singletons that do not store request- or user-specific mutable data
831
832 For static assets and config, see [Hoist Static I/O to Module Level](./server-hoist-static-io.md).
833
834 ### 3.4 Cross-Request LRU Caching
835
836 **Impact: HIGH (caches across requests)**
837
838 `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
839
840 **Implementation:**
841
842 ```typescript
843 import { LRUCache } from 'lru-cache'
844
845 const cache = new LRUCache<string, any>({
846 max: 1000,
847 ttl: 5 * 60 * 1000 // 5 minutes
848 })
849
850 export async function getUser(id: string) {
851 const cached = cache.get(id)
852 if (cached) return cached
853
854 const user = await db.user.findUnique({ where: { id } })
855 cache.set(id, user)
856 return user
857 }
858
859 // Request 1: DB query, result cached
860 // Request 2: cache hit, no DB query
861 ```
862
863 Use when sequential user actions hit multiple endpoints needing the same data within seconds.
864
865 **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.
866
867 **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
868
869 Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
870
871 ### 3.5 Hoist Static I/O to Module Level
872
873 **Impact: HIGH (avoids repeated file/network I/O per request)**
874
875 When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
876
877 **Incorrect: reads font file on every request**
878
879 ```typescript
880 // app/api/og/route.tsx
881 import { ImageResponse } from 'next/og'
882
883 export async function GET(request: Request) {
884 // Runs on EVERY request - expensive!
885 const fontData = await fetch(
886 new URL('./fonts/Inter.ttf', import.meta.url)
887 ).then(res => res.arrayBuffer())
888
889 const logoData = await fetch(
890 new URL('./images/logo.png', import.meta.url)
891 ).then(res => res.arrayBuffer())
892
893 return new ImageResponse(
894 <div style={{ fontFamily: 'Inter' }}>
895 <img src={logoData} />
896 Hello World
897 </div>,
898 { fonts: [{ name: 'Inter', data: fontData }] }
899 )
900 }
901 ```
902
903 **Correct: loads once at module initialization**
904
905 ```typescript
906 // app/api/og/route.tsx
907 import { ImageResponse } from 'next/og'
908
909 // Module-level: runs ONCE when module is first imported
910 const fontData = fetch(
911 new URL('./fonts/Inter.ttf', import.meta.url)
912 ).then(res => res.arrayBuffer())
913
914 const logoData = fetch(
915 new URL('./images/logo.png', import.meta.url)
916 ).then(res => res.arrayBuffer())
917
918 export async function GET(request: Request) {
919 // Await the already-started promises
920 const [font, logo] = await Promise.all([fontData, logoData])
921
922 return new ImageResponse(
923 <div style={{ fontFamily: 'Inter' }}>
924 <img src={logo} />
925 Hello World
926 </div>,
927 { fonts: [{ name: 'Inter', data: font }] }
928 )
929 }
930 ```
931
932 **Correct: synchronous fs at module level**
933
934 ```typescript
935 // app/api/og/route.tsx
936 import { ImageResponse } from 'next/og'
937 import { readFileSync } from 'fs'
938 import { join } from 'path'
939
940 // Synchronous read at module level - blocks only during module init
941 const fontData = readFileSync(
942 join(process.cwd(), 'public/fonts/Inter.ttf')
943 )
944
945 const logoData = readFileSync(
946 join(process.cwd(), 'public/images/logo.png')
947 )
948
949 export async function GET(request: Request) {
950 return new ImageResponse(
951 <div style={{ fontFamily: 'Inter' }}>
952 <img src={logoData} />
953 Hello World
954 </div>,
955 { fonts: [{ name: 'Inter', data: fontData }] }
956 )
957 }
958 ```
959
960 **Incorrect: reads config on every call**
961
962 ```typescript
963 import fs from 'node:fs/promises'
964
965 export async function processRequest(data: Data) {
966 const config = JSON.parse(
967 await fs.readFile('./config.json', 'utf-8')
968 )
969 const template = await fs.readFile('./template.html', 'utf-8')
970
971 return render(template, data, config)
972 }
973 ```
974
975 **Correct: hoists config and template to module level**
976
977 ```typescript
978 import fs from 'node:fs/promises'
979
980 const configPromise = fs
981 .readFile('./config.json', 'utf-8')
982 .then(JSON.parse)
983 const templatePromise = fs.readFile('./template.html', 'utf-8')
984
985 export async function processRequest(data: Data) {
986 const [config, template] = await Promise.all([
987 configPromise,
988 templatePromise,
989 ])
990
991 return render(template, data, config)
992 }
993 ```
994
995 When to use this pattern:
996
997 - Loading fonts for OG image generation
998
999 - Loading static logos, icons, or watermarks
1000
1001 - Reading configuration files that don't change at runtime
1002
1003 - Loading email templates or other static templates
1004
1005 - Any static asset that's the same across all requests
1006
1007 When not to use this pattern:
1008
1009 - Assets that vary per request or user
1010
1011 - Files that may change during runtime (use caching with TTL instead)
1012
1013 - Large files that would consume too much memory if kept loaded
1014
1015 - Sensitive data that shouldn't persist in memory
1016
1017 With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
1018
1019 In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
1020
1021 ### 3.6 Minimize Serialization at RSC Boundaries
1022
1023 **Impact: HIGH (reduces data transfer size)**
1024
1025 The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
1026
1027 **Incorrect: serializes all 50 fields**
1028
1029 ```tsx
1030 async function Page() {
1031 const user = await fetchUser() // 50 fields
1032 return <Profile user={user} />
1033 }
1034
1035 'use client'
1036 function Profile({ user }: { user: User }) {
1037 return <div>{user.name}</div> // uses 1 field
1038 }
1039 ```
1040
1041 **Correct: serializes only 1 field**
1042
1043 ```tsx
1044 async function Page() {
1045 const user = await fetchUser()
1046 return <Profile name={user.name} />
1047 }
1048
1049 'use client'
1050 function Profile({ name }: { name: string }) {
1051 return <div>{name}</div>
1052 }
1053 ```
1054
1055 ### 3.7 Parallel Data Fetching with Component Composition
1056
1057 **Impact: CRITICAL (eliminates server-side waterfalls)**
1058
1059 React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
1060
1061 **Incorrect: Sidebar waits for Page's fetch to complete**
1062
1063 ```tsx
1064 export default async function Page() {
1065 const header = await fetchHeader()
1066 return (
1067 <div>
1068 <div>{header}</div>
1069 <Sidebar />
1070 </div>
1071 )
1072 }
1073
1074 async function Sidebar() {
1075 const items = await fetchSidebarItems()
1076 return <nav>{items.map(renderItem)}</nav>
1077 }
1078 ```
1079
1080 **Correct: both fetch simultaneously**
1081
1082 ```tsx
1083 async function Header() {
1084 const data = await fetchHeader()
1085 return <div>{data}</div>
1086 }
1087
1088 async function Sidebar() {
1089 const items = await fetchSidebarItems()
1090 return <nav>{items.map(renderItem)}</nav>
1091 }
1092
1093 export default function Page() {
1094 return (
1095 <div>
1096 <Header />
1097 <Sidebar />
1098 </div>
1099 )
1100 }
1101 ```
1102
1103 **Alternative with children prop:**
1104
1105 ```tsx
1106 async function Header() {
1107 const data = await fetchHeader()
1108 return <div>{data}</div>
1109 }
1110
1111 async function Sidebar() {
1112 const items = await fetchSidebarItems()
1113 return <nav>{items.map(renderItem)}</nav>
1114 }
1115
1116 function Layout({ children }: { children: ReactNode }) {
1117 return (
1118 <div>
1119 <Header />
1120 {children}
1121 </div>
1122 )
1123 }
1124
1125 export default function Page() {
1126 return (
1127 <Layout>
1128 <Sidebar />
1129 </Layout>
1130 )
1131 }
1132 ```
1133
1134 ### 3.8 Parallel Nested Data Fetching
1135
1136 **Impact: CRITICAL (eliminates server-side waterfalls)**
1137
1138 When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.
1139
1140 **Incorrect: a single slow item blocks all nested fetches**
1141
1142 ```tsx
1143 const chats = await Promise.all(
1144 chatIds.map(id => getChat(id))
1145 )
1146
1147 const chatAuthors = await Promise.all(
1148 chats.map(chat => getUser(chat.author))
1149 )
1150 ```
1151
1152 If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.
1153
1154 **Correct: each item chains its own nested fetch**
1155
1156 ```tsx
1157 const chatAuthors = await Promise.all(
1158 chatIds.map(id => getChat(id).then(chat => getUser(chat.author)))
1159 )
1160 ```
1161
1162 Each item independently chains `getChat` → `getUser`, so a slow chat doesn't block author fetches for the others.
1163
1164 ### 3.9 Per-Request Deduplication with React.cache()
1165
1166 **Impact: MEDIUM (deduplicates within request)**
1167
1168 Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
1169
1170 **Usage:**
1171
1172 ```typescript
1173 import { cache } from 'react'
1174
1175 export const getCurrentUser = cache(async () => {
1176 const session = await auth()
1177 if (!session?.user?.id) return null
1178 return await db.user.findUnique({
1179 where: { id: session.user.id }
1180 })
1181 })
1182 ```
1183
1184 Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
1185
1186 **Avoid inline objects as arguments:**
1187
1188 `React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
1189
1190 **Incorrect: always cache miss**
1191
1192 ```typescript
1193 const getUser = cache(async (params: { uid: number }) => {
1194 return await db.user.findUnique({ where: { id: params.uid } })
1195 })
1196
1197 // Each call creates new object, never hits cache
1198 getUser({ uid: 1 })
1199 getUser({ uid: 1 }) // Cache miss, runs query again
1200 ```
1201
1202 **Correct: cache hit**
1203
1204 ```typescript
1205 const params = { uid: 1 }
1206 getUser(params) // Query runs
1207 getUser(params) // Cache hit (same reference)
1208 ```
1209
1210 If you must pass objects, pass the same reference:
1211
1212 **Next.js-Specific Note:**
1213
1214 In 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:
1215
1216 - Database queries (Prisma, Drizzle, etc.)
1217
1218 - Heavy computations
1219
1220 - Authentication checks
1221
1222 - File system operations
1223
1224 - Any non-fetch async work
1225
1226 Use `React.cache()` to deduplicate these operations across your component tree.
1227
1228 Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
1229
1230 ### 3.10 Use after() for Non-Blocking Operations
1231
1232 **Impact: MEDIUM (faster response times)**
1233
1234 Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
1235
1236 **Incorrect: blocks response**
1237
1238 ```tsx
1239 import { logUserAction } from '@/app/utils'
1240
1241 export async function POST(request: Request) {
1242 // Perform mutation
1243 await updateDatabase(request)
1244
1245 // Logging blocks the response
1246 const userAgent = request.headers.get('user-agent') || 'unknown'
1247 await logUserAction({ userAgent })
1248
1249 return new Response(JSON.stringify({ status: 'success' }), {
1250 status: 200,
1251 headers: { 'Content-Type': 'application/json' }
1252 })
1253 }
1254 ```
1255
1256 **Correct: non-blocking**
1257
1258 ```tsx
1259 import { after } from 'next/server'
1260 import { headers, cookies } from 'next/headers'
1261 import { logUserAction } from '@/app/utils'
1262
1263 export async function POST(request: Request) {
1264 // Perform mutation
1265 await updateDatabase(request)
1266
1267 // Log after response is sent
1268 after(async () => {
1269 const userAgent = (await headers()).get('user-agent') || 'unknown'
1270 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
1271
1272 logUserAction({ sessionCookie, userAgent })
1273 })
1274
1275 return new Response(JSON.stringify({ status: 'success' }), {
1276 status: 200,
1277 headers: { 'Content-Type': 'application/json' }
1278 })
1279 }
1280 ```
1281
1282 The response is sent immediately while logging happens in the background.
1283
1284 **Common use cases:**
1285
1286 - Analytics tracking
1287
1288 - Audit logging
1289
1290 - Sending notifications
1291
1292 - Cache invalidation
1293
1294 - Cleanup tasks
1295
1296 **Important notes:**
1297
1298 - `after()` runs even if the response fails or redirects
1299
1300 - Works in Server Actions, Route Handlers, and Server Components
1301
1302 Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1303
1304 ---
1305
1306 ## 4. Client-Side Data Fetching
1307
1308 **Impact: MEDIUM-HIGH**
1309
1310 Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1311
1312 ### 4.1 Deduplicate Global Event Listeners
1313
1314 **Impact: LOW (single listener for N components)**
1315
1316 Use `useSWRSubscription()` to share global event listeners across component instances.
1317
1318 **Incorrect: N instances = N listeners**
1319
1320 ```tsx
1321 function useKeyboardShortcut(key: string, callback: () => void) {
1322 useEffect(() => {
1323 const handler = (e: KeyboardEvent) => {
1324 if (e.metaKey && e.key === key) {
1325 callback()
1326 }
1327 }
1328 window.addEventListener('keydown', handler)
1329 return () => window.removeEventListener('keydown', handler)
1330 }, [key, callback])
1331 }
1332 ```
1333
1334 When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1335
1336 **Correct: N instances = 1 listener**
1337
1338 ```tsx
1339 import useSWRSubscription from 'swr/subscription'
1340
1341 // Module-level Map to track callbacks per key
1342 const keyCallbacks = new Map<string, Set<() => void>>()
1343
1344 function useKeyboardShortcut(key: string, callback: () => void) {
1345 // Register this callback in the Map
1346 useEffect(() => {
1347 if (!keyCallbacks.has(key)) {
1348 keyCallbacks.set(key, new Set())
1349 }
1350 keyCallbacks.get(key)!.add(callback)
1351
1352 return () => {
1353 const set = keyCallbacks.get(key)
1354 if (set) {
1355 set.delete(callback)
1356 if (set.size === 0) {
1357 keyCallbacks.delete(key)
1358 }
1359 }
1360 }
1361 }, [key, callback])
1362
1363 useSWRSubscription('global-keydown', () => {
1364 const handler = (e: KeyboardEvent) => {
1365 if (e.metaKey && keyCallbacks.has(e.key)) {
1366 keyCallbacks.get(e.key)!.forEach(cb => cb())
1367 }
1368 }
1369 window.addEventListener('keydown', handler)
1370 return () => window.removeEventListener('keydown', handler)
1371 })
1372 }
1373
1374 function Profile() {
1375 // Multiple shortcuts will share the same listener
1376 useKeyboardShortcut('p', () => { /* ... */ })
1377 useKeyboardShortcut('k', () => { /* ... */ })
1378 // ...
1379 }
1380 ```
1381
1382 ### 4.2 Use Passive Event Listeners for Scrolling Performance
1383
1384 **Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
1385
1386 Add `{ 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.
1387
1388 **Incorrect:**
1389
1390 ```typescript
1391 useEffect(() => {
1392 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1393 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1394
1395 document.addEventListener('touchstart', handleTouch)
1396 document.addEventListener('wheel', handleWheel)
1397
1398 return () => {
1399 document.removeEventListener('touchstart', handleTouch)
1400 document.removeEventListener('wheel', handleWheel)
1401 }
1402 }, [])
1403 ```
1404
1405 **Correct:**
1406
1407 ```typescript
1408 useEffect(() => {
1409 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1410 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1411
1412 document.addEventListener('touchstart', handleTouch, { passive: true })
1413 document.addEventListener('wheel', handleWheel, { passive: true })
1414
1415 return () => {
1416 document.removeEventListener('touchstart', handleTouch)
1417 document.removeEventListener('wheel', handleWheel)
1418 }
1419 }, [])
1420 ```
1421
1422 **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1423
1424 **Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1425
1426 ### 4.3 Use SWR for Automatic Deduplication
1427
1428 **Impact: MEDIUM-HIGH (automatic deduplication)**
1429
1430 SWR enables request deduplication, caching, and revalidation across component instances.
1431
1432 **Incorrect: no deduplication, each instance fetches**
1433
1434 ```tsx
1435 function UserList() {
1436 const [users, setUsers] = useState([])
1437 useEffect(() => {
1438 fetch('/api/users')
1439 .then(r => r.json())
1440 .then(setUsers)
1441 }, [])
1442 }
1443 ```
1444
1445 **Correct: multiple instances share one request**
1446
1447 ```tsx
1448 import useSWR from 'swr'
1449
1450 function UserList() {
1451 const { data: users } = useSWR('/api/users', fetcher)
1452 }
1453 ```
1454
1455 **For immutable data:**
1456
1457 ```tsx
1458 import { useImmutableSWR } from '@/lib/swr'
1459
1460 function StaticContent() {
1461 const { data } = useImmutableSWR('/api/config', fetcher)
1462 }
1463 ```
1464
1465 **For mutations:**
1466
1467 ```tsx
1468 import { useSWRMutation } from 'swr/mutation'
1469
1470 function UpdateButton() {
1471 const { trigger } = useSWRMutation('/api/user', updateUser)
1472 return <button onClick={() => trigger()}>Update</button>
1473 }
1474 ```
1475
1476 Reference: [https://swr.vercel.app](https://swr.vercel.app)
1477
1478 ### 4.4 Version and Minimize localStorage Data
1479
1480 **Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
1481
1482 Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1483
1484 **Incorrect:**
1485
1486 ```typescript
1487 // No version, stores everything, no error handling
1488 localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
1489 const data = localStorage.getItem('userConfig')
1490 ```
1491
1492 **Correct:**
1493
1494 ```typescript
1495 const VERSION = 'v2'
1496
1497 function saveConfig(config: { theme: string; language: string }) {
1498 try {
1499 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1500 } catch {
1501 // Throws in incognito/private browsing, quota exceeded, or disabled
1502 }
1503 }
1504
1505 function loadConfig() {
1506 try {
1507 const data = localStorage.getItem(`userConfig:${VERSION}`)
1508 return data ? JSON.parse(data) : null
1509 } catch {
1510 return null
1511 }
1512 }
1513
1514 // Migration from v1 to v2
1515 function migrate() {
1516 try {
1517 const v1 = localStorage.getItem('userConfig:v1')
1518 if (v1) {
1519 const old = JSON.parse(v1)
1520 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
1521 localStorage.removeItem('userConfig:v1')
1522 }
1523 } catch {}
1524 }
1525 ```
1526
1527 **Store minimal fields from server responses:**
1528
1529 ```typescript
1530 // User object has 20+ fields, only store what UI needs
1531 function cachePrefs(user: FullUser) {
1532 try {
1533 localStorage.setItem('prefs:v1', JSON.stringify({
1534 theme: user.preferences.theme,
1535 notifications: user.preferences.notifications
1536 }))
1537 } catch {}
1538 }
1539 ```
1540
1541 **Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1542
1543 **Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1544
1545 ---
1546
1547 ## 5. Re-render Optimization
1548
1549 **Impact: MEDIUM**
1550
1551 Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1552
1553 ### 5.1 Calculate Derived State During Rendering
1554
1555 **Impact: MEDIUM (avoids redundant renders and state drift)**
1556
1557 If 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.
1558
1559 **Incorrect: redundant state and effect**
1560
1561 ```tsx
1562 function Form() {
1563 const [firstName, setFirstName] = useState('First')
1564 const [lastName, setLastName] = useState('Last')
1565 const [fullName, setFullName] = useState('')
1566
1567 useEffect(() => {
1568 setFullName(firstName + ' ' + lastName)
1569 }, [firstName, lastName])
1570
1571 return <p>{fullName}</p>
1572 }
1573 ```
1574
1575 **Correct: derive during render**
1576
1577 ```tsx
1578 function Form() {
1579 const [firstName, setFirstName] = useState('First')
1580 const [lastName, setLastName] = useState('Last')
1581 const fullName = firstName + ' ' + lastName
1582
1583 return <p>{fullName}</p>
1584 }
1585 ```
1586
1587 Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
1588
1589 ### 5.2 Defer State Reads to Usage Point
1590
1591 **Impact: MEDIUM (avoids unnecessary subscriptions)**
1592
1593 Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1594
1595 **Incorrect: subscribes to all searchParams changes**
1596
1597 ```tsx
1598 function ShareButton({ chatId }: { chatId: string }) {
1599 const searchParams = useSearchParams()
1600
1601 const handleShare = () => {
1602 const ref = searchParams.get('ref')
1603 shareChat(chatId, { ref })
1604 }
1605
1606 return <button onClick={handleShare}>Share</button>
1607 }
1608 ```
1609
1610 **Correct: reads on demand, no subscription**
1611
1612 ```tsx
1613 function ShareButton({ chatId }: { chatId: string }) {
1614 const handleShare = () => {
1615 const params = new URLSearchParams(window.location.search)
1616 const ref = params.get('ref')
1617 shareChat(chatId, { ref })
1618 }
1619
1620 return <button onClick={handleShare}>Share</button>
1621 }
1622 ```
1623
1624 ### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
1625
1626 **Impact: LOW-MEDIUM (wasted computation on every render)**
1627
1628 When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
1629
1630 Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1631
1632 **Incorrect:**
1633
1634 ```tsx
1635 function Header({ user, notifications }: Props) {
1636 const isLoading = useMemo(() => {
1637 return user.isLoading || notifications.isLoading
1638 }, [user.isLoading, notifications.isLoading])
1639
1640 if (isLoading) return <Skeleton />
1641 // return some markup
1642 }
1643 ```
1644
1645 **Correct:**
1646
1647 ```tsx
1648 function Header({ user, notifications }: Props) {
1649 const isLoading = user.isLoading || notifications.isLoading
1650
1651 if (isLoading) return <Skeleton />
1652 // return some markup
1653 }
1654 ```
1655
1656 ### 5.4 Don't Define Components Inside Components
1657
1658 **Impact: HIGH (prevents remount on every render)**
1659
1660 Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
1661
1662 A common reason developers do this is to access parent variables without passing props. Always pass props instead.
1663
1664 **Incorrect: remounts on every render**
1665
1666 ```tsx
1667 function UserProfile({ user, theme }) {
1668 // Defined inside to access `theme` - BAD
1669 const Avatar = () => (
1670 <img
1671 src={user.avatarUrl}
1672 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1673 />
1674 )
1675
1676 // Defined inside to access `user` - BAD
1677 const Stats = () => (
1678 <div>
1679 <span>{user.followers} followers</span>
1680 <span>{user.posts} posts</span>
1681 </div>
1682 )
1683
1684 return (
1685 <div>
1686 <Avatar />
1687 <Stats />
1688 </div>
1689 )
1690 }
1691 ```
1692
1693 Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.
1694
1695 **Correct: pass props instead**
1696
1697 ```tsx
1698 function Avatar({ src, theme }: { src: string; theme: string }) {
1699 return (
1700 <img
1701 src={src}
1702 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1703 />
1704 )
1705 }
1706
1707 function Stats({ followers, posts }: { followers: number; posts: number }) {
1708 return (
1709 <div>
1710 <span>{followers} followers</span>
1711 <span>{posts} posts</span>
1712 </div>
1713 )
1714 }
1715
1716 function UserProfile({ user, theme }) {
1717 return (
1718 <div>
1719 <Avatar src={user.avatarUrl} theme={theme} />
1720 <Stats followers={user.followers} posts={user.posts} />
1721 </div>
1722 )
1723 }
1724 ```
1725
1726 **Symptoms of this bug:**
1727
1728 - Input fields lose focus on every keystroke
1729
1730 - Animations restart unexpectedly
1731
1732 - `useEffect` cleanup/setup runs on every parent render
1733
1734 - Scroll position resets inside the component
1735
1736 ### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1737
1738 **Impact: MEDIUM (restores memoization by using a constant for default value)**
1739
1740 When 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()`.
1741
1742 To address this issue, extract the default value into a constant.
1743
1744 **Incorrect: `onClick` has different values on every rerender**
1745
1746 ```tsx
1747 const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1748 // ...
1749 })
1750
1751 // Used without optional onClick
1752 <UserAvatar />
1753 ```
1754
1755 **Correct: stable default value**
1756
1757 ```tsx
1758 const NOOP = () => {};
1759
1760 const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1761 // ...
1762 })
1763
1764 // Used without optional onClick
1765 <UserAvatar />
1766 ```
1767
1768 ### 5.6 Extract to Memoized Components
1769
1770 **Impact: MEDIUM (enables early returns)**
1771
1772 Extract expensive work into memoized components to enable early returns before computation.
1773
1774 **Incorrect: computes avatar even when loading**
1775
1776 ```tsx
1777 function Profile({ user, loading }: Props) {
1778 const avatar = useMemo(() => {
1779 const id = computeAvatarId(user)
1780 return <Avatar id={id} />
1781 }, [user])
1782
1783 if (loading) return <Skeleton />
1784 return <div>{avatar}</div>
1785 }
1786 ```
1787
1788 **Correct: skips computation when loading**
1789
1790 ```tsx
1791 const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1792 const id = useMemo(() => computeAvatarId(user), [user])
1793 return <Avatar id={id} />
1794 })
1795
1796 function Profile({ user, loading }: Props) {
1797 if (loading) return <Skeleton />
1798 return (
1799 <div>
1800 <UserAvatar user={user} />
1801 </div>
1802 )
1803 }
1804 ```
1805
1806 **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.
1807
1808 ### 5.7 Narrow Effect Dependencies
1809
1810 **Impact: LOW (minimizes effect re-runs)**
1811
1812 Specify primitive dependencies instead of objects to minimize effect re-runs.
1813
1814 **Incorrect: re-runs on any user field change**
1815
1816 ```tsx
1817 useEffect(() => {
1818 console.log(user.id)
1819 }, [user])
1820 ```
1821
1822 **Correct: re-runs only when id changes**
1823
1824 ```tsx
1825 useEffect(() => {
1826 console.log(user.id)
1827 }, [user.id])
1828 ```
1829
1830 **For derived state, compute outside effect:**
1831
1832 ```tsx
1833 // Incorrect: runs on width=767, 766, 765...
1834 useEffect(() => {
1835 if (width < 768) {
1836 enableMobileMode()
1837 }
1838 }, [width])
1839
1840 // Correct: runs only on boolean transition
1841 const isMobile = width < 768
1842 useEffect(() => {
1843 if (isMobile) {
1844 enableMobileMode()
1845 }
1846 }, [isMobile])
1847 ```
1848
1849 ### 5.8 Put Interaction Logic in Event Handlers
1850
1851 **Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
1852
1853 If 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.
1854
1855 **Incorrect: event modeled as state + effect**
1856
1857 ```tsx
1858 function Form() {
1859 const [submitted, setSubmitted] = useState(false)
1860 const theme = useContext(ThemeContext)
1861
1862 useEffect(() => {
1863 if (submitted) {
1864 post('/api/register')
1865 showToast('Registered', theme)
1866 }
1867 }, [submitted, theme])
1868
1869 return <button onClick={() => setSubmitted(true)}>Submit</button>
1870 }
1871 ```
1872
1873 **Correct: do it in the handler**
1874
1875 ```tsx
1876 function Form() {
1877 const theme = useContext(ThemeContext)
1878
1879 function handleSubmit() {
1880 post('/api/register')
1881 showToast('Registered', theme)
1882 }
1883
1884 return <button onClick={handleSubmit}>Submit</button>
1885 }
1886 ```
1887
1888 Reference: [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)
1889
1890 ### 5.9 Split Combined Hook Computations
1891
1892 **Impact: MEDIUM (avoids recomputing independent steps)**
1893
1894 When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.
1895
1896 **Incorrect: changing `sortOrder` recomputes filtering**
1897
1898 ```tsx
1899 const sortedProducts = useMemo(() => {
1900 const filtered = products.filter((p) => p.category === category)
1901 const sorted = filtered.toSorted((a, b) =>
1902 sortOrder === "asc" ? a.price - b.price : b.price - a.price
1903 )
1904 return sorted
1905 }, [products, category, sortOrder])
1906 ```
1907
1908 **Correct: filtering only recomputes when products or category change**
1909
1910 ```tsx
1911 const filteredProducts = useMemo(
1912 () => products.filter((p) => p.category === category),
1913 [products, category]
1914 )
1915
1916 const sortedProducts = useMemo(
1917 () =>
1918 filteredProducts.toSorted((a, b) =>
1919 sortOrder === "asc" ? a.price - b.price : b.price - a.price
1920 ),
1921 [filteredProducts, sortOrder]
1922 )
1923 ```
1924
1925 This pattern also applies to `useEffect` when combining unrelated side effects:
1926
1927 **Incorrect: both effects run when either dependency changes**
1928
1929 ```tsx
1930 useEffect(() => {
1931 analytics.trackPageView(pathname)
1932 document.title = `${pageTitle} | My App`
1933 }, [pathname, pageTitle])
1934 ```
1935
1936 **Correct: effects run independently**
1937
1938 ```tsx
1939 useEffect(() => {
1940 analytics.trackPageView(pathname)
1941 }, [pathname])
1942
1943 useEffect(() => {
1944 document.title = `${pageTitle} | My App`
1945 }, [pageTitle])
1946 ```
1947
1948 **Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.
1949
1950 ### 5.10 Subscribe to Derived State
1951
1952 **Impact: MEDIUM (reduces re-render frequency)**
1953
1954 Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1955
1956 **Incorrect: re-renders on every pixel change**
1957
1958 ```tsx
1959 function Sidebar() {
1960 const width = useWindowWidth() // updates continuously
1961 const isMobile = width < 768
1962 return <nav className={isMobile ? 'mobile' : 'desktop'} />
1963 }
1964 ```
1965
1966 **Correct: re-renders only when boolean changes**
1967
1968 ```tsx
1969 function Sidebar() {
1970 const isMobile = useMediaQuery('(max-width: 767px)')
1971 return <nav className={isMobile ? 'mobile' : 'desktop'} />
1972 }
1973 ```
1974
1975 ### 5.11 Use Functional setState Updates
1976
1977 **Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
1978
1979 When 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.
1980
1981 **Incorrect: requires state as dependency**
1982
1983 ```tsx
1984 function TodoList() {
1985 const [items, setItems] = useState(initialItems)
1986
1987 // Callback must depend on items, recreated on every items change
1988 const addItems = useCallback((newItems: Item[]) => {
1989 setItems([...items, ...newItems])
1990 }, [items]) // ❌ items dependency causes recreations
1991
1992 // Risk of stale closure if dependency is forgotten
1993 const removeItem = useCallback((id: string) => {
1994 setItems(items.filter(item => item.id !== id))
1995 }, []) // ❌ Missing items dependency - will use stale items!
1996
1997 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1998 }
1999 ```
2000
2001 The 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.
2002
2003 **Correct: stable callbacks, no stale closures**
2004
2005 ```tsx
2006 function TodoList() {
2007 const [items, setItems] = useState(initialItems)
2008
2009 // Stable callback, never recreated
2010 const addItems = useCallback((newItems: Item[]) => {
2011 setItems(curr => [...curr, ...newItems])
2012 }, []) // ✅ No dependencies needed
2013
2014 // Always uses latest state, no stale closure risk
2015 const removeItem = useCallback((id: string) => {
2016 setItems(curr => curr.filter(item => item.id !== id))
2017 }, []) // ✅ Safe and stable
2018
2019 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
2020 }
2021 ```
2022
2023 **Benefits:**
2024
2025 1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2026
2027 2. **No stale closures** - Always operates on the latest state value
2028
2029 3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
2030
2031 4. **Prevents bugs** - Eliminates the most common source of React closure bugs
2032
2033 **When to use functional updates:**
2034
2035 - Any setState that depends on the current state value
2036
2037 - Inside useCallback/useMemo when state is needed
2038
2039 - Event handlers that reference state
2040
2041 - Async operations that update state
2042
2043 **When direct updates are fine:**
2044
2045 - Setting state to a static value: `setCount(0)`
2046
2047 - Setting state from props/arguments only: `setName(newName)`
2048
2049 - State doesn't depend on previous value
2050
2051 **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.
2052
2053 ### 5.12 Use Lazy State Initialization
2054
2055 **Impact: MEDIUM (wasted computation on every render)**
2056
2057 Pass 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.
2058
2059 **Incorrect: runs on every render**
2060
2061 ```tsx
2062 function FilteredList({ items }: { items: Item[] }) {
2063 // buildSearchIndex() runs on EVERY render, even after initialization
2064 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
2065 const [query, setQuery] = useState('')
2066
2067 // When query changes, buildSearchIndex runs again unnecessarily
2068 return <SearchResults index={searchIndex} query={query} />
2069 }
2070
2071 function UserProfile() {
2072 // JSON.parse runs on every render
2073 const [settings, setSettings] = useState(
2074 JSON.parse(localStorage.getItem('settings') || '{}')
2075 )
2076
2077 return <SettingsForm settings={settings} onChange={setSettings} />
2078 }
2079 ```
2080
2081 **Correct: runs only once**
2082
2083 ```tsx
2084 function FilteredList({ items }: { items: Item[] }) {
2085 // buildSearchIndex() runs ONLY on initial render
2086 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
2087 const [query, setQuery] = useState('')
2088
2089 return <SearchResults index={searchIndex} query={query} />
2090 }
2091
2092 function UserProfile() {
2093 // JSON.parse runs only on initial render
2094 const [settings, setSettings] = useState(() => {
2095 const stored = localStorage.getItem('settings')
2096 return stored ? JSON.parse(stored) : {}
2097 })
2098
2099 return <SettingsForm settings={settings} onChange={setSettings} />
2100 }
2101 ```
2102
2103 Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
2104
2105 For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
2106
2107 ### 5.13 Use Transitions for Non-Urgent Updates
2108
2109 **Impact: MEDIUM (maintains UI responsiveness)**
2110
2111 Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
2112
2113 **Incorrect: blocks UI on every scroll**
2114
2115 ```tsx
2116 function ScrollTracker() {
2117 const [scrollY, setScrollY] = useState(0)
2118 useEffect(() => {
2119 const handler = () => setScrollY(window.scrollY)
2120 window.addEventListener('scroll', handler, { passive: true })
2121 return () => window.removeEventListener('scroll', handler)
2122 }, [])
2123 }
2124 ```
2125
2126 **Correct: non-blocking updates**
2127
2128 ```tsx
2129 import { startTransition } from 'react'
2130
2131 function ScrollTracker() {
2132 const [scrollY, setScrollY] = useState(0)
2133 useEffect(() => {
2134 const handler = () => {
2135 startTransition(() => setScrollY(window.scrollY))
2136 }
2137 window.addEventListener('scroll', handler, { passive: true })
2138 return () => window.removeEventListener('scroll', handler)
2139 }, [])
2140 }
2141 ```
2142
2143 ### 5.14 Use useDeferredValue for Expensive Derived Renders
2144
2145 **Impact: MEDIUM (keeps input responsive during heavy computation)**
2146
2147 When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.
2148
2149 **Incorrect: input feels laggy while filtering**
2150
2151 ```tsx
2152 function Search({ items }: { items: Item[] }) {
2153 const [query, setQuery] = useState('')
2154 const filtered = items.filter(item => fuzzyMatch(item, query))
2155
2156 return (
2157 <>
2158 <input value={query} onChange={e => setQuery(e.target.value)} />
2159 <ResultsList results={filtered} />
2160 </>
2161 )
2162 }
2163 ```
2164
2165 **Correct: input stays snappy, results render when ready**
2166
2167 ```tsx
2168 function Search({ items }: { items: Item[] }) {
2169 const [query, setQuery] = useState('')
2170 const deferredQuery = useDeferredValue(query)
2171 const filtered = useMemo(
2172 () => items.filter(item => fuzzyMatch(item, deferredQuery)),
2173 [items, deferredQuery]
2174 )
2175 const isStale = query !== deferredQuery
2176
2177 return (
2178 <>
2179 <input value={query} onChange={e => setQuery(e.target.value)} />
2180 <div style={{ opacity: isStale ? 0.7 : 1 }}>
2181 <ResultsList results={filtered} />
2182 </div>
2183 </>
2184 )
2185 }
2186 ```
2187
2188 **When to use:**
2189
2190 - Filtering/searching large lists
2191
2192 - Expensive visualizations (charts, graphs) reacting to input
2193
2194 - Any derived state that causes noticeable render delays
2195
2196 **Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.
2197
2198 Reference: [https://react.dev/reference/react/useDeferredValue](https://react.dev/reference/react/useDeferredValue)
2199
2200 ### 5.15 Use useRef for Transient Values
2201
2202 **Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
2203
2204 When 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.
2205
2206 **Incorrect: renders every update**
2207
2208 ```tsx
2209 function Tracker() {
2210 const [lastX, setLastX] = useState(0)
2211
2212 useEffect(() => {
2213 const onMove = (e: MouseEvent) => setLastX(e.clientX)
2214 window.addEventListener('mousemove', onMove)
2215 return () => window.removeEventListener('mousemove', onMove)
2216 }, [])
2217
2218 return (
2219 <div
2220 style={{
2221 position: 'fixed',
2222 top: 0,
2223 left: lastX,
2224 width: 8,
2225 height: 8,
2226 background: 'black',
2227 }}
2228 />
2229 )
2230 }
2231 ```
2232
2233 **Correct: no re-render for tracking**
2234
2235 ```tsx
2236 function Tracker() {
2237 const lastXRef = useRef(0)
2238 const dotRef = useRef<HTMLDivElement>(null)
2239
2240 useEffect(() => {
2241 const onMove = (e: MouseEvent) => {
2242 lastXRef.current = e.clientX
2243 const node = dotRef.current
2244 if (node) {
2245 node.style.transform = `translateX(${e.clientX}px)`
2246 }
2247 }
2248 window.addEventListener('mousemove', onMove)
2249 return () => window.removeEventListener('mousemove', onMove)
2250 }, [])
2251
2252 return (
2253 <div
2254 ref={dotRef}
2255 style={{
2256 position: 'fixed',
2257 top: 0,
2258 left: 0,
2259 width: 8,
2260 height: 8,
2261 background: 'black',
2262 transform: 'translateX(0px)',
2263 }}
2264 />
2265 )
2266 }
2267 ```
2268
2269 ---
2270
2271 ## 6. Rendering Performance
2272
2273 **Impact: MEDIUM**
2274
2275 Optimizing the rendering process reduces the work the browser needs to do.
2276
2277 ### 6.1 Animate SVG Wrapper Instead of SVG Element
2278
2279 **Impact: LOW (enables hardware acceleration)**
2280
2281 Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
2282
2283 **Incorrect: animating SVG directly - no hardware acceleration**
2284
2285 ```tsx
2286 function LoadingSpinner() {
2287 return (
2288 <svg
2289 className="animate-spin"
2290 width="24"
2291 height="24"
2292 viewBox="0 0 24 24"
2293 >
2294 <circle cx="12" cy="12" r="10" stroke="currentColor" />
2295 </svg>
2296 )
2297 }
2298 ```
2299
2300 **Correct: animating wrapper div - hardware accelerated**
2301
2302 ```tsx
2303 function LoadingSpinner() {
2304 return (
2305 <div className="animate-spin">
2306 <svg
2307 width="24"
2308 height="24"
2309 viewBox="0 0 24 24"
2310 >
2311 <circle cx="12" cy="12" r="10" stroke="currentColor" />
2312 </svg>
2313 </div>
2314 )
2315 }
2316 ```
2317
2318 This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
2319
2320 ### 6.2 CSS content-visibility for Long Lists
2321
2322 **Impact: HIGH (faster initial render)**
2323
2324 Apply `content-visibility: auto` to defer off-screen rendering.
2325
2326 **CSS:**
2327
2328 ```css
2329 .message-item {
2330 content-visibility: auto;
2331 contain-intrinsic-size: 0 80px;
2332 }
2333 ```
2334
2335 **Example:**
2336
2337 ```tsx
2338 function MessageList({ messages }: { messages: Message[] }) {
2339 return (
2340 <div className="overflow-y-auto h-screen">
2341 {messages.map(msg => (
2342 <div key={msg.id} className="message-item">
2343 <Avatar user={msg.author} />
2344 <div>{msg.content}</div>
2345 </div>
2346 ))}
2347 </div>
2348 )
2349 }
2350 ```
2351
2352 For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
2353
2354 ### 6.3 Hoist Static JSX Elements
2355
2356 **Impact: LOW (avoids re-creation)**
2357
2358 Extract static JSX outside components to avoid re-creation.
2359
2360 **Incorrect: recreates element every render**
2361
2362 ```tsx
2363 function LoadingSkeleton() {
2364 return <div className="animate-pulse h-20 bg-gray-200" />
2365 }
2366
2367 function Container() {
2368 return (
2369 <div>
2370 {loading && <LoadingSkeleton />}
2371 </div>
2372 )
2373 }
2374 ```
2375
2376 **Correct: reuses same element**
2377
2378 ```tsx
2379 const loadingSkeleton = (
2380 <div className="animate-pulse h-20 bg-gray-200" />
2381 )
2382
2383 function Container() {
2384 return (
2385 <div>
2386 {loading && loadingSkeleton}
2387 </div>
2388 )
2389 }
2390 ```
2391
2392 This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
2393
2394 **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.
2395
2396 ### 6.4 Optimize SVG Precision
2397
2398 **Impact: LOW (reduces file size)**
2399
2400 Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
2401
2402 **Incorrect: excessive precision**
2403
2404 ```svg
2405 <path d="M 10.293847 20.847362 L 30.938472 40.192837" />
2406 ```
2407
2408 **Correct: 1 decimal place**
2409
2410 ```svg
2411 <path d="M 10.3 20.8 L 30.9 40.2" />
2412 ```
2413
2414 **Automate with SVGO:**
2415
2416 ```bash
2417 npx svgo --precision=1 --multipass icon.svg
2418 ```
2419
2420 ### 6.5 Prevent Hydration Mismatch Without Flickering
2421
2422 **Impact: MEDIUM (avoids visual flicker and hydration errors)**
2423
2424 When 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.
2425
2426 **Incorrect: breaks SSR**
2427
2428 ```tsx
2429 function ThemeWrapper({ children }: { children: ReactNode }) {
2430 // localStorage is not available on server - throws error
2431 const theme = localStorage.getItem('theme') || 'light'
2432
2433 return (
2434 <div className={theme}>
2435 {children}
2436 </div>
2437 )
2438 }
2439 ```
2440
2441 Server-side rendering will fail because `localStorage` is undefined.
2442
2443 **Incorrect: visual flickering**
2444
2445 ```tsx
2446 function ThemeWrapper({ children }: { children: ReactNode }) {
2447 const [theme, setTheme] = useState('light')
2448
2449 useEffect(() => {
2450 // Runs after hydration - causes visible flash
2451 const stored = localStorage.getItem('theme')
2452 if (stored) {
2453 setTheme(stored)
2454 }
2455 }, [])
2456
2457 return (
2458 <div className={theme}>
2459 {children}
2460 </div>
2461 )
2462 }
2463 ```
2464
2465 Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
2466
2467 **Correct: no flicker, no hydration mismatch**
2468
2469 ```tsx
2470 function ThemeWrapper({ children }: { children: ReactNode }) {
2471 return (
2472 <>
2473 <div id="theme-wrapper">
2474 {children}
2475 </div>
2476 <script
2477 dangerouslySetInnerHTML={{
2478 __html: `
2479 (function() {
2480 try {
2481 var theme = localStorage.getItem('theme') || 'light';
2482 var el = document.getElementById('theme-wrapper');
2483 if (el) el.className = theme;
2484 } catch (e) {}
2485 })();
2486 `,
2487 }}
2488 />
2489 </>
2490 )
2491 }
2492 ```
2493
2494 The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
2495
2496 This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
2497
2498 ### 6.6 Suppress Expected Hydration Mismatches
2499
2500 **Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
2501
2502 In 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.
2503
2504 **Incorrect: known mismatch warnings**
2505
2506 ```tsx
2507 function Timestamp() {
2508 return <span>{new Date().toLocaleString()}</span>
2509 }
2510 ```
2511
2512 **Correct: suppress expected mismatch only**
2513
2514 ```tsx
2515 function Timestamp() {
2516 return (
2517 <span suppressHydrationWarning>
2518 {new Date().toLocaleString()}
2519 </span>
2520 )
2521 }
2522 ```
2523
2524 ### 6.7 Use Activity Component for Show/Hide
2525
2526 **Impact: MEDIUM (preserves state/DOM)**
2527
2528 Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2529
2530 **Usage:**
2531
2532 ```tsx
2533 import { Activity } from 'react'
2534
2535 function Dropdown({ isOpen }: Props) {
2536 return (
2537 <Activity mode={isOpen ? 'visible' : 'hidden'}>
2538 <ExpensiveMenu />
2539 </Activity>
2540 )
2541 }
2542 ```
2543
2544 Avoids expensive re-renders and state loss.
2545
2546 ### 6.8 Use defer or async on Script Tags
2547
2548 **Impact: HIGH (eliminates render-blocking)**
2549
2550 Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
2551
2552 - **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
2553
2554 - **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
2555
2556 Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
2557
2558 **Incorrect: blocks rendering**
2559
2560 ```tsx
2561 export default function Document() {
2562 return (
2563 <html>
2564 <head>
2565 <script src="https://example.com/analytics.js" />
2566 <script src="/scripts/utils.js" />
2567 </head>
2568 <body>{/* content */}</body>
2569 </html>
2570 )
2571 }
2572 ```
2573
2574 **Correct: non-blocking**
2575
2576 ```tsx
2577 import Script from 'next/script'
2578
2579 export default function Page() {
2580 return (
2581 <>
2582 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />
2583 <Script src="/scripts/utils.js" strategy="beforeInteractive" />
2584 </>
2585 )
2586 }
2587 ```
2588
2589 **Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
2590
2591 Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
2592
2593 ### 6.9 Use Explicit Conditional Rendering
2594
2595 **Impact: LOW (prevents rendering 0 or NaN)**
2596
2597 Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2598
2599 **Incorrect: renders "0" when count is 0**
2600
2601 ```tsx
2602 function Badge({ count }: { count: number }) {
2603 return (
2604 <div>
2605 {count && <span className="badge">{count}</span>}
2606 </div>
2607 )
2608 }
2609
2610 // When count = 0, renders: <div>0</div>
2611 // When count = 5, renders: <div><span class="badge">5</span></div>
2612 ```
2613
2614 **Correct: renders nothing when count is 0**
2615
2616 ```tsx
2617 function Badge({ count }: { count: number }) {
2618 return (
2619 <div>
2620 {count > 0 ? <span className="badge">{count}</span> : null}
2621 </div>
2622 )
2623 }
2624
2625 // When count = 0, renders: <div></div>
2626 // When count = 5, renders: <div><span class="badge">5</span></div>
2627 ```
2628
2629 ### 6.10 Use React DOM Resource Hints
2630
2631 **Impact: HIGH (reduces load time for critical resources)**
2632
2633 React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
2634
2635 - **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
2636
2637 - **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
2638
2639 - **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
2640
2641 - **`preloadModule(href)`**: Fetch an ES module you'll use soon
2642
2643 - **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
2644
2645 - **`preinitModule(href)`**: Fetch and evaluate an ES module
2646
2647 **Example: preconnect to third-party APIs**
2648
2649 ```tsx
2650 import { preconnect, prefetchDNS } from 'react-dom'
2651
2652 export default function App() {
2653 prefetchDNS('https://analytics.example.com')
2654 preconnect('https://api.example.com')
2655
2656 return <main>{/* content */}</main>
2657 }
2658 ```
2659
2660 **Example: preload critical fonts and styles**
2661
2662 ```tsx
2663 import { preload, preinit } from 'react-dom'
2664
2665 export default function RootLayout({ children }) {
2666 // Preload font file
2667 preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
2668
2669 // Fetch and apply critical stylesheet immediately
2670 preinit('/styles/critical.css', { as: 'style' })
2671
2672 return (
2673 <html>
2674 <body>{children}</body>
2675 </html>
2676 )
2677 }
2678 ```
2679
2680 **Example: preload modules for code-split routes**
2681
2682 ```tsx
2683 import { preloadModule, preinitModule } from 'react-dom'
2684
2685 function Navigation() {
2686 const preloadDashboard = () => {
2687 preloadModule('/dashboard.js', { as: 'script' })
2688 }
2689
2690 return (
2691 <nav>
2692 <a href="/dashboard" onMouseEnter={preloadDashboard}>
2693 Dashboard
2694 </a>
2695 </nav>
2696 )
2697 }
2698 ```
2699
2700 **When to use each:**
2701
2702 | API | Use case |
2703
2704 |-----|----------|
2705
2706 | `prefetchDNS` | Third-party domains you'll connect to later |
2707
2708 | `preconnect` | APIs or CDNs you'll fetch from immediately |
2709
2710 | `preload` | Critical resources needed for current page |
2711
2712 | `preloadModule` | JS modules for likely next navigation |
2713
2714 | `preinit` | Stylesheets/scripts that must execute early |
2715
2716 | `preinitModule` | ES modules that must execute early |
2717
2718 Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)
2719
2720 ### 6.11 Use useTransition Over Manual Loading States
2721
2722 **Impact: LOW (reduces re-renders and improves code clarity)**
2723
2724 Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2725
2726 **Incorrect: manual loading state**
2727
2728 ```tsx
2729 function SearchResults() {
2730 const [query, setQuery] = useState('')
2731 const [results, setResults] = useState([])
2732 const [isLoading, setIsLoading] = useState(false)
2733
2734 const handleSearch = async (value: string) => {
2735 setIsLoading(true)
2736 setQuery(value)
2737 const data = await fetchResults(value)
2738 setResults(data)
2739 setIsLoading(false)
2740 }
2741
2742 return (
2743 <>
2744 <input onChange={(e) => handleSearch(e.target.value)} />
2745 {isLoading && <Spinner />}
2746 <ResultsList results={results} />
2747 </>
2748 )
2749 }
2750 ```
2751
2752 **Correct: useTransition with built-in pending state**
2753
2754 ```tsx
2755 import { useTransition, useState } from 'react'
2756
2757 function SearchResults() {
2758 const [query, setQuery] = useState('')
2759 const [results, setResults] = useState([])
2760 const [isPending, startTransition] = useTransition()
2761
2762 const handleSearch = (value: string) => {
2763 setQuery(value) // Update input immediately
2764
2765 startTransition(async () => {
2766 // Fetch and update results
2767 const data = await fetchResults(value)
2768 setResults(data)
2769 })
2770 }
2771
2772 return (
2773 <>
2774 <input onChange={(e) => handleSearch(e.target.value)} />
2775 {isPending && <Spinner />}
2776 <ResultsList results={results} />
2777 </>
2778 )
2779 }
2780 ```
2781
2782 **Benefits:**
2783
2784 - **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2785
2786 - **Error resilience**: Pending state correctly resets even if the transition throws
2787
2788 - **Better responsiveness**: Keeps the UI responsive during updates
2789
2790 - **Interrupt handling**: New transitions automatically cancel pending ones
2791
2792 Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
2793
2794 ---
2795
2796 ## 7. JavaScript Performance
2797
2798 **Impact: LOW-MEDIUM**
2799
2800 Micro-optimizations for hot paths can add up to meaningful improvements.
2801
2802 ### 7.1 Avoid Layout Thrashing
2803
2804 **Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
2805
2806 Avoid 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.
2807
2808 **This is OK: browser batches style changes**
2809
2810 ```typescript
2811 function updateElementStyles(element: HTMLElement) {
2812 // Each line invalidates style, but browser batches the recalculation
2813 element.style.width = '100px'
2814 element.style.height = '200px'
2815 element.style.backgroundColor = 'blue'
2816 element.style.border = '1px solid black'
2817 }
2818 ```
2819
2820 **Incorrect: interleaved reads and writes force reflows**
2821
2822 ```typescript
2823 function layoutThrashing(element: HTMLElement) {
2824 element.style.width = '100px'
2825 const width = element.offsetWidth // Forces reflow
2826 element.style.height = '200px'
2827 const height = element.offsetHeight // Forces another reflow
2828 }
2829 ```
2830
2831 **Correct: batch writes, then read once**
2832
2833 ```typescript
2834 function updateElementStyles(element: HTMLElement) {
2835 // Batch all writes together
2836 element.style.width = '100px'
2837 element.style.height = '200px'
2838 element.style.backgroundColor = 'blue'
2839 element.style.border = '1px solid black'
2840
2841 // Read after all writes are done (single reflow)
2842 const { width, height } = element.getBoundingClientRect()
2843 }
2844 ```
2845
2846 **Correct: batch reads, then writes**
2847
2848 ```typescript
2849 function updateElementStyles(element: HTMLElement) {
2850 element.classList.add('highlighted-box')
2851
2852 const { width, height } = element.getBoundingClientRect()
2853 }
2854 ```
2855
2856 **Better: use CSS classes**
2857
2858 **React example:**
2859
2860 ```tsx
2861 // Incorrect: interleaving style changes with layout queries
2862 function Box({ isHighlighted }: { isHighlighted: boolean }) {
2863 const ref = useRef<HTMLDivElement>(null)
2864
2865 useEffect(() => {
2866 if (ref.current && isHighlighted) {
2867 ref.current.style.width = '100px'
2868 const width = ref.current.offsetWidth // Forces layout
2869 ref.current.style.height = '200px'
2870 }
2871 }, [isHighlighted])
2872
2873 return <div ref={ref}>Content</div>
2874 }
2875
2876 // Correct: toggle class
2877 function Box({ isHighlighted }: { isHighlighted: boolean }) {
2878 return (
2879 <div className={isHighlighted ? 'highlighted-box' : ''}>
2880 Content
2881 </div>
2882 )
2883 }
2884 ```
2885
2886 Prefer 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.
2887
2888 See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2889
2890 ### 7.2 Build Index Maps for Repeated Lookups
2891
2892 **Impact: LOW-MEDIUM (1M ops to 2K ops)**
2893
2894 Multiple `.find()` calls by the same key should use a Map.
2895
2896 **Incorrect (O(n) per lookup):**
2897
2898 ```typescript
2899 function processOrders(orders: Order[], users: User[]) {
2900 return orders.map(order => ({
2901 ...order,
2902 user: users.find(u => u.id === order.userId)
2903 }))
2904 }
2905 ```
2906
2907 **Correct (O(1) per lookup):**
2908
2909 ```typescript
2910 function processOrders(orders: Order[], users: User[]) {
2911 const userById = new Map(users.map(u => [u.id, u]))
2912
2913 return orders.map(order => ({
2914 ...order,
2915 user: userById.get(order.userId)
2916 }))
2917 }
2918 ```
2919
2920 Build map once (O(n)), then all lookups are O(1).
2921
2922 For 1000 orders × 1000 users: 1M ops → 2K ops.
2923
2924 ### 7.3 Cache Property Access in Loops
2925
2926 **Impact: LOW-MEDIUM (reduces lookups)**
2927
2928 Cache object property lookups in hot paths.
2929
2930 **Incorrect: 3 lookups × N iterations**
2931
2932 ```typescript
2933 for (let i = 0; i < arr.length; i++) {
2934 process(obj.config.settings.value)
2935 }
2936 ```
2937
2938 **Correct: 1 lookup total**
2939
2940 ```typescript
2941 const value = obj.config.settings.value
2942 const len = arr.length
2943 for (let i = 0; i < len; i++) {
2944 process(value)
2945 }
2946 ```
2947
2948 ### 7.4 Cache Repeated Function Calls
2949
2950 **Impact: MEDIUM (avoid redundant computation)**
2951
2952 Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2953
2954 **Incorrect: redundant computation**
2955
2956 ```typescript
2957 function ProjectList({ projects }: { projects: Project[] }) {
2958 return (
2959 <div>
2960 {projects.map(project => {
2961 // slugify() called 100+ times for same project names
2962 const slug = slugify(project.name)
2963
2964 return <ProjectCard key={project.id} slug={slug} />
2965 })}
2966 </div>
2967 )
2968 }
2969 ```
2970
2971 **Correct: cached results**
2972
2973 ```typescript
2974 // Module-level cache
2975 const slugifyCache = new Map<string, string>()
2976
2977 function cachedSlugify(text: string): string {
2978 if (slugifyCache.has(text)) {
2979 return slugifyCache.get(text)!
2980 }
2981 const result = slugify(text)
2982 slugifyCache.set(text, result)
2983 return result
2984 }
2985
2986 function ProjectList({ projects }: { projects: Project[] }) {
2987 return (
2988 <div>
2989 {projects.map(project => {
2990 // Computed only once per unique project name
2991 const slug = cachedSlugify(project.name)
2992
2993 return <ProjectCard key={project.id} slug={slug} />
2994 })}
2995 </div>
2996 )
2997 }
2998 ```
2999
3000 **Simpler pattern for single-value functions:**
3001
3002 ```typescript
3003 let isLoggedInCache: boolean | null = null
3004
3005 function isLoggedIn(): boolean {
3006 if (isLoggedInCache !== null) {
3007 return isLoggedInCache
3008 }
3009
3010 isLoggedInCache = document.cookie.includes('auth=')
3011 return isLoggedInCache
3012 }
3013
3014 // Clear cache when auth changes
3015 function onAuthChange() {
3016 isLoggedInCache = null
3017 }
3018 ```
3019
3020 Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
3021
3022 Reference: [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)
3023
3024 ### 7.5 Cache Storage API Calls
3025
3026 **Impact: LOW-MEDIUM (reduces expensive I/O)**
3027
3028 `localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
3029
3030 **Incorrect: reads storage on every call**
3031
3032 ```typescript
3033 function getTheme() {
3034 return localStorage.getItem('theme') ?? 'light'
3035 }
3036 // Called 10 times = 10 storage reads
3037 ```
3038
3039 **Correct: Map cache**
3040
3041 ```typescript
3042 const storageCache = new Map<string, string | null>()
3043
3044 function getLocalStorage(key: string) {
3045 if (!storageCache.has(key)) {
3046 storageCache.set(key, localStorage.getItem(key))
3047 }
3048 return storageCache.get(key)
3049 }
3050
3051 function setLocalStorage(key: string, value: string) {
3052 localStorage.setItem(key, value)
3053 storageCache.set(key, value) // keep cache in sync
3054 }
3055 ```
3056
3057 Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
3058
3059 **Cookie caching:**
3060
3061 ```typescript
3062 let cookieCache: Record<string, string> | null = null
3063
3064 function getCookie(name: string) {
3065 if (!cookieCache) {
3066 cookieCache = Object.fromEntries(
3067 document.cookie.split('; ').map(c => c.split('='))
3068 )
3069 }
3070 return cookieCache[name]
3071 }
3072 ```
3073
3074 **Important: invalidate on external changes**
3075
3076 ```typescript
3077 window.addEventListener('storage', (e) => {
3078 if (e.key) storageCache.delete(e.key)
3079 })
3080
3081 document.addEventListener('visibilitychange', () => {
3082 if (document.visibilityState === 'visible') {
3083 storageCache.clear()
3084 }
3085 })
3086 ```
3087
3088 If storage can change externally (another tab, server-set cookies), invalidate cache:
3089
3090 ### 7.6 Combine Multiple Array Iterations
3091
3092 **Impact: LOW-MEDIUM (reduces iterations)**
3093
3094 Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
3095
3096 **Incorrect: 3 iterations**
3097
3098 ```typescript
3099 const admins = users.filter(u => u.isAdmin)
3100 const testers = users.filter(u => u.isTester)
3101 const inactive = users.filter(u => !u.isActive)
3102 ```
3103
3104 **Correct: 1 iteration**
3105
3106 ```typescript
3107 const admins: User[] = []
3108 const testers: User[] = []
3109 const inactive: User[] = []
3110
3111 for (const user of users) {
3112 if (user.isAdmin) admins.push(user)
3113 if (user.isTester) testers.push(user)
3114 if (!user.isActive) inactive.push(user)
3115 }
3116 ```
3117
3118 ### 7.7 Defer Non-Critical Work with requestIdleCallback
3119
3120 **Impact: MEDIUM (keeps UI responsive during background tasks)**
3121
3122 Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.
3123
3124 **Incorrect: blocks main thread during user interaction**
3125
3126 ```typescript
3127 function handleSearch(query: string) {
3128 const results = searchItems(query)
3129 setResults(results)
3130
3131 // These block the main thread immediately
3132 analytics.track('search', { query })
3133 saveToRecentSearches(query)
3134 prefetchTopResults(results.slice(0, 3))
3135 }
3136 ```
3137
3138 **Correct: defers non-critical work to idle time**
3139
3140 ```typescript
3141 function handleSearch(query: string) {
3142 const results = searchItems(query)
3143 setResults(results)
3144
3145 // Defer non-critical work to idle periods
3146 requestIdleCallback(() => {
3147 analytics.track('search', { query })
3148 })
3149
3150 requestIdleCallback(() => {
3151 saveToRecentSearches(query)
3152 })
3153
3154 requestIdleCallback(() => {
3155 prefetchTopResults(results.slice(0, 3))
3156 })
3157 }
3158 ```
3159
3160 **With timeout for required work:**
3161
3162 ```typescript
3163 // Ensure analytics fires within 2 seconds even if browser stays busy
3164 requestIdleCallback(
3165 () => analytics.track('page_view', { path: location.pathname }),
3166 { timeout: 2000 }
3167 )
3168 ```
3169
3170 **Chunking large tasks:**
3171
3172 ```typescript
3173 function processLargeDataset(items: Item[]) {
3174 let index = 0
3175
3176 function processChunk(deadline: IdleDeadline) {
3177 // Process items while we have idle time (aim for <50ms chunks)
3178 while (index < items.length && deadline.timeRemaining() > 0) {
3179 processItem(items[index])
3180 index++
3181 }
3182
3183 // Schedule next chunk if more items remain
3184 if (index < items.length) {
3185 requestIdleCallback(processChunk)
3186 }
3187 }
3188
3189 requestIdleCallback(processChunk)
3190 }
3191 ```
3192
3193 **With fallback for unsupported browsers:**
3194
3195 ```typescript
3196 const scheduleIdleWork = window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1))
3197
3198 scheduleIdleWork(() => {
3199 // Non-critical work
3200 })
3201 ```
3202
3203 **When to use:**
3204
3205 - Analytics and telemetry
3206
3207 - Saving state to localStorage/IndexedDB
3208
3209 - Prefetching resources for likely next actions
3210
3211 - Processing non-urgent data transformations
3212
3213 - Lazy initialization of non-critical features
3214
3215 **When NOT to use:**
3216
3217 - User-initiated actions that need immediate feedback
3218
3219 - Rendering updates the user is waiting for
3220
3221 - Time-sensitive operations
3222
3223 ### 7.8 Early Length Check for Array Comparisons
3224
3225 **Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
3226
3227 When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
3228
3229 In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
3230
3231 **Incorrect: always runs expensive comparison**
3232
3233 ```typescript
3234 function hasChanges(current: string[], original: string[]) {
3235 // Always sorts and joins, even when lengths differ
3236 return current.sort().join() !== original.sort().join()
3237 }
3238 ```
3239
3240 Two 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.
3241
3242 **Correct (O(1) length check first):**
3243
3244 ```typescript
3245 function hasChanges(current: string[], original: string[]) {
3246 // Early return if lengths differ
3247 if (current.length !== original.length) {
3248 return true
3249 }
3250 // Only sort when lengths match
3251 const currentSorted = current.toSorted()
3252 const originalSorted = original.toSorted()
3253 for (let i = 0; i < currentSorted.length; i++) {
3254 if (currentSorted[i] !== originalSorted[i]) {
3255 return true
3256 }
3257 }
3258 return false
3259 }
3260 ```
3261
3262 This new approach is more efficient because:
3263
3264 - It avoids the overhead of sorting and joining the arrays when lengths differ
3265
3266 - It avoids consuming memory for the joined strings (especially important for large arrays)
3267
3268 - It avoids mutating the original arrays
3269
3270 - It returns early when a difference is found
3271
3272 ### 7.9 Early Return from Functions
3273
3274 **Impact: LOW-MEDIUM (avoids unnecessary computation)**
3275
3276 Return early when result is determined to skip unnecessary processing.
3277
3278 **Incorrect: processes all items even after finding answer**
3279
3280 ```typescript
3281 function validateUsers(users: User[]) {
3282 let hasError = false
3283 let errorMessage = ''
3284
3285 for (const user of users) {
3286 if (!user.email) {
3287 hasError = true
3288 errorMessage = 'Email required'
3289 }
3290 if (!user.name) {
3291 hasError = true
3292 errorMessage = 'Name required'
3293 }
3294 // Continues checking all users even after error found
3295 }
3296
3297 return hasError ? { valid: false, error: errorMessage } : { valid: true }
3298 }
3299 ```
3300
3301 **Correct: returns immediately on first error**
3302
3303 ```typescript
3304 function validateUsers(users: User[]) {
3305 for (const user of users) {
3306 if (!user.email) {
3307 return { valid: false, error: 'Email required' }
3308 }
3309 if (!user.name) {
3310 return { valid: false, error: 'Name required' }
3311 }
3312 }
3313
3314 return { valid: true }
3315 }
3316 ```
3317
3318 ### 7.10 Hoist RegExp Creation
3319
3320 **Impact: LOW-MEDIUM (avoids recreation)**
3321
3322 Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
3323
3324 **Incorrect: new RegExp every render**
3325
3326 ```tsx
3327 function Highlighter({ text, query }: Props) {
3328 const regex = new RegExp(`(${query})`, 'gi')
3329 const parts = text.split(regex)
3330 return <>{parts.map((part, i) => ...)}</>
3331 }
3332 ```
3333
3334 **Correct: memoize or hoist**
3335
3336 ```tsx
3337 const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
3338
3339 function Highlighter({ text, query }: Props) {
3340 const regex = useMemo(
3341 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
3342 [query]
3343 )
3344 const parts = text.split(regex)
3345 return <>{parts.map((part, i) => ...)}</>
3346 }
3347 ```
3348
3349 **Warning: global regex has mutable state**
3350
3351 ```typescript
3352 const regex = /foo/g
3353 regex.test('foo') // true, lastIndex = 3
3354 regex.test('foo') // false, lastIndex = 0
3355 ```
3356
3357 Global regex (`/g`) has mutable `lastIndex` state:
3358
3359 ### 7.11 Use flatMap to Map and Filter in One Pass
3360
3361 **Impact: LOW-MEDIUM (eliminates intermediate array)**
3362
3363 Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
3364
3365 **Incorrect: 2 iterations, intermediate array**
3366
3367 ```typescript
3368 const userNames = users
3369 .map(user => user.isActive ? user.name : null)
3370 .filter(Boolean)
3371 ```
3372
3373 **Correct: 1 iteration, no intermediate array**
3374
3375 ```typescript
3376 const userNames = users.flatMap(user =>
3377 user.isActive ? [user.name] : []
3378 )
3379 ```
3380
3381 **More examples:**
3382
3383 ```typescript
3384 // Extract valid emails from responses
3385 // Before
3386 const emails = responses
3387 .map(r => r.success ? r.data.email : null)
3388 .filter(Boolean)
3389
3390 // After
3391 const emails = responses.flatMap(r =>
3392 r.success ? [r.data.email] : []
3393 )
3394
3395 // Parse and filter valid numbers
3396 // Before
3397 const numbers = strings
3398 .map(s => parseInt(s, 10))
3399 .filter(n => !isNaN(n))
3400
3401 // After
3402 const numbers = strings.flatMap(s => {
3403 const n = parseInt(s, 10)
3404 return isNaN(n) ? [] : [n]
3405 })
3406 ```
3407
3408 **When to use:**
3409
3410 - Transforming items while filtering some out
3411
3412 - Conditional mapping where some inputs produce no output
3413
3414 - Parsing/validating where invalid inputs should be skipped
3415
3416 ### 7.12 Use Loop for Min/Max Instead of Sort
3417
3418 **Impact: LOW (O(n) instead of O(n log n))**
3419
3420 Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
3421
3422 **Incorrect (O(n log n) - sort to find latest):**
3423
3424 ```typescript
3425 interface Project {
3426 id: string
3427 name: string
3428 updatedAt: number
3429 }
3430
3431 function getLatestProject(projects: Project[]) {
3432 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
3433 return sorted[0]
3434 }
3435 ```
3436
3437 Sorts the entire array just to find the maximum value.
3438
3439 **Incorrect (O(n log n) - sort for oldest and newest):**
3440
3441 ```typescript
3442 function getOldestAndNewest(projects: Project[]) {
3443 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
3444 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
3445 }
3446 ```
3447
3448 Still sorts unnecessarily when only min/max are needed.
3449
3450 **Correct (O(n) - single loop):**
3451
3452 ```typescript
3453 function getLatestProject(projects: Project[]) {
3454 if (projects.length === 0) return null
3455
3456 let latest = projects[0]
3457
3458 for (let i = 1; i < projects.length; i++) {
3459 if (projects[i].updatedAt > latest.updatedAt) {
3460 latest = projects[i]
3461 }
3462 }
3463
3464 return latest
3465 }
3466
3467 function getOldestAndNewest(projects: Project[]) {
3468 if (projects.length === 0) return { oldest: null, newest: null }
3469
3470 let oldest = projects[0]
3471 let newest = projects[0]
3472
3473 for (let i = 1; i < projects.length; i++) {
3474 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
3475 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
3476 }
3477
3478 return { oldest, newest }
3479 }
3480 ```
3481
3482 Single pass through the array, no copying, no sorting.
3483
3484 **Alternative: Math.min/Math.max for small arrays**
3485
3486 ```typescript
3487 const numbers = [5, 2, 8, 1, 9]
3488 const min = Math.min(...numbers)
3489 const max = Math.max(...numbers)
3490 ```
3491
3492 This 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.
3493
3494 ### 7.13 Use Set/Map for O(1) Lookups
3495
3496 **Impact: LOW-MEDIUM (O(n) to O(1))**
3497
3498 Convert arrays to Set/Map for repeated membership checks.
3499
3500 **Incorrect (O(n) per check):**
3501
3502 ```typescript
3503 const allowedIds = ['a', 'b', 'c', ...]
3504 items.filter(item => allowedIds.includes(item.id))
3505 ```
3506
3507 **Correct (O(1) per check):**
3508
3509 ```typescript
3510 const allowedIds = new Set(['a', 'b', 'c', ...])
3511 items.filter(item => allowedIds.has(item.id))
3512 ```
3513
3514 ### 7.14 Use toSorted() Instead of sort() for Immutability
3515
3516 **Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
3517
3518 `.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.
3519
3520 **Incorrect: mutates original array**
3521
3522 ```typescript
3523 function UserList({ users }: { users: User[] }) {
3524 // Mutates the users prop array!
3525 const sorted = useMemo(
3526 () => users.sort((a, b) => a.name.localeCompare(b.name)),
3527 [users]
3528 )
3529 return <div>{sorted.map(renderUser)}</div>
3530 }
3531 ```
3532
3533 **Correct: creates new array**
3534
3535 ```typescript
3536 function UserList({ users }: { users: User[] }) {
3537 // Creates new sorted array, original unchanged
3538 const sorted = useMemo(
3539 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
3540 [users]
3541 )
3542 return <div>{sorted.map(renderUser)}</div>
3543 }
3544 ```
3545
3546 **Why this matters in React:**
3547
3548 1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
3549
3550 2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
3551
3552 **Browser support: fallback for older browsers**
3553
3554 ```typescript
3555 // Fallback for older browsers
3556 const sorted = [...items].sort((a, b) => a.value - b.value)
3557 ```
3558
3559 `.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
3560
3561 **Other immutable array methods:**
3562
3563 - `.toSorted()` - immutable sort
3564
3565 - `.toReversed()` - immutable reverse
3566
3567 - `.toSpliced()` - immutable splice
3568
3569 - `.with()` - immutable element replacement
3570
3571 ---
3572
3573 ## 8. Advanced Patterns
3574
3575 **Impact: LOW**
3576
3577 Advanced patterns for specific cases that require careful implementation.
3578
3579 ### 8.1 Do Not Put Effect Events in Dependency Arrays
3580
3581 **Impact: LOW (avoids unnecessary effect re-runs and lint errors)**
3582
3583 Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.
3584
3585 **Incorrect: Effect Event added as a dependency**
3586
3587 ```tsx
3588 import { useEffect, useEffectEvent } from 'react'
3589
3590 function ChatRoom({ roomId, onConnected }: {
3591 roomId: string
3592 onConnected: () => void
3593 }) {
3594 const handleConnected = useEffectEvent(onConnected)
3595
3596 useEffect(() => {
3597 const connection = createConnection(roomId)
3598 connection.on('connected', handleConnected)
3599 connection.connect()
3600
3601 return () => connection.disconnect()
3602 }, [roomId, handleConnected])
3603 }
3604 ```
3605
3606 Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.
3607
3608 **Correct: depend on reactive values, not the Effect Event**
3609
3610 ```tsx
3611 import { useEffect, useEffectEvent } from 'react'
3612
3613 function ChatRoom({ roomId, onConnected }: {
3614 roomId: string
3615 onConnected: () => void
3616 }) {
3617 const handleConnected = useEffectEvent(onConnected)
3618
3619 useEffect(() => {
3620 const connection = createConnection(roomId)
3621 connection.on('connected', handleConnected)
3622 connection.connect()
3623
3624 return () => connection.disconnect()
3625 }, [roomId])
3626 }
3627 ```
3628
3629 Reference: [https://react.dev/reference/react/useEffectEvent#effect-event-in-deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)
3630
3631 ### 8.2 Initialize App Once, Not Per Mount
3632
3633 **Impact: LOW-MEDIUM (avoids duplicate init in development)**
3634
3635 Do 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.
3636
3637 **Incorrect: runs twice in dev, re-runs on remount**
3638
3639 ```tsx
3640 function Comp() {
3641 useEffect(() => {
3642 loadFromStorage()
3643 checkAuthToken()
3644 }, [])
3645
3646 // ...
3647 }
3648 ```
3649
3650 **Correct: once per app load**
3651
3652 ```tsx
3653 let didInit = false
3654
3655 function Comp() {
3656 useEffect(() => {
3657 if (didInit) return
3658 didInit = true
3659 loadFromStorage()
3660 checkAuthToken()
3661 }, [])
3662
3663 // ...
3664 }
3665 ```
3666
3667 Reference: [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)
3668
3669 ### 8.3 Store Event Handlers in Refs
3670
3671 **Impact: LOW (stable subscriptions)**
3672
3673 Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
3674
3675 **Incorrect: re-subscribes on every render**
3676
3677 ```tsx
3678 function useWindowEvent(event: string, handler: (e) => void) {
3679 useEffect(() => {
3680 window.addEventListener(event, handler)
3681 return () => window.removeEventListener(event, handler)
3682 }, [event, handler])
3683 }
3684 ```
3685
3686 **Correct: stable subscription**
3687
3688 ```tsx
3689 import { useEffectEvent } from 'react'
3690
3691 function useWindowEvent(event: string, handler: (e) => void) {
3692 const onEvent = useEffectEvent(handler)
3693
3694 useEffect(() => {
3695 window.addEventListener(event, onEvent)
3696 return () => window.removeEventListener(event, onEvent)
3697 }, [event])
3698 }
3699 ```
3700
3701 **Alternative: use `useEffectEvent` if you're on latest React:**
3702
3703 `useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
3704
3705 ### 8.4 useEffectEvent for Stable Callback Refs
3706
3707 **Impact: LOW (prevents effect re-runs)**
3708
3709 Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
3710
3711 **Incorrect: effect re-runs on every callback change**
3712
3713 ```tsx
3714 function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3715 const [query, setQuery] = useState('')
3716
3717 useEffect(() => {
3718 const timeout = setTimeout(() => onSearch(query), 300)
3719 return () => clearTimeout(timeout)
3720 }, [query, onSearch])
3721 }
3722 ```
3723
3724 **Correct: using React's useEffectEvent**
3725
3726 ```tsx
3727 import { useEffectEvent } from 'react';
3728
3729 function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3730 const [query, setQuery] = useState('')
3731 const onSearchEvent = useEffectEvent(onSearch)
3732
3733 useEffect(() => {
3734 const timeout = setTimeout(() => onSearchEvent(query), 300)
3735 return () => clearTimeout(timeout)
3736 }, [query])
3737 }
3738 ```
3739
3740 ---
3741
3742 ## References
3743
3744 1. [https://react.dev](https://react.dev)
3745 2. [https://nextjs.org](https://nextjs.org)
3746 3. [https://swr.vercel.app](https://swr.vercel.app)
3747 4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
3748 5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
3749 6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
3750 7. [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)
3751
3751 lines MARKDOWN