Two files, one repository
CherryHQ/cherry-studio ships 2 formats across 3 indexed files. The question worth asking is whether the second one says anything the first does not.
CompareCLAUDE.md ↔ AGENTS.md
| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 60 | 29 | 0% |
| Commands | 0 | 2 | 15 | 0% |
| Section tags | 6 | 6 | 6 | 33% |
What each file covers
Sections
0 shared · 60 only in A · 29 only in B- − React Best Practices
- − Abstract
- − Table of Contents
- − 1. Eliminating Waterfalls
- − 1.1 Defer Await Until Needed
- − 1.2 Dependency-Based Parallelization
- − 1.3 Prevent Waterfall Chains in API Routes
- − 1.4 Promise.all() for Independent Operations
- − 1.5 Strategic Suspense Boundaries
- − 2. Bundle Size Optimization
- − 2.1 Avoid Barrel File Imports
- − 2.2 Conditional Module Loading
- − 2.3 Defer Non-Critical Third-Party Libraries
- − 2.4 Dynamic Imports for Heavy Components
- − 2.5 Preload Based on User Intent
- − 3. Server-Side Performance
- − 3.1 Authenticate Server Actions Like API Routes
- − 3.2 Avoid Duplicate Serialization in RSC Props
- − 3.3 Cross-Request LRU Caching
- − 3.4 Hoist Static I/O to Module Level
- − 3.5 Minimize Serialization at RSC Boundaries
- − 3.6 Parallel Data Fetching with Component Composition
- − 3.7 Per-Request Deduplication with React.cache()
- − 3.8 Use after() for Non-Blocking Operations
- − 4. Client-Side Data Fetching
- − 4.1 Deduplicate Global Event Listeners
- − 4.2 Use Passive Event Listeners for Scrolling Performance
- − 4.3 Use SWR for Automatic Deduplication
- − 4.4 Version and Minimize localStorage Data
- − 5. Re-render Optimization
- − 5.1 Calculate Derived State During Rendering
- − 5.2 Defer State Reads to Usage Point
- − 5.3 Do not wrap a simple expression with a primitive result type in useMemo
- − 5.4 Don't Define Components Inside Components
- − 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
- − 5.6 Extract to Memoized Components
- − 5.7 Narrow Effect Dependencies
- − 5.8 Put Interaction Logic in Event Handlers
- − 5.9 Subscribe to Derived State
- − 5.10 Use Functional setState Updates
- − 5.11 Use Lazy State Initialization
- − 5.12 Use Transitions for Non-Urgent Updates
- − 5.13 Use useRef for Transient Values
- − 6. Rendering Performance
- − 6.1 Animate SVG Wrapper Instead of SVG Element
- − 6.2 CSS content-visibility for Long Lists
- − 6.3 Hoist Static JSX Elements
- − 6.4 Optimize SVG Precision
- − 6.5 Prevent Hydration Mismatch Without Flickering
- − 6.6 Suppress Expected Hydration Mismatches
- − 6.7 Use Activity Component for Show/Hide
- − 6.8 Use defer or async on Script Tags
- − 6.9 Use Explicit Conditional Rendering
- − 6.10 Use React DOM Resource Hints
- − 6.11 Use useTransition Over Manual Loading States
- − 7. JavaScript Performance
- − 7.1 Avoid Layout Thrashing
- − 7.2 Build Index Maps for Repeated Lookups
- − 7.3 Cache Property Access in Loops
- − 7.4 Cache Repeated Function Calls
- + Guiding Principles (MUST FOLLOW)
- + Mindset
- + Operational Rules
- + Development
- + Commands
- + Testing
- + Patched Dependencies
- + GitHub
- + Pull Requests
- + Code Review
- + Issues
- + Conventions
- + TypeScript
- + Naming Conventions
- + Logging
- + Paths
- + i18n
- + UI Design
- + Architecture
- + Code Organization
- + Data
- + IPC (IpcApi)
- + Window Manager
- + Main Process Services (Lifecycle)
- + Non-Lifecycle Services (Direct-Import Singleton)
- + Schema & Migration Rules
- + Data Classification Toolchain
- + Breaking Changes Log
- + Local Instructions
Commands
0 shared · 2 only in A · 15 only in B- − node.style.transform = `translateX(${e.clientX}px)`
- − npx svgo --precision=1 --multipass icon.svg
- + pnpm lint
- + pnpm test
- + pnpm format
- + git log
- + git commit -S --signoff
- + git cat-file commit HEAD
- + pnpm install
- + pnpm build:check
- + pnpm i18n:sync
- + pnpm test:lint
- + vitest.config.*
- + gh-create-pr
- + gh-create-issue
- + pnpm i18n:check
- + pnpm db:migrations:generate
Section tags
6 shared · 6 only in A · 6 only in B- − build
- − architecture
- − security
- − api
- − performance
- − deployment
- + setup
- + test
- + testing-strategy
- + git-pr
- + database
- + agent-behaviour
- lint-format
- code-style
- types
- dependencies
- ui
- do-not
Line diff
CherryHQ/cherry-studio · .agents/skills/vercel-react-best-practices/AGENTS.md
@@ −1 @@
1# React Best Practices
2
3**Version 1.0.0**
4Vercel Engineering
5January 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
17Comprehensive 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
231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
24 - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
25 - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
26 - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
27 - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
28 - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
292. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
30 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
31 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
32 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
33 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
34 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
353. [Server-Side Performance](#3-server-side-performance) — **HIGH**
36 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
37 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
38 - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
39 - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level)
40 - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries)
41 - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition)
42 - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache)
43 - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations)
444. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
45 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
46 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
47 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
48 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
495. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
50 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
51 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
52 - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
53 - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)
54 - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
55 - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)
56 - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)
57 - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)
58 - 5.9 [Subscribe to Derived State](#59-subscribe-to-derived-state)
59 - 5.10 [Use Functional setState Updates](#510-use-functional-setstate-updates)
60 - 5.11 [Use Lazy State Initialization](#511-use-lazy-state-initialization)
61 - 5.12 [Use Transitions for Non-Urgent Updates](#512-use-transitions-for-non-urgent-updates)
62 - 5.13 [Use useRef for Transient Values](#513-use-useref-for-transient-values)
636. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
64 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
65 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
66 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
67 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
68 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
69 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
70 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
71 - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)
72 - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)
73 - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)
74 - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)
757. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
76 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
77 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
78 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
79 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
80 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
81 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
82 - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
83 - 7.8 [Early Return from Functions](#78-early-return-from-functions)
84 - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
85 - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass)
86 - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort)
87 - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups)
88 - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability)
898. [Advanced Patterns](#8-advanced-patterns) — **LOW**
90 - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
91 - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
92 - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
93
94---
95
96## 1. Eliminating Waterfalls
97
98**Impact: CRITICAL**
99
100Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
101
102### 1.1 Defer Await Until Needed
103
104**Impact: HIGH (avoids blocking unused code paths)**
105
106Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
107
108**Incorrect: blocks both branches**
109
110```typescript
111async function handleRequest(userId: string, skipProcessing: boolean) {
112 const userData = await fetchUserData(userId)
113
114 if (skipProcessing) {
115 // Returns immediately but still waited for userData
116 return { skipped: true }
117 }
118
119 // Only this branch uses userData
120 return processUserData(userData)
121}
122```
123
124**Correct: only blocks when needed**
125
126```typescript
127async function handleRequest(userId: string, skipProcessing: boolean) {
128 if (skipProcessing) {
129 // Returns immediately without waiting
130 return { skipped: true }
131 }
132
133 // Fetch only when needed
134 const userData = await fetchUserData(userId)
135 return processUserData(userData)
136}
137```
138
139**Another example: early return optimization**
140
141```typescript
142// Incorrect: always fetches permissions
143async function updateResource(resourceId: string, userId: string) {
144 const permissions = await fetchPermissions(userId)
145 const resource = await getResource(resourceId)
146
147 if (!resource) {
148 return { error: 'Not found' }
149 }
150
151 if (!permissions.canEdit) {
152 return { error: 'Forbidden' }
153 }
154
155 return await updateResourceData(resource, permissions)
156}
157
158// Correct: fetches only when needed
159async function updateResource(resourceId: string, userId: string) {
160 const resource = await getResource(resourceId)
161
162 if (!resource) {
163 return { error: 'Not found' }
164 }
165
166 const permissions = await fetchPermissions(userId)
167
168 if (!permissions.canEdit) {
169 return { error: 'Forbidden' }
170 }
171
172 return await updateResourceData(resource, permissions)
173}
174```
175
176This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
177
178### 1.2 Dependency-Based Parallelization
179
180**Impact: CRITICAL (2-10× improvement)**
181
182For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
183
184**Incorrect: profile waits for config unnecessarily**
185
186```typescript
187const [user, config] = await Promise.all([
188 fetchUser(),
189 fetchConfig()
190])
191const profile = await fetchProfile(user.id)
192```
193
194**Correct: config and profile run in parallel**
195
196```typescript
197import { all } from 'better-all'
198
199const { user, config, profile } = await all({
200 async user() { return fetchUser() },
201 async config() { return fetchConfig() },
202 async profile() {
203 return fetchProfile((await this.$.user).id)
204 }
205})
206```
207
208**Alternative without extra dependencies:**
209
210```typescript
211const userPromise = fetchUser()
212const profilePromise = userPromise.then(user => fetchProfile(user.id))
213
214const [user, config, profile] = await Promise.all([
215 userPromise,
216 fetchConfig(),
217 profilePromise
218])
219```
220
221We can also create all the promises first, and do `Promise.all()` at the end.
222
223Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
224
225### 1.3 Prevent Waterfall Chains in API Routes
226
227**Impact: CRITICAL (2-10× improvement)**
228
229In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
230
231**Incorrect: config waits for auth, data waits for both**
232
233```typescript
234export async function GET(request: Request) {
235 const session = await auth()
236 const config = await fetchConfig()
237 const data = await fetchData(session.user.id)
238 return Response.json({ data, config })
239}
240```
241
242**Correct: auth and config start immediately**
243
244```typescript
245export async function GET(request: Request) {
246 const sessionPromise = auth()
247 const configPromise = fetchConfig()
248 const session = await sessionPromise
249 const [config, data] = await Promise.all([
250 configPromise,
251 fetchData(session.user.id)
252 ])
253 return Response.json({ data, config })
254}
255```
256
257For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
258
259### 1.4 Promise.all() for Independent Operations
260
261**Impact: CRITICAL (2-10× improvement)**
262
263When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
264
265**Incorrect: sequential execution, 3 round trips**
266
267```typescript
268const user = await fetchUser()
269const posts = await fetchPosts()
270const comments = await fetchComments()
271```
272
273**Correct: parallel execution, 1 round trip**
274
275```typescript
276const [user, posts, comments] = await Promise.all([
277 fetchUser(),
278 fetchPosts(),
279 fetchComments()
280])
281```
282
283### 1.5 Strategic Suspense Boundaries
284
285**Impact: HIGH (faster initial paint)**
286
287Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
288
289**Incorrect: wrapper blocked by data fetching**
290
291```tsx
292async function Page() {
293 const data = await fetchData() // Blocks entire page
294
295 return (
296 <div>
297 <div>Sidebar</div>
298 <div>Header</div>
299 <div>
300 <DataDisplay data={data} />
301 </div>
302 <div>Footer</div>
303 </div>
304 )
305}
306```
307
308The entire layout waits for data even though only the middle section needs it.
309
310**Correct: wrapper shows immediately, data streams in**
311
312```tsx
313function Page() {
314 return (
315 <div>
316 <div>Sidebar</div>
317 <div>Header</div>
318 <div>
319 <Suspense fallback={<Skeleton />}>
320 <DataDisplay />
321 </Suspense>
322 </div>
323 <div>Footer</div>
324 </div>
325 )
326}
327
328async function DataDisplay() {
329 const data = await fetchData() // Only blocks this component
330 return <div>{data.content}</div>
331}
332```
333
334Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
335
336**Alternative: share promise across components**
337
338```tsx
339function Page() {
340 // Start fetch immediately, but don't await
341 const dataPromise = fetchData()
342
343 return (
344 <div>
345 <div>Sidebar</div>
346 <div>Header</div>
347 <Suspense fallback={<Skeleton />}>
348 <DataDisplay dataPromise={dataPromise} />
349 <DataSummary dataPromise={dataPromise} />
350 </Suspense>
351 <div>Footer</div>
352 </div>
353 )
354}
355
356function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
357 const data = use(dataPromise) // Unwraps the promise
358 return <div>{data.content}</div>
359}
360
361function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
362 const data = use(dataPromise) // Reuses the same promise
363 return <div>{data.summary}</div>
364}
365```
366
367Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
368
369**When NOT to use this pattern:**
370
371- Critical data needed for layout decisions (affects positioning)
372
373- SEO-critical content above the fold
374
375- Small, fast queries where suspense overhead isn't worth it
376
377- When you want to avoid layout shift (loading → content jump)
378
379**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
380
381---
382
383## 2. Bundle Size Optimization
384
385**Impact: CRITICAL**
386
387Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
388
389### 2.1 Avoid Barrel File Imports
390
391**Impact: CRITICAL (200-800ms import cost, slow builds)**
392
393Import 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'`).
394
395Popular 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.
396
397**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.
398
399**Incorrect: imports entire library**
400
401```tsx
402import { Check, X, Menu } from 'lucide-react'
403// Loads 1,583 modules, takes ~2.8s extra in dev
404// Runtime cost: 200-800ms on every cold start
405
406import { Button, TextField } from '@mui/material'
407// Loads 2,225 modules, takes ~4.2s extra in dev
408```
409
410**Correct: imports only what you need**
411
412```tsx
413import Check from 'lucide-react/dist/esm/icons/check'
414import X from 'lucide-react/dist/esm/icons/x'
415import Menu from 'lucide-react/dist/esm/icons/menu'
416// Loads only 3 modules (~2KB vs ~1MB)
417
418import Button from '@mui/material/Button'
419import TextField from '@mui/material/TextField'
420// Loads only what you use
421```
422
423**Alternative: Next.js 13.5+**
424
425```js
426// next.config.js - use optimizePackageImports
427module.exports = {
428 experimental: {
429 optimizePackageImports: ['lucide-react', '@mui/material']
430 }
431}
432
433// Then you can keep the ergonomic barrel imports:
434import { Check, X, Menu } from 'lucide-react'
435// Automatically transformed to direct imports at build time
436```
437
438Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
439
440Libraries 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`.
441
442Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
443
444### 2.2 Conditional Module Loading
445
446**Impact: HIGH (loads large data only when needed)**
447
448Load large data or modules only when a feature is activated.
449
450**Example: lazy-load animation frames**
451
452```tsx
453function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
454 const [frames, setFrames] = useState<Frame[] | null>(null)
455
456 useEffect(() => {
457 if (enabled && !frames && typeof window !== 'undefined') {
458 import('./animation-frames.js')
459 .then(mod => setFrames(mod.frames))
460 .catch(() => setEnabled(false))
461 }
462 }, [enabled, frames, setEnabled])
463
464 if (!frames) return <Skeleton />
465 return <Canvas frames={frames} />
466}
467```
468
469The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
470
471### 2.3 Defer Non-Critical Third-Party Libraries
472
473**Impact: MEDIUM (loads after hydration)**
474
475Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
476
477**Incorrect: blocks initial bundle**
478
479```tsx
480import { Analytics } from '@vercel/analytics/react'
481
482export default function RootLayout({ children }) {
483 return (
484 <html>
485 <body>
486 {children}
487 <Analytics />
488 </body>
489 </html>
490 )
491}
492```
493
494**Correct: loads after hydration**
495
496```tsx
497import dynamic from 'next/dynamic'
498
499const Analytics = dynamic(
500 () => import('@vercel/analytics/react').then(m => m.Analytics),
501 { ssr: false }
502)
503
504export default function RootLayout({ children }) {
505 return (
506 <html>
507 <body>
508 {children}
509 <Analytics />
510 </body>
511 </html>
512 )
513}
514```
515
516### 2.4 Dynamic Imports for Heavy Components
517
518**Impact: CRITICAL (directly affects TTI and LCP)**
519
520Use `next/dynamic` to lazy-load large components not needed on initial render.
521
522**Incorrect: Monaco bundles with main chunk ~300KB**
523
524```tsx
525import { MonacoEditor } from './monaco-editor'
526
527function CodePanel({ code }: { code: string }) {
528 return <MonacoEditor value={code} />
529}
530```
531
532**Correct: Monaco loads on demand**
533
534```tsx
535import dynamic from 'next/dynamic'
536
537const MonacoEditor = dynamic(
538 () => import('./monaco-editor').then(m => m.MonacoEditor),
539 { ssr: false }
540)
541
542function CodePanel({ code }: { code: string }) {
543 return <MonacoEditor value={code} />
544}
545```
546
547### 2.5 Preload Based on User Intent
548
549**Impact: MEDIUM (reduces perceived latency)**
550
551Preload heavy bundles before they're needed to reduce perceived latency.
552
553**Example: preload on hover/focus**
554
555```tsx
556function EditorButton({ onClick }: { onClick: () => void }) {
557 const preload = () => {
558 if (typeof window !== 'undefined') {
559 void import('./monaco-editor')
560 }
561 }
562
563 return (
564 <button
565 onMouseEnter={preload}
566 onFocus={preload}
567 onClick={onClick}
568 >
569 Open Editor
570 </button>
571 )
572}
573```
574
575**Example: preload when feature flag is enabled**
576
577```tsx
578function FlagsProvider({ children, flags }: Props) {
579 useEffect(() => {
580 if (flags.editorEnabled && typeof window !== 'undefined') {
581 void import('./monaco-editor').then(mod => mod.init())
582 }
583 }, [flags.editorEnabled])
584
585 return <FlagsContext.Provider value={flags}>
586 {children}
587 </FlagsContext.Provider>
588}
589```
590
591The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
592
593---
594
595## 3. Server-Side Performance
596
597**Impact: HIGH**
598
599Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
600
601### 3.1 Authenticate Server Actions Like API Routes
602
603**Impact: CRITICAL (prevents unauthorized access to server mutations)**
604
605Server 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.
606
607Next.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."
608
609**Incorrect: no authentication check**
610
611```typescript
612'use server'
613
614export async function deleteUser(userId: string) {
615 // Anyone can call this! No auth check
616 await db.user.delete({ where: { id: userId } })
617 return { success: true }
618}
619```
620
621**Correct: authentication inside the action**
622
623```typescript
624'use server'
625
626import { verifySession } from '@/lib/auth'
627import { unauthorized } from '@/lib/errors'
628
629export async function deleteUser(userId: string) {
630 // Always check auth inside the action
631 const session = await verifySession()
632
633 if (!session) {
634 throw unauthorized('Must be logged in')
635 }
636
637 // Check authorization too
638 if (session.user.role !== 'admin' && session.user.id !== userId) {
639 throw unauthorized('Cannot delete other users')
640 }
641
642 await db.user.delete({ where: { id: userId } })
643 return { success: true }
644}
645```
646
647**With input validation:**
648
649```typescript
650'use server'
651
652import { verifySession } from '@/lib/auth'
653import { z } from 'zod'
654
655const updateProfileSchema = z.object({
656 userId: z.string().uuid(),
657 name: z.string().min(1).max(100),
658 email: z.string().email()
659})
660
661export async function updateProfile(data: unknown) {
662 // Validate input first
663 const validated = updateProfileSchema.parse(data)
664
665 // Then authenticate
666 const session = await verifySession()
667 if (!session) {
668 throw new Error('Unauthorized')
669 }
670
671 // Then authorize
672 if (session.user.id !== validated.userId) {
673 throw new Error('Can only update own profile')
674 }
675
676 // Finally perform the mutation
677 await db.user.update({
678 where: { id: validated.userId },
679 data: {
680 name: validated.name,
681 email: validated.email
682 }
683 })
684
685 return { success: true }
686}
687```
688
689Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
690
691### 3.2 Avoid Duplicate Serialization in RSC Props
692
693**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
694
695RSC→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.
696
697**Incorrect: duplicates array**
698
699```tsx
700// RSC: sends 6 strings (2 arrays × 3 items)
701<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
702```
703
704**Correct: sends 3 strings**
705
706```tsx
707// RSC: send once
708<ClientList usernames={usernames} />
709
710// Client: transform there
711'use client'
712const sorted = useMemo(() => [...usernames].sort(), [usernames])
713```
714
715**Nested deduplication behavior:**
716
717```tsx
718// string[] - duplicates everything
719usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
720
721// object[] - duplicates array structure only
722users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
723```
724
725Deduplication works recursively. Impact varies by data type:
726
727- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
728
729- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
730
731**Operations breaking deduplication: create new references**
732
733- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
734
735- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
736
737**More examples:**
738
739```tsx
740// ❌ Bad
741<C users={users} active={users.filter(u => u.active)} />
742<C product={product} productName={product.name} />
743
744// ✅ Good
745<C users={users} />
746<C product={product} />
747// Do filtering/destructuring in client
748```
749
750**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
751
752### 3.3 Cross-Request LRU Caching
753
754**Impact: HIGH (caches across requests)**
755
756`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
757
758**Implementation:**
759
760```typescript
761import { LRUCache } from 'lru-cache'
762
763const cache = new LRUCache<string, any>({
764 max: 1000,
765 ttl: 5 * 60 * 1000 // 5 minutes
766})
767
768export async function getUser(id: string) {
769 const cached = cache.get(id)
770 if (cached) return cached
771
772 const user = await db.user.findUnique({ where: { id } })
773 cache.set(id, user)
774 return user
775}
776
777// Request 1: DB query, result cached
778// Request 2: cache hit, no DB query
779```
780
781Use when sequential user actions hit multiple endpoints needing the same data within seconds.
782
783**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.
784
785**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
786
787Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
788
789### 3.4 Hoist Static I/O to Module Level
790
791**Impact: HIGH (avoids repeated file/network I/O per request)**
792
793When 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.
794
795**Incorrect: reads font file on every request**
796
797**Correct: loads once at module initialization**
798
799**Alternative: synchronous file reads with Node.js fs**
800
801**General Node.js example: loading config or templates**
802
803**When to use this pattern:**
804
805- Loading fonts for OG image generation
806
807- Loading static logos, icons, or watermarks
808
809- Reading configuration files that don't change at runtime
810
811- Loading email templates or other static templates
812
813- Any static asset that's the same across all requests
814
815**When NOT to use this pattern:**
816
817- Assets that vary per request or user
818
819- Files that may change during runtime (use caching with TTL instead)
820
821- Large files that would consume too much memory if kept loaded
822
823- Sensitive data that shouldn't persist in memory
824
825**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.
826
827**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
828
829### 3.5 Minimize Serialization at RSC Boundaries
830
831**Impact: HIGH (reduces data transfer size)**
832
833The 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.
834
835**Incorrect: serializes all 50 fields**
836
837```tsx
838async function Page() {
839 const user = await fetchUser() // 50 fields
840 return <Profile user={user} />
841}
842
843'use client'
844function Profile({ user }: { user: User }) {
845 return <div>{user.name}</div> // uses 1 field
846}
847```
848
849**Correct: serializes only 1 field**
850
851```tsx
852async function Page() {
853 const user = await fetchUser()
854 return <Profile name={user.name} />
855}
856
857'use client'
858function Profile({ name }: { name: string }) {
859 return <div>{name}</div>
860}
861```
862
863### 3.6 Parallel Data Fetching with Component Composition
864
865**Impact: CRITICAL (eliminates server-side waterfalls)**
866
867React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
868
869**Incorrect: Sidebar waits for Page's fetch to complete**
870
871```tsx
872export default async function Page() {
873 const header = await fetchHeader()
874 return (
875 <div>
876 <div>{header}</div>
877 <Sidebar />
878 </div>
879 )
880}
881
882async function Sidebar() {
883 const items = await fetchSidebarItems()
884 return <nav>{items.map(renderItem)}</nav>
885}
886```
887
888**Correct: both fetch simultaneously**
889
890```tsx
891async function Header() {
892 const data = await fetchHeader()
893 return <div>{data}</div>
894}
895
896async function Sidebar() {
897 const items = await fetchSidebarItems()
898 return <nav>{items.map(renderItem)}</nav>
899}
900
901export default function Page() {
902 return (
903 <div>
904 <Header />
905 <Sidebar />
906 </div>
907 )
908}
909```
910
911**Alternative with children prop:**
912
913```tsx
914async function Header() {
915 const data = await fetchHeader()
916 return <div>{data}</div>
917}
918
919async function Sidebar() {
920 const items = await fetchSidebarItems()
921 return <nav>{items.map(renderItem)}</nav>
922}
923
924function Layout({ children }: { children: ReactNode }) {
925 return (
926 <div>
927 <Header />
928 {children}
929 </div>
930 )
931}
932
933export default function Page() {
934 return (
935 <Layout>
936 <Sidebar />
937 </Layout>
938 )
939}
940```
941
942### 3.7 Per-Request Deduplication with React.cache()
943
944**Impact: MEDIUM (deduplicates within request)**
945
946Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
947
948**Usage:**
949
950```typescript
951import { cache } from 'react'
952
953export const getCurrentUser = cache(async () => {
954 const session = await auth()
955 if (!session?.user?.id) return null
956 return await db.user.findUnique({
957 where: { id: session.user.id }
958 })
959})
960```
961
962Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
963
964**Avoid inline objects as arguments:**
965
966`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
967
968**Incorrect: always cache miss**
969
970```typescript
971const getUser = cache(async (params: { uid: number }) => {
972 return await db.user.findUnique({ where: { id: params.uid } })
973})
974
975// Each call creates new object, never hits cache
976getUser({ uid: 1 })
977getUser({ uid: 1 }) // Cache miss, runs query again
978```
979
980**Correct: cache hit**
981
982```typescript
983const params = { uid: 1 }
984getUser(params) // Query runs
985getUser(params) // Cache hit (same reference)
986```
987
988If you must pass objects, pass the same reference:
989
990**Next.js-Specific Note:**
991
992In 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:
993
994- Database queries (Prisma, Drizzle, etc.)
995
996- Heavy computations
997
998- Authentication checks
999
1000- File system operations
1001
1002- Any non-fetch async work
1003
1004Use `React.cache()` to deduplicate these operations across your component tree.
1005
1006Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
1007
1008### 3.8 Use after() for Non-Blocking Operations
1009
1010**Impact: MEDIUM (faster response times)**
1011
1012Use 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.
1013
1014**Incorrect: blocks response**
1015
1016```tsx
1017import { logUserAction } from '@/app/utils'
1018
1019export async function POST(request: Request) {
1020 // Perform mutation
1021 await updateDatabase(request)
1022
1023 // Logging blocks the response
1024 const userAgent = request.headers.get('user-agent') || 'unknown'
1025 await logUserAction({ userAgent })
1026
1027 return new Response(JSON.stringify({ status: 'success' }), {
1028 status: 200,
1029 headers: { 'Content-Type': 'application/json' }
1030 })
1031}
1032```
1033
1034**Correct: non-blocking**
1035
1036```tsx
1037import { after } from 'next/server'
1038import { headers, cookies } from 'next/headers'
1039import { logUserAction } from '@/app/utils'
1040
1041export async function POST(request: Request) {
1042 // Perform mutation
1043 await updateDatabase(request)
1044
1045 // Log after response is sent
1046 after(async () => {
1047 const userAgent = (await headers()).get('user-agent') || 'unknown'
1048 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
1049
1050 logUserAction({ sessionCookie, userAgent })
1051 })
1052
1053 return new Response(JSON.stringify({ status: 'success' }), {
1054 status: 200,
1055 headers: { 'Content-Type': 'application/json' }
1056 })
1057}
1058```
1059
1060The response is sent immediately while logging happens in the background.
1061
1062**Common use cases:**
1063
1064- Analytics tracking
1065
1066- Audit logging
1067
1068- Sending notifications
1069
1070- Cache invalidation
1071
1072- Cleanup tasks
1073
1074**Important notes:**
1075
1076- `after()` runs even if the response fails or redirects
1077
1078- Works in Server Actions, Route Handlers, and Server Components
1079
1080Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1081
1082---
1083
1084## 4. Client-Side Data Fetching
1085
1086**Impact: MEDIUM-HIGH**
1087
1088Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1089
1090### 4.1 Deduplicate Global Event Listeners
1091
1092**Impact: LOW (single listener for N components)**
1093
1094Use `useSWRSubscription()` to share global event listeners across component instances.
1095
1096**Incorrect: N instances = N listeners**
1097
1098```tsx
1099function useKeyboardShortcut(key: string, callback: () => void) {
1100 useEffect(() => {
1101 const handler = (e: KeyboardEvent) => {
1102 if (e.metaKey && e.key === key) {
1103 callback()
1104 }
1105 }
1106 window.addEventListener('keydown', handler)
1107 return () => window.removeEventListener('keydown', handler)
1108 }, [key, callback])
1109}
1110```
1111
1112When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1113
1114**Correct: N instances = 1 listener**
1115
1116```tsx
1117import useSWRSubscription from 'swr/subscription'
1118
1119// Module-level Map to track callbacks per key
1120const keyCallbacks = new Map<string, Set<() => void>>()
1121
1122function useKeyboardShortcut(key: string, callback: () => void) {
1123 // Register this callback in the Map
1124 useEffect(() => {
1125 if (!keyCallbacks.has(key)) {
1126 keyCallbacks.set(key, new Set())
1127 }
1128 keyCallbacks.get(key)!.add(callback)
1129
1130 return () => {
1131 const set = keyCallbacks.get(key)
1132 if (set) {
1133 set.delete(callback)
1134 if (set.size === 0) {
1135 keyCallbacks.delete(key)
1136 }
1137 }
1138 }
1139 }, [key, callback])
1140
1141 useSWRSubscription('global-keydown', () => {
1142 const handler = (e: KeyboardEvent) => {
1143 if (e.metaKey && keyCallbacks.has(e.key)) {
1144 keyCallbacks.get(e.key)!.forEach(cb => cb())
1145 }
1146 }
1147 window.addEventListener('keydown', handler)
1148 return () => window.removeEventListener('keydown', handler)
1149 })
1150}
1151
1152function Profile() {
1153 // Multiple shortcuts will share the same listener
1154 useKeyboardShortcut('p', () => { /* ... */ })
1155 useKeyboardShortcut('k', () => { /* ... */ })
1156 // ...
1157}
1158```
1159
1160### 4.2 Use Passive Event Listeners for Scrolling Performance
1161
1162**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
1163
1164Add `{ 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.
1165
1166**Incorrect:**
1167
1168```typescript
1169useEffect(() => {
1170 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1171 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1172
1173 document.addEventListener('touchstart', handleTouch)
1174 document.addEventListener('wheel', handleWheel)
1175
1176 return () => {
1177 document.removeEventListener('touchstart', handleTouch)
1178 document.removeEventListener('wheel', handleWheel)
1179 }
1180}, [])
1181```
1182
1183**Correct:**
1184
1185```typescript
1186useEffect(() => {
1187 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1188 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1189
1190 document.addEventListener('touchstart', handleTouch, { passive: true })
1191 document.addEventListener('wheel', handleWheel, { passive: true })
1192
1193 return () => {
1194 document.removeEventListener('touchstart', handleTouch)
1195 document.removeEventListener('wheel', handleWheel)
1196 }
1197}, [])
1198```
1199
1200**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1201
1202**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1203
1204### 4.3 Use SWR for Automatic Deduplication
1205
1206**Impact: MEDIUM-HIGH (automatic deduplication)**
1207
1208SWR enables request deduplication, caching, and revalidation across component instances.
1209
1210**Incorrect: no deduplication, each instance fetches**
1211
1212```tsx
1213function UserList() {
1214 const [users, setUsers] = useState([])
1215 useEffect(() => {
1216 fetch('/api/users')
1217 .then(r => r.json())
1218 .then(setUsers)
1219 }, [])
1220}
1221```
1222
1223**Correct: multiple instances share one request**
1224
1225```tsx
1226import useSWR from 'swr'
1227
1228function UserList() {
1229 const { data: users } = useSWR('/api/users', fetcher)
1230}
1231```
1232
1233**For immutable data:**
1234
1235```tsx
1236import { useImmutableSWR } from '@/lib/swr'
1237
1238function StaticContent() {
1239 const { data } = useImmutableSWR('/api/config', fetcher)
1240}
1241```
1242
1243**For mutations:**
1244
1245```tsx
1246import { useSWRMutation } from 'swr/mutation'
1247
1248function UpdateButton() {
1249 const { trigger } = useSWRMutation('/api/user', updateUser)
1250 return <button onClick={() => trigger()}>Update</button>
1251}
1252```
1253
1254Reference: [https://swr.vercel.app](https://swr.vercel.app)
1255
1256### 4.4 Version and Minimize localStorage Data
1257
1258**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
1259
1260Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1261
1262**Incorrect:**
1263
1264```typescript
1265// No version, stores everything, no error handling
1266localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
1267const data = localStorage.getItem('userConfig')
1268```
1269
1270**Correct:**
1271
1272```typescript
1273const VERSION = 'v2'
1274
1275function saveConfig(config: { theme: string; language: string }) {
1276 try {
1277 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1278 } catch {
1279 // Throws in incognito/private browsing, quota exceeded, or disabled
1280 }
1281}
1282
1283function loadConfig() {
1284 try {
1285 const data = localStorage.getItem(`userConfig:${VERSION}`)
1286 return data ? JSON.parse(data) : null
1287 } catch {
1288 return null
1289 }
1290}
1291
1292// Migration from v1 to v2
1293function migrate() {
1294 try {
1295 const v1 = localStorage.getItem('userConfig:v1')
1296 if (v1) {
1297 const old = JSON.parse(v1)
1298 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
1299 localStorage.removeItem('userConfig:v1')
1300 }
1301 } catch {}
1302}
1303```
1304
1305**Store minimal fields from server responses:**
1306
1307```typescript
1308// User object has 20+ fields, only store what UI needs
1309function cachePrefs(user: FullUser) {
1310 try {
1311 localStorage.setItem('prefs:v1', JSON.stringify({
1312 theme: user.preferences.theme,
1313 notifications: user.preferences.notifications
1314 }))
1315 } catch {}
1316}
1317```
1318
1319**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1320
1321**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1322
1323---
1324
1325## 5. Re-render Optimization
1326
1327**Impact: MEDIUM**
1328
1329Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1330
1331### 5.1 Calculate Derived State During Rendering
1332
1333**Impact: MEDIUM (avoids redundant renders and state drift)**
1334
1335If 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.
1336
1337**Incorrect: redundant state and effect**
1338
1339```tsx
1340function Form() {
1341 const [firstName, setFirstName] = useState('First')
1342 const [lastName, setLastName] = useState('Last')
1343 const [fullName, setFullName] = useState('')
1344
1345 useEffect(() => {
1346 setFullName(firstName + ' ' + lastName)
1347 }, [firstName, lastName])
1348
1349 return <p>{fullName}</p>
1350}
1351```
1352
1353**Correct: derive during render**
1354
1355```tsx
1356function Form() {
1357 const [firstName, setFirstName] = useState('First')
1358 const [lastName, setLastName] = useState('Last')
1359 const fullName = firstName + ' ' + lastName
1360
1361 return <p>{fullName}</p>
1362}
1363```
1364
1365Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
1366
1367### 5.2 Defer State Reads to Usage Point
1368
1369**Impact: MEDIUM (avoids unnecessary subscriptions)**
1370
1371Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1372
1373**Incorrect: subscribes to all searchParams changes**
1374
1375```tsx
1376function ShareButton({ chatId }: { chatId: string }) {
1377 const searchParams = useSearchParams()
1378
1379 const handleShare = () => {
1380 const ref = searchParams.get('ref')
1381 shareChat(chatId, { ref })
1382 }
1383
1384 return <button onClick={handleShare}>Share</button>
1385}
1386```
1387
1388**Correct: reads on demand, no subscription**
1389
1390```tsx
1391function ShareButton({ chatId }: { chatId: string }) {
1392 const handleShare = () => {
1393 const params = new URLSearchParams(window.location.search)
1394 const ref = params.get('ref')
1395 shareChat(chatId, { ref })
1396 }
1397
1398 return <button onClick={handleShare}>Share</button>
1399}
1400```
1401
1402### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
1403
1404**Impact: LOW-MEDIUM (wasted computation on every render)**
1405
1406When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
1407
1408Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1409
1410**Incorrect:**
1411
1412```tsx
1413function Header({ user, notifications }: Props) {
1414 const isLoading = useMemo(() => {
1415 return user.isLoading || notifications.isLoading
1416 }, [user.isLoading, notifications.isLoading])
1417
1418 if (isLoading) return <Skeleton />
1419 // return some markup
1420}
1421```
1422
1423**Correct:**
1424
1425```tsx
1426function Header({ user, notifications }: Props) {
1427 const isLoading = user.isLoading || notifications.isLoading
1428
1429 if (isLoading) return <Skeleton />
1430 // return some markup
1431}
1432```
1433
1434### 5.4 Don't Define Components Inside Components
1435
1436**Impact: HIGH (prevents remount on every render)**
1437
1438Defining 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.
1439
1440A common reason developers do this is to access parent variables without passing props. Always pass props instead.
1441
1442**Incorrect: remounts on every render**
1443
1444```tsx
1445function UserProfile({ user, theme }) {
1446 // Defined inside to access `theme` - BAD
1447 const Avatar = () => (
1448 <img
1449 src={user.avatarUrl}
1450 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1451 />
1452 )
1453
1454 // Defined inside to access `user` - BAD
1455 const Stats = () => (
1456 <div>
1457 <span>{user.followers} followers</span>
1458 <span>{user.posts} posts</span>
1459 </div>
1460 )
1461
1462 return (
1463 <div>
1464 <Avatar />
1465 <Stats />
1466 </div>
1467 )
1468}
1469```
1470
1471Every 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.
1472
1473**Correct: pass props instead**
1474
1475```tsx
1476function Avatar({ src, theme }: { src: string; theme: string }) {
1477 return (
1478 <img
1479 src={src}
1480 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1481 />
1482 )
1483}
1484
1485function Stats({ followers, posts }: { followers: number; posts: number }) {
1486 return (
1487 <div>
1488 <span>{followers} followers</span>
1489 <span>{posts} posts</span>
1490 </div>
1491 )
1492}
1493
1494function UserProfile({ user, theme }) {
1495 return (
1496 <div>
1497 <Avatar src={user.avatarUrl} theme={theme} />
1498 <Stats followers={user.followers} posts={user.posts} />
1499 </div>
1500 )
1501}
1502```
1503
1504**Symptoms of this bug:**
1505
1506- Input fields lose focus on every keystroke
1507
1508- Animations restart unexpectedly
1509
1510- `useEffect` cleanup/setup runs on every parent render
1511
1512- Scroll position resets inside the component
1513
1514### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1515
1516**Impact: MEDIUM (restores memoization by using a constant for default value)**
1517
1518When 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()`.
1519
1520To address this issue, extract the default value into a constant.
1521
1522**Incorrect: `onClick` has different values on every rerender**
1523
1524```tsx
1525const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1526 // ...
1527})
1528
1529// Used without optional onClick
1530<UserAvatar />
1531```
1532
1533**Correct: stable default value**
1534
1535```tsx
1536const NOOP = () => {};
1537
1538const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1539 // ...
1540})
1541
1542// Used without optional onClick
1543<UserAvatar />
1544```
1545
1546### 5.6 Extract to Memoized Components
1547
1548**Impact: MEDIUM (enables early returns)**
1549
1550Extract expensive work into memoized components to enable early returns before computation.
1551
1552**Incorrect: computes avatar even when loading**
1553
1554```tsx
1555function Profile({ user, loading }: Props) {
1556 const avatar = useMemo(() => {
1557 const id = computeAvatarId(user)
1558 return <Avatar id={id} />
1559 }, [user])
1560
1561 if (loading) return <Skeleton />
1562 return <div>{avatar}</div>
1563}
1564```
1565
1566**Correct: skips computation when loading**
1567
1568```tsx
1569const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1570 const id = useMemo(() => computeAvatarId(user), [user])
1571 return <Avatar id={id} />
1572})
1573
1574function Profile({ user, loading }: Props) {
1575 if (loading) return <Skeleton />
1576 return (
1577 <div>
1578 <UserAvatar user={user} />
1579 </div>
1580 )
1581}
1582```
1583
1584**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.
1585
1586### 5.7 Narrow Effect Dependencies
1587
1588**Impact: LOW (minimizes effect re-runs)**
1589
1590Specify primitive dependencies instead of objects to minimize effect re-runs.
1591
1592**Incorrect: re-runs on any user field change**
1593
1594```tsx
1595useEffect(() => {
1596 console.log(user.id)
1597}, [user])
1598```
1599
1600**Correct: re-runs only when id changes**
1601
1602```tsx
1603useEffect(() => {
1604 console.log(user.id)
1605}, [user.id])
1606```
1607
1608**For derived state, compute outside effect:**
1609
1610```tsx
1611// Incorrect: runs on width=767, 766, 765...
1612useEffect(() => {
1613 if (width < 768) {
1614 enableMobileMode()
1615 }
1616}, [width])
1617
1618// Correct: runs only on boolean transition
1619const isMobile = width < 768
1620useEffect(() => {
1621 if (isMobile) {
1622 enableMobileMode()
1623 }
1624}, [isMobile])
1625```
1626
1627### 5.8 Put Interaction Logic in Event Handlers
1628
1629**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
1630
1631If 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.
1632
1633**Incorrect: event modeled as state + effect**
1634
1635```tsx
1636function Form() {
1637 const [submitted, setSubmitted] = useState(false)
1638 const theme = useContext(ThemeContext)
1639
1640 useEffect(() => {
1641 if (submitted) {
1642 post('/api/register')
1643 showToast('Registered', theme)
1644 }
1645 }, [submitted, theme])
1646
1647 return <button onClick={() => setSubmitted(true)}>Submit</button>
1648}
1649```
1650
1651**Correct: do it in the handler**
1652
1653```tsx
1654function Form() {
1655 const theme = useContext(ThemeContext)
1656
1657 function handleSubmit() {
1658 post('/api/register')
1659 showToast('Registered', theme)
1660 }
1661
1662 return <button onClick={handleSubmit}>Submit</button>
1663}
1664```
1665
1666Reference: [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)
1667
1668### 5.9 Subscribe to Derived State
1669
1670**Impact: MEDIUM (reduces re-render frequency)**
1671
1672Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1673
1674**Incorrect: re-renders on every pixel change**
1675
1676```tsx
1677function Sidebar() {
1678 const width = useWindowWidth() // updates continuously
1679 const isMobile = width < 768
1680 return <nav className={isMobile ? 'mobile' : 'desktop'} />
1681}
1682```
1683
1684**Correct: re-renders only when boolean changes**
1685
1686```tsx
1687function Sidebar() {
1688 const isMobile = useMediaQuery('(max-width: 767px)')
1689 return <nav className={isMobile ? 'mobile' : 'desktop'} />
1690}
1691```
1692
1693### 5.10 Use Functional setState Updates
1694
1695**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
1696
1697When 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.
1698
1699**Incorrect: requires state as dependency**
1700
1701```tsx
1702function TodoList() {
1703 const [items, setItems] = useState(initialItems)
1704
1705 // Callback must depend on items, recreated on every items change
1706 const addItems = useCallback((newItems: Item[]) => {
1707 setItems([...items, ...newItems])
1708 }, [items]) // ❌ items dependency causes recreations
1709
1710 // Risk of stale closure if dependency is forgotten
1711 const removeItem = useCallback((id: string) => {
1712 setItems(items.filter(item => item.id !== id))
1713 }, []) // ❌ Missing items dependency - will use stale items!
1714
1715 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1716}
1717```
1718
1719The 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.
1720
1721**Correct: stable callbacks, no stale closures**
1722
1723```tsx
1724function TodoList() {
1725 const [items, setItems] = useState(initialItems)
1726
1727 // Stable callback, never recreated
1728 const addItems = useCallback((newItems: Item[]) => {
1729 setItems(curr => [...curr, ...newItems])
1730 }, []) // ✅ No dependencies needed
1731
1732 // Always uses latest state, no stale closure risk
1733 const removeItem = useCallback((id: string) => {
1734 setItems(curr => curr.filter(item => item.id !== id))
1735 }, []) // ✅ Safe and stable
1736
1737 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1738}
1739```
1740
1741**Benefits:**
1742
17431. **Stable callback references** - Callbacks don't need to be recreated when state changes
1744
17452. **No stale closures** - Always operates on the latest state value
1746
17473. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
1748
17494. **Prevents bugs** - Eliminates the most common source of React closure bugs
1750
1751**When to use functional updates:**
1752
1753- Any setState that depends on the current state value
1754
1755- Inside useCallback/useMemo when state is needed
1756
1757- Event handlers that reference state
1758
1759- Async operations that update state
1760
1761**When direct updates are fine:**
1762
1763- Setting state to a static value: `setCount(0)`
1764
1765- Setting state from props/arguments only: `setName(newName)`
1766
1767- State doesn't depend on previous value
1768
1769**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.
1770
1771### 5.11 Use Lazy State Initialization
1772
1773**Impact: MEDIUM (wasted computation on every render)**
1774
1775Pass 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.
1776
1777**Incorrect: runs on every render**
1778
1779```tsx
1780function FilteredList({ items }: { items: Item[] }) {
1781 // buildSearchIndex() runs on EVERY render, even after initialization
1782 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
1783 const [query, setQuery] = useState('')
1784
1785 // When query changes, buildSearchIndex runs again unnecessarily
1786 return <SearchResults index={searchIndex} query={query} />
1787}
1788
1789function UserProfile() {
1790 // JSON.parse runs on every render
1791 const [settings, setSettings] = useState(
1792 JSON.parse(localStorage.getItem('settings') || '{}')
1793 )
1794
1795 return <SettingsForm settings={settings} onChange={setSettings} />
1796}
1797```
1798
1799**Correct: runs only once**
1800
1801```tsx
1802function FilteredList({ items }: { items: Item[] }) {
1803 // buildSearchIndex() runs ONLY on initial render
1804 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
1805 const [query, setQuery] = useState('')
1806
1807 return <SearchResults index={searchIndex} query={query} />
1808}
1809
1810function UserProfile() {
1811 // JSON.parse runs only on initial render
1812 const [settings, setSettings] = useState(() => {
1813 const stored = localStorage.getItem('settings')
1814 return stored ? JSON.parse(stored) : {}
1815 })
1816
1817 return <SettingsForm settings={settings} onChange={setSettings} />
1818}
1819```
1820
1821Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
1822
1823For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
1824
1825### 5.12 Use Transitions for Non-Urgent Updates
1826
1827**Impact: MEDIUM (maintains UI responsiveness)**
1828
1829Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
1830
1831**Incorrect: blocks UI on every scroll**
1832
1833```tsx
1834function ScrollTracker() {
1835 const [scrollY, setScrollY] = useState(0)
1836 useEffect(() => {
1837 const handler = () => setScrollY(window.scrollY)
1838 window.addEventListener('scroll', handler, { passive: true })
1839 return () => window.removeEventListener('scroll', handler)
1840 }, [])
1841}
1842```
1843
1844**Correct: non-blocking updates**
1845
1846```tsx
1847import { startTransition } from 'react'
1848
1849function ScrollTracker() {
1850 const [scrollY, setScrollY] = useState(0)
1851 useEffect(() => {
1852 const handler = () => {
1853 startTransition(() => setScrollY(window.scrollY))
1854 }
1855 window.addEventListener('scroll', handler, { passive: true })
1856 return () => window.removeEventListener('scroll', handler)
1857 }, [])
1858}
1859```
1860
1861### 5.13 Use useRef for Transient Values
1862
1863**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
1864
1865When 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.
1866
1867**Incorrect: renders every update**
1868
1869```tsx
1870function Tracker() {
1871 const [lastX, setLastX] = useState(0)
1872
1873 useEffect(() => {
1874 const onMove = (e: MouseEvent) => setLastX(e.clientX)
1875 window.addEventListener('mousemove', onMove)
1876 return () => window.removeEventListener('mousemove', onMove)
1877 }, [])
1878
1879 return (
1880 <div
1881 style={{
1882 position: 'fixed',
1883 top: 0,
1884 left: lastX,
1885 width: 8,
1886 height: 8,
1887 background: 'black',
1888 }}
1889 />
1890 )
1891}
1892```
1893
1894**Correct: no re-render for tracking**
1895
1896```tsx
1897function Tracker() {
1898 const lastXRef = useRef(0)
1899 const dotRef = useRef<HTMLDivElement>(null)
1900
1901 useEffect(() => {
1902 const onMove = (e: MouseEvent) => {
1903 lastXRef.current = e.clientX
1904 const node = dotRef.current
1905 if (node) {
1906 node.style.transform = `translateX(${e.clientX}px)`
1907 }
1908 }
1909 window.addEventListener('mousemove', onMove)
1910 return () => window.removeEventListener('mousemove', onMove)
1911 }, [])
1912
1913 return (
1914 <div
1915 ref={dotRef}
1916 style={{
1917 position: 'fixed',
1918 top: 0,
1919 left: 0,
1920 width: 8,
1921 height: 8,
1922 background: 'black',
1923 transform: 'translateX(0px)',
1924 }}
1925 />
1926 )
1927}
1928```
1929
1930---
1931
1932## 6. Rendering Performance
1933
1934**Impact: MEDIUM**
1935
1936Optimizing the rendering process reduces the work the browser needs to do.
1937
1938### 6.1 Animate SVG Wrapper Instead of SVG Element
1939
1940**Impact: LOW (enables hardware acceleration)**
1941
1942Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
1943
1944**Incorrect: animating SVG directly - no hardware acceleration**
1945
1946```tsx
1947function LoadingSpinner() {
1948 return (
1949 <svg
1950 className="animate-spin"
1951 width="24"
1952 height="24"
1953 viewBox="0 0 24 24"
1954 >
1955 <circle cx="12" cy="12" r="10" stroke="currentColor" />
1956 </svg>
1957 )
1958}
1959```
1960
1961**Correct: animating wrapper div - hardware accelerated**
1962
1963```tsx
1964function LoadingSpinner() {
1965 return (
1966 <div className="animate-spin">
1967 <svg
1968 width="24"
1969 height="24"
1970 viewBox="0 0 24 24"
1971 >
1972 <circle cx="12" cy="12" r="10" stroke="currentColor" />
1973 </svg>
1974 </div>
1975 )
1976}
1977```
1978
1979This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
1980
1981### 6.2 CSS content-visibility for Long Lists
1982
1983**Impact: HIGH (faster initial render)**
1984
1985Apply `content-visibility: auto` to defer off-screen rendering.
1986
1987**CSS:**
1988
1989```css
1990.message-item {
1991 content-visibility: auto;
1992 contain-intrinsic-size: 0 80px;
1993}
1994```
1995
1996**Example:**
1997
1998```tsx
1999function MessageList({ messages }: { messages: Message[] }) {
2000 return (
2001 <div className="overflow-y-auto h-screen">
2002 {messages.map(msg => (
2003 <div key={msg.id} className="message-item">
2004 <Avatar user={msg.author} />
2005 <div>{msg.content}</div>
2006 </div>
2007 ))}
2008 </div>
2009 )
2010}
2011```
2012
2013For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
2014
2015### 6.3 Hoist Static JSX Elements
2016
2017**Impact: LOW (avoids re-creation)**
2018
2019Extract static JSX outside components to avoid re-creation.
2020
2021**Incorrect: recreates element every render**
2022
2023```tsx
2024function LoadingSkeleton() {
2025 return <div className="animate-pulse h-20 bg-gray-200" />
2026}
2027
2028function Container() {
2029 return (
2030 <div>
2031 {loading && <LoadingSkeleton />}
2032 </div>
2033 )
2034}
2035```
2036
2037**Correct: reuses same element**
2038
2039```tsx
2040const loadingSkeleton = (
2041 <div className="animate-pulse h-20 bg-gray-200" />
2042)
2043
2044function Container() {
2045 return (
2046 <div>
2047 {loading && loadingSkeleton}
2048 </div>
2049 )
2050}
2051```
2052
2053This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
2054
2055**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.
2056
2057### 6.4 Optimize SVG Precision
2058
2059**Impact: LOW (reduces file size)**
2060
2061Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
2062
2063**Incorrect: excessive precision**
2064
2065```svg
2066<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
2067```
2068
2069**Correct: 1 decimal place**
2070
2071```svg
2072<path d="M 10.3 20.8 L 30.9 40.2" />
2073```
2074
2075**Automate with SVGO:**
2076
2077```bash
2078npx svgo --precision=1 --multipass icon.svg
2079```
2080
2081### 6.5 Prevent Hydration Mismatch Without Flickering
2082
2083**Impact: MEDIUM (avoids visual flicker and hydration errors)**
2084
2085When 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.
2086
2087**Incorrect: breaks SSR**
2088
2089```tsx
2090function ThemeWrapper({ children }: { children: ReactNode }) {
2091 // localStorage is not available on server - throws error
2092 const theme = localStorage.getItem('theme') || 'light'
2093
2094 return (
2095 <div className={theme}>
2096 {children}
2097 </div>
2098 )
2099}
2100```
2101
2102Server-side rendering will fail because `localStorage` is undefined.
2103
2104**Incorrect: visual flickering**
2105
2106```tsx
2107function ThemeWrapper({ children }: { children: ReactNode }) {
2108 const [theme, setTheme] = useState('light')
2109
2110 useEffect(() => {
2111 // Runs after hydration - causes visible flash
2112 const stored = localStorage.getItem('theme')
2113 if (stored) {
2114 setTheme(stored)
2115 }
2116 }, [])
2117
2118 return (
2119 <div className={theme}>
2120 {children}
2121 </div>
2122 )
2123}
2124```
2125
2126Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
2127
2128**Correct: no flicker, no hydration mismatch**
2129
2130```tsx
2131function ThemeWrapper({ children }: { children: ReactNode }) {
2132 return (
2133 <>
2134 <div id="theme-wrapper">
2135 {children}
2136 </div>
2137 <script
2138 dangerouslySetInnerHTML={{
2139 __html: `
2140 (function() {
2141 try {
2142 var theme = localStorage.getItem('theme') || 'light';
2143 var el = document.getElementById('theme-wrapper');
2144 if (el) el.className = theme;
2145 } catch (e) {}
2146 })();
2147 `,
2148 }}
2149 />
2150 </>
2151 )
2152}
2153```
2154
2155The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
2156
2157This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
2158
2159### 6.6 Suppress Expected Hydration Mismatches
2160
2161**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
2162
2163In 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.
2164
2165**Incorrect: known mismatch warnings**
2166
2167```tsx
2168function Timestamp() {
2169 return <span>{new Date().toLocaleString()}</span>
2170}
2171```
2172
2173**Correct: suppress expected mismatch only**
2174
2175```tsx
2176function Timestamp() {
2177 return (
2178 <span suppressHydrationWarning>
2179 {new Date().toLocaleString()}
2180 </span>
2181 )
2182}
2183```
2184
2185### 6.7 Use Activity Component for Show/Hide
2186
2187**Impact: MEDIUM (preserves state/DOM)**
2188
2189Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2190
2191**Usage:**
2192
2193```tsx
2194import { Activity } from 'react'
2195
2196function Dropdown({ isOpen }: Props) {
2197 return (
2198 <Activity mode={isOpen ? 'visible' : 'hidden'}>
2199 <ExpensiveMenu />
2200 </Activity>
2201 )
2202}
2203```
2204
2205Avoids expensive re-renders and state loss.
2206
2207### 6.8 Use defer or async on Script Tags
2208
2209**Impact: HIGH (eliminates render-blocking)**
2210
2211Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
2212
2213- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
2214
2215- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
2216
2217Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
2218
2219**Incorrect: blocks rendering**
2220
2221```tsx
2222export default function Document() {
2223 return (
2224 <html>
2225 <head>
2226 <script src="https://example.com/analytics.js" />
2227 <script src="/scripts/utils.js" />
2228 </head>
2229 <body>{/* content */}</body>
2230 </html>
2231 )
2232}
2233```
2234
2235**Correct: non-blocking**
2236
2237```tsx
2238import Script from 'next/script'
2239
2240export default function Page() {
2241 return (
2242 <>
2243 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />
2244 <Script src="/scripts/utils.js" strategy="beforeInteractive" />
2245 </>
2246 )
2247}
2248```
2249
2250**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
2251
2252Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
2253
2254### 6.9 Use Explicit Conditional Rendering
2255
2256**Impact: LOW (prevents rendering 0 or NaN)**
2257
2258Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2259
2260**Incorrect: renders "0" when count is 0**
2261
2262```tsx
2263function Badge({ count }: { count: number }) {
2264 return (
2265 <div>
2266 {count && <span className="badge">{count}</span>}
2267 </div>
2268 )
2269}
2270
2271// When count = 0, renders: <div>0</div>
2272// When count = 5, renders: <div><span class="badge">5</span></div>
2273```
2274
2275**Correct: renders nothing when count is 0**
2276
2277```tsx
2278function Badge({ count }: { count: number }) {
2279 return (
2280 <div>
2281 {count > 0 ? <span className="badge">{count}</span> : null}
2282 </div>
2283 )
2284}
2285
2286// When count = 0, renders: <div></div>
2287// When count = 5, renders: <div><span class="badge">5</span></div>
2288```
2289
2290### 6.10 Use React DOM Resource Hints
2291
2292**Impact: HIGH (reduces load time for critical resources)**
2293
2294React 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.
2295
2296- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
2297
2298- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
2299
2300- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
2301
2302- **`preloadModule(href)`**: Fetch an ES module you'll use soon
2303
2304- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
2305
2306- **`preinitModule(href)`**: Fetch and evaluate an ES module
2307
2308**Example: preconnect to third-party APIs**
2309
2310```tsx
2311import { preconnect, prefetchDNS } from 'react-dom'
2312
2313export default function App() {
2314 prefetchDNS('https://analytics.example.com')
2315 preconnect('https://api.example.com')
2316
2317 return <main>{/* content */}</main>
2318}
2319```
2320
2321**Example: preload critical fonts and styles**
2322
2323```tsx
2324import { preload, preinit } from 'react-dom'
2325
2326export default function RootLayout({ children }) {
2327 // Preload font file
2328 preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
2329
2330 // Fetch and apply critical stylesheet immediately
2331 preinit('/styles/critical.css', { as: 'style' })
2332
2333 return (
2334 <html>
2335 <body>{children}</body>
2336 </html>
2337 )
2338}
2339```
2340
2341**Example: preload modules for code-split routes**
2342
2343```tsx
2344import { preloadModule, preinitModule } from 'react-dom'
2345
2346function Navigation() {
2347 const preloadDashboard = () => {
2348 preloadModule('/dashboard.js', { as: 'script' })
2349 }
2350
2351 return (
2352 <nav>
2353 <a href="/dashboard" onMouseEnter={preloadDashboard}>
2354 Dashboard
2355 </a>
2356 </nav>
2357 )
2358}
2359```
2360
2361**When to use each:**
2362
2363| API | Use case |
2364
2365|-----|----------|
2366
2367| `prefetchDNS` | Third-party domains you'll connect to later |
2368
2369| `preconnect` | APIs or CDNs you'll fetch from immediately |
2370
2371| `preload` | Critical resources needed for current page |
2372
2373| `preloadModule` | JS modules for likely next navigation |
2374
2375| `preinit` | Stylesheets/scripts that must execute early |
2376
2377| `preinitModule` | ES modules that must execute early |
2378
2379Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)
2380
2381### 6.11 Use useTransition Over Manual Loading States
2382
2383**Impact: LOW (reduces re-renders and improves code clarity)**
2384
2385Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2386
2387**Incorrect: manual loading state**
2388
2389```tsx
2390function SearchResults() {
2391 const [query, setQuery] = useState('')
2392 const [results, setResults] = useState([])
2393 const [isLoading, setIsLoading] = useState(false)
2394
2395 const handleSearch = async (value: string) => {
2396 setIsLoading(true)
2397 setQuery(value)
2398 const data = await fetchResults(value)
2399 setResults(data)
2400 setIsLoading(false)
2401 }
2402
2403 return (
2404 <>
2405 <input onChange={(e) => handleSearch(e.target.value)} />
2406 {isLoading && <Spinner />}
2407 <ResultsList results={results} />
2408 </>
2409 )
2410}
2411```
2412
2413**Correct: useTransition with built-in pending state**
2414
2415```tsx
2416import { useTransition, useState } from 'react'
2417
2418function SearchResults() {
2419 const [query, setQuery] = useState('')
2420 const [results, setResults] = useState([])
2421 const [isPending, startTransition] = useTransition()
2422
2423 const handleSearch = (value: string) => {
2424 setQuery(value) // Update input immediately
2425
2426 startTransition(async () => {
2427 // Fetch and update results
2428 const data = await fetchResults(value)
2429 setResults(data)
2430 })
2431 }
2432
2433 return (
2434 <>
2435 <input onChange={(e) => handleSearch(e.target.value)} />
2436 {isPending && <Spinner />}
2437 <ResultsList results={results} />
2438 </>
2439 )
2440}
2441```
2442
2443**Benefits:**
2444
2445- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2446
2447- **Error resilience**: Pending state correctly resets even if the transition throws
2448
2449- **Better responsiveness**: Keeps the UI responsive during updates
2450
2451- **Interrupt handling**: New transitions automatically cancel pending ones
2452
2453Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
2454
2455---
2456
2457## 7. JavaScript Performance
2458
2459**Impact: LOW-MEDIUM**
2460
2461Micro-optimizations for hot paths can add up to meaningful improvements.
2462
2463### 7.1 Avoid Layout Thrashing
2464
2465**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
2466
2467Avoid 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.
2468
2469**This is OK: browser batches style changes**
2470
2471```typescript
2472function updateElementStyles(element: HTMLElement) {
2473 // Each line invalidates style, but browser batches the recalculation
2474 element.style.width = '100px'
2475 element.style.height = '200px'
2476 element.style.backgroundColor = 'blue'
2477 element.style.border = '1px solid black'
2478}
2479```
2480
2481**Incorrect: interleaved reads and writes force reflows**
2482
2483```typescript
2484function layoutThrashing(element: HTMLElement) {
2485 element.style.width = '100px'
2486 const width = element.offsetWidth // Forces reflow
2487 element.style.height = '200px'
2488 const height = element.offsetHeight // Forces another reflow
2489}
2490```
2491
2492**Correct: batch writes, then read once**
2493
2494```typescript
2495function updateElementStyles(element: HTMLElement) {
2496 // Batch all writes together
2497 element.style.width = '100px'
2498 element.style.height = '200px'
2499 element.style.backgroundColor = 'blue'
2500 element.style.border = '1px solid black'
2501
2502 // Read after all writes are done (single reflow)
2503 const { width, height } = element.getBoundingClientRect()
2504}
2505```
2506
2507**Correct: batch reads, then writes**
2508
2509```typescript
2510function updateElementStyles(element: HTMLElement) {
2511 element.classList.add('highlighted-box')
2512
2513 const { width, height } = element.getBoundingClientRect()
2514}
2515```
2516
2517**Better: use CSS classes**
2518
2519**React example:**
2520
2521```tsx
2522// Incorrect: interleaving style changes with layout queries
2523function Box({ isHighlighted }: { isHighlighted: boolean }) {
2524 const ref = useRef<HTMLDivElement>(null)
2525
2526 useEffect(() => {
2527 if (ref.current && isHighlighted) {
2528 ref.current.style.width = '100px'
2529 const width = ref.current.offsetWidth // Forces layout
2530 ref.current.style.height = '200px'
2531 }
2532 }, [isHighlighted])
2533
2534 return <div ref={ref}>Content</div>
2535}
2536
2537// Correct: toggle class
2538function Box({ isHighlighted }: { isHighlighted: boolean }) {
2539 return (
2540 <div className={isHighlighted ? 'highlighted-box' : ''}>
2541 Content
2542 </div>
2543 )
2544}
2545```
2546
2547Prefer 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.
2548
2549See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2550
2551### 7.2 Build Index Maps for Repeated Lookups
2552
2553**Impact: LOW-MEDIUM (1M ops to 2K ops)**
2554
2555Multiple `.find()` calls by the same key should use a Map.
2556
2557**Incorrect (O(n) per lookup):**
2558
2559```typescript
2560function processOrders(orders: Order[], users: User[]) {
2561 return orders.map(order => ({
2562 ...order,
2563 user: users.find(u => u.id === order.userId)
2564 }))
2565}
2566```
2567
2568**Correct (O(1) per lookup):**
2569
2570```typescript
2571function processOrders(orders: Order[], users: User[]) {
2572 const userById = new Map(users.map(u => [u.id, u]))
2573
2574 return orders.map(order => ({
2575 ...order,
2576 user: userById.get(order.userId)
2577 }))
2578}
2579```
2580
2581Build map once (O(n)), then all lookups are O(1).
2582
2583For 1000 orders × 1000 users: 1M ops → 2K ops.
2584
2585### 7.3 Cache Property Access in Loops
2586
2587**Impact: LOW-MEDIUM (reduces lookups)**
2588
2589Cache object property lookups in hot paths.
2590
2591**Incorrect: 3 lookups × N iterations**
2592
2593```typescript
2594for (let i = 0; i < arr.length; i++) {
2595 process(obj.config.settings.value)
2596}
2597```
2598
2599**Correct: 1 lookup total**
2600
2601```typescript
2602const value = obj.config.settings.value
2603const len = arr.length
2604for (let i = 0; i < len; i++) {
2605 process(value)
2606}
2607```
2608
2609### 7.4 Cache Repeated Function Calls
2610
2611**Impact: MEDIUM (avoid redundant computation)**
2612
2613Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2614
2615**Incorrect: redundant computation**
2616
2617```typescript
2618function ProjectList({ projects }: { projects: Project[] }) {
2619 return (
2620 <div>
2621 {projects.map(project => {
2622 // slugify() called 100+ times for same project names
2623 const slug = slugify(project.name)
2624
2625 return <ProjectCard key={project.id} slug={slug} />
2626 })}
2627 </div>
2628 )
2629}
2630```
2631
2632**Correct: cached results**
2633
2634```typescript
2635// Module-level cache
2636const slugifyCache = new Map<string, string>()
2637
2638function cachedSlugify(text: string): string {
2639 if (slugifyCache.has(text)) {
2640 return slugifyCache.get(text)!
2641 }
2642 const result = slugify(text)
2643 slugifyCache.set(text, result)
2644 return result
2645}
2646
2647function ProjectList({ projects }: { projects: Project[] }) {
2648 return (
2649 <div>
2650 {projects.map(project => {
2651 // Computed only once per unique project name
2652 const slug = cachedSlugify(project.name)
2653
2654 return <ProjectCard key={project.id} slug={slug} />
2655 })}
2656 </div>
2657 )
2658}
2659```
2660
2661**Simpler pattern for single-value functions:**
2662
2663```typescript
2664let isLoggedInCache: boolean | null = null
2665
2666function isLoggedIn(): boolean {
2667 if (isLoggedInCache !== null) {
2668 return isLoggedInCache
2669 }
2670
2671 isLoggedInCache = document.cookie.includes('auth=')
2672 return isLoggedInCache
2673}
2674
2675// Clear cache when auth changes
2676function onAuthChange() {
2677 isLoggedInCache = null
2678}
2679```
2680
2681Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2682
2683Reference: [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)
2684
2685### 7.5 Cache Storage API Calls
2686
2687**Impact: LOW-MEDIUM (reduces expensive I/O)**
2688
2689`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
2690
2691**Incorrect: reads storage on every call**
2692
2693```typescript
2694function getTheme() {
2695 return localStorage.getItem('theme') ?? 'light'
2696}
2697// Called 10 times = 10 storage reads
2698```
2699
2700**Correct: Map cache**
2701
2702```typescript
2703const storageCache = new Map<string, string | null>()
2704
2705function getLocalStorage(key: string) {
2706 if (!storageCache.has(key)) {
2707 storageCache.set(key, localStorage.getItem(key))
2708 }
2709 return storageCache.get(key)
2710}
2711
2712function setLocalStorage(key: string, value: string) {
2713 localStorage.setItem(key, value)
2714 storageCache.set(key, value) // keep cache in sync
2715}
2716```
2717
2718Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2719
2720**Cookie caching:**
2721
2722```typescript
2723let cookieCache: Record<string, string> | null = null
2724
2725function getCookie(name: string) {
2726 if (!cookieCache) {
2727 cookieCache = Object.fromEntries(
2728 document.cookie.split('; ').map(c => c.split('='))
2729 )
2730 }
2731 return cookieCache[name]
2732}
2733```
2734
2735**Important: invalidate on external changes**
2736
2737```typescript
2738window.addEventListener('storage', (e) => {
2739 if (e.key) storageCache.delete(e.key)
2740})
2741
2742document.addEventListener('visibilitychange', () => {
2743 if (document.visibilityState === 'visible') {
2744 storageCache.clear()
2745 }
2746})
2747```
2748
2749If storage can change externally (another tab, server-set cookies), invalidate cache:
2750
2751### 7.6 Combine Multiple Array Iterations
2752
2753**Impact: LOW-MEDIUM (reduces iterations)**
2754
2755Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
2756
2757**Incorrect: 3 iterations**
2758
2759```typescript
2760const admins = users.filter(u => u.isAdmin)
2761const testers = users.filter(u => u.isTester)
2762const inactive = users.filter(u => !u.isActive)
2763```
2764
2765**Correct: 1 iteration**
2766
2767```typescript
2768const admins: User[] = []
2769const testers: User[] = []
2770const inactive: User[] = []
2771
2772for (const user of users) {
2773 if (user.isAdmin) admins.push(user)
2774 if (user.isTester) testers.push(user)
2775 if (!user.isActive) inactive.push(user)
2776}
2777```
2778
2779### 7.7 Early Length Check for Array Comparisons
2780
2781**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
2782
2783When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
2784
2785In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
2786
2787**Incorrect: always runs expensive comparison**
2788
2789```typescript
2790function hasChanges(current: string[], original: string[]) {
2791 // Always sorts and joins, even when lengths differ
2792 return current.sort().join() !== original.sort().join()
2793}
2794```
2795
2796Two 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.
2797
2798**Correct (O(1) length check first):**
2799
2800```typescript
2801function hasChanges(current: string[], original: string[]) {
2802 // Early return if lengths differ
2803 if (current.length !== original.length) {
2804 return true
2805 }
2806 // Only sort when lengths match
2807 const currentSorted = current.toSorted()
2808 const originalSorted = original.toSorted()
2809 for (let i = 0; i < currentSorted.length; i++) {
2810 if (currentSorted[i] !== originalSorted[i]) {
2811 return true
2812 }
2813 }
2814 return false
2815}
2816```
2817
2818This new approach is more efficient because:
2819
2820- It avoids the overhead of sorting and joining the arrays when lengths differ
2821
2822- It avoids consuming memory for the joined strings (especially important for large arrays)
2823
2824- It avoids mutating the original arrays
2825
2826- It returns early when a difference is found
2827
2828### 7.8 Early Return from Functions
2829
2830**Impact: LOW-MEDIUM (avoids unnecessary computation)**
2831
2832Return early when result is determined to skip unnecessary processing.
2833
2834**Incorrect: processes all items even after finding answer**
2835
2836```typescript
2837function validateUsers(users: User[]) {
2838 let hasError = false
2839 let errorMessage = ''
2840
2841 for (const user of users) {
2842 if (!user.email) {
2843 hasError = true
2844 errorMessage = 'Email required'
2845 }
2846 if (!user.name) {
2847 hasError = true
2848 errorMessage = 'Name required'
2849 }
2850 // Continues checking all users even after error found
2851 }
2852
2853 return hasError ? { valid: false, error: errorMessage } : { valid: true }
2854}
2855```
2856
2857**Correct: returns immediately on first error**
2858
2859```typescript
2860function validateUsers(users: User[]) {
2861 for (const user of users) {
2862 if (!user.email) {
2863 return { valid: false, error: 'Email required' }
2864 }
2865 if (!user.name) {
2866 return { valid: false, error: 'Name required' }
2867 }
2868 }
2869
2870 return { valid: true }
2871}
2872```
2873
2874### 7.9 Hoist RegExp Creation
2875
2876**Impact: LOW-MEDIUM (avoids recreation)**
2877
2878Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
2879
2880**Incorrect: new RegExp every render**
2881
2882```tsx
2883function Highlighter({ text, query }: Props) {
2884 const regex = new RegExp(`(${query})`, 'gi')
2885 const parts = text.split(regex)
2886 return <>{parts.map((part, i) => ...)}</>
2887}
2888```
2889
2890**Correct: memoize or hoist**
2891
2892```tsx
2893const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2894
2895function Highlighter({ text, query }: Props) {
2896 const regex = useMemo(
2897 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
2898 [query]
2899 )
2900 const parts = text.split(regex)
2901 return <>{parts.map((part, i) => ...)}</>
2902}
2903```
2904
2905**Warning: global regex has mutable state**
2906
2907```typescript
2908const regex = /foo/g
2909regex.test('foo') // true, lastIndex = 3
2910regex.test('foo') // false, lastIndex = 0
2911```
2912
2913Global regex (`/g`) has mutable `lastIndex` state:
2914
2915### 7.10 Use flatMap to Map and Filter in One Pass
2916
2917**Impact: LOW-MEDIUM (eliminates intermediate array)**
2918
2919Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
2920
2921**Incorrect: 2 iterations, intermediate array**
2922
2923```typescript
2924const userNames = users
2925 .map(user => user.isActive ? user.name : null)
2926 .filter(Boolean)
2927```
2928
2929**Correct: 1 iteration, no intermediate array**
2930
2931```typescript
2932const userNames = users.flatMap(user =>
2933 user.isActive ? [user.name] : []
2934)
2935```
2936
2937**More examples:**
2938
2939```typescript
2940// Extract valid emails from responses
2941// Before
2942const emails = responses
2943 .map(r => r.success ? r.data.email : null)
2944 .filter(Boolean)
2945
2946// After
2947const emails = responses.flatMap(r =>
2948 r.success ? [r.data.email] : []
2949)
2950
2951// Parse and filter valid numbers
2952// Before
2953const numbers = strings
2954 .map(s => parseInt(s, 10))
2955 .filter(n => !isNaN(n))
2956
2957// After
2958const numbers = strings.flatMap(s => {
2959 const n = parseInt(s, 10)
2960 return isNaN(n) ? [] : [n]
2961})
2962```
2963
2964**When to use:**
2965
2966- Transforming items while filtering some out
2967
2968- Conditional mapping where some inputs produce no output
2969
2970- Parsing/validating where invalid inputs should be skipped
2971
2972### 7.11 Use Loop for Min/Max Instead of Sort
2973
2974**Impact: LOW (O(n) instead of O(n log n))**
2975
2976Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
2977
2978**Incorrect (O(n log n) - sort to find latest):**
2979
2980```typescript
2981interface Project {
2982 id: string
2983 name: string
2984 updatedAt: number
2985}
2986
2987function getLatestProject(projects: Project[]) {
2988 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
2989 return sorted[0]
2990}
2991```
2992
2993Sorts the entire array just to find the maximum value.
2994
2995**Incorrect (O(n log n) - sort for oldest and newest):**
2996
2997```typescript
2998function getOldestAndNewest(projects: Project[]) {
2999 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
3000 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
3001}
3002```
3003
3004Still sorts unnecessarily when only min/max are needed.
3005
3006**Correct (O(n) - single loop):**
3007
3008```typescript
3009function getLatestProject(projects: Project[]) {
3010 if (projects.length === 0) return null
3011
3012 let latest = projects[0]
3013
3014 for (let i = 1; i < projects.length; i++) {
3015 if (projects[i].updatedAt > latest.updatedAt) {
3016 latest = projects[i]
3017 }
3018 }
3019
3020 return latest
3021}
3022
3023function getOldestAndNewest(projects: Project[]) {
3024 if (projects.length === 0) return { oldest: null, newest: null }
3025
3026 let oldest = projects[0]
3027 let newest = projects[0]
3028
3029 for (let i = 1; i < projects.length; i++) {
3030 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
3031 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
3032 }
3033
3034 return { oldest, newest }
3035}
3036```
3037
3038Single pass through the array, no copying, no sorting.
3039
3040**Alternative: Math.min/Math.max for small arrays**
3041
3042```typescript
3043const numbers = [5, 2, 8, 1, 9]
3044const min = Math.min(...numbers)
3045const max = Math.max(...numbers)
3046```
3047
3048This 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.
3049
3050### 7.12 Use Set/Map for O(1) Lookups
3051
3052**Impact: LOW-MEDIUM (O(n) to O(1))**
3053
3054Convert arrays to Set/Map for repeated membership checks.
3055
3056**Incorrect (O(n) per check):**
3057
3058```typescript
3059const allowedIds = ['a', 'b', 'c', ...]
3060items.filter(item => allowedIds.includes(item.id))
3061```
3062
3063**Correct (O(1) per check):**
3064
3065```typescript
3066const allowedIds = new Set(['a', 'b', 'c', ...])
3067items.filter(item => allowedIds.has(item.id))
3068```
3069
3070### 7.13 Use toSorted() Instead of sort() for Immutability
3071
3072**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
3073
3074`.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.
3075
3076**Incorrect: mutates original array**
3077
3078```typescript
3079function UserList({ users }: { users: User[] }) {
3080 // Mutates the users prop array!
3081 const sorted = useMemo(
3082 () => users.sort((a, b) => a.name.localeCompare(b.name)),
3083 [users]
3084 )
3085 return <div>{sorted.map(renderUser)}</div>
3086}
3087```
3088
3089**Correct: creates new array**
3090
3091```typescript
3092function UserList({ users }: { users: User[] }) {
3093 // Creates new sorted array, original unchanged
3094 const sorted = useMemo(
3095 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
3096 [users]
3097 )
3098 return <div>{sorted.map(renderUser)}</div>
3099}
3100```
3101
3102**Why this matters in React:**
3103
31041. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
3105
31062. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
3107
3108**Browser support: fallback for older browsers**
3109
3110```typescript
3111// Fallback for older browsers
3112const sorted = [...items].sort((a, b) => a.value - b.value)
3113```
3114
3115`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
3116
3117**Other immutable array methods:**
3118
3119- `.toSorted()` - immutable sort
3120
3121- `.toReversed()` - immutable reverse
3122
3123- `.toSpliced()` - immutable splice
3124
3125- `.with()` - immutable element replacement
3126
3127---
3128
3129## 8. Advanced Patterns
3130
3131**Impact: LOW**
3132
3133Advanced patterns for specific cases that require careful implementation.
3134
3135### 8.1 Initialize App Once, Not Per Mount
3136
3137**Impact: LOW-MEDIUM (avoids duplicate init in development)**
3138
3139Do 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.
3140
3141**Incorrect: runs twice in dev, re-runs on remount**
3142
3143```tsx
3144function Comp() {
3145 useEffect(() => {
3146 loadFromStorage()
3147 checkAuthToken()
3148 }, [])
3149
3150 // ...
3151}
3152```
3153
3154**Correct: once per app load**
3155
3156```tsx
3157let didInit = false
3158
3159function Comp() {
3160 useEffect(() => {
3161 if (didInit) return
3162 didInit = true
3163 loadFromStorage()
3164 checkAuthToken()
3165 }, [])
3166
3167 // ...
3168}
3169```
3170
3171Reference: [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)
3172
3173### 8.2 Store Event Handlers in Refs
3174
3175**Impact: LOW (stable subscriptions)**
3176
3177Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
3178
3179**Incorrect: re-subscribes on every render**
3180
3181```tsx
3182function useWindowEvent(event: string, handler: (e) => void) {
3183 useEffect(() => {
3184 window.addEventListener(event, handler)
3185 return () => window.removeEventListener(event, handler)
3186 }, [event, handler])
3187}
3188```
3189
3190**Correct: stable subscription**
3191
3192```tsx
3193import { useEffectEvent } from 'react'
3194
3195function useWindowEvent(event: string, handler: (e) => void) {
3196 const onEvent = useEffectEvent(handler)
3197
3198 useEffect(() => {
3199 window.addEventListener(event, onEvent)
3200 return () => window.removeEventListener(event, onEvent)
3201 }, [event])
3202}
3203```
3204
3205**Alternative: use `useEffectEvent` if you're on latest React:**
3206
3207`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
3208
3209### 8.3 useEffectEvent for Stable Callback Refs
3210
3211**Impact: LOW (prevents effect re-runs)**
3212
3213Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
3214
3215**Incorrect: effect re-runs on every callback change**
3216
3217```tsx
3218function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3219 const [query, setQuery] = useState('')
3220
3221 useEffect(() => {
3222 const timeout = setTimeout(() => onSearch(query), 300)
3223 return () => clearTimeout(timeout)
3224 }, [query, onSearch])
3225}
3226```
3227
3228**Correct: using React's useEffectEvent**
3229
3230```tsx
3231import { useEffectEvent } from 'react';
3232
3233function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3234 const [query, setQuery] = useState('')
3235 const onSearchEvent = useEffectEvent(onSearch)
3236
3237 useEffect(() => {
3238 const timeout = setTimeout(() => onSearchEvent(query), 300)
3239 return () => clearTimeout(timeout)
3240 }, [query])
3241}
3242```
3243
3244---
3245
3246## References
3247
32481. [https://react.dev](https://react.dev)
32492. [https://nextjs.org](https://nextjs.org)
32503. [https://swr.vercel.app](https://swr.vercel.app)
32514. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
32525. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
32536. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
32547. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
3255
CherryHQ/cherry-studio · CLAUDE.md
@@ +1 @@
1## Guiding Principles (MUST FOLLOW)
2
3### Mindset
4
5How to approach any coding task in this repo.
6
7#### Think Before Coding
8
9- State assumptions explicitly. If uncertain, ask before implementing.
10- When multiple interpretations exist, surface them — do not pick silently.
11- If a simpler approach exists, say so. Push back when warranted.
12- If something is unclear, stop. Name what is confusing. Ask.
13
14#### Simplicity First
15
16- Write the minimum code that solves the problem. Nothing speculative.
17- No features beyond what was asked.
18- No abstractions for single-use code.
19- No "flexibility" or "configurability" that was not requested.
20- No error handling for impossible scenarios.
21- If you wrote 200 lines and it could be 50, rewrite it.
22- Inline comments cap at 2 lines. Needing more means the code is a patch — fix the implementation instead of narrating it. Say *why*, never restate *what*; no changelogs, no rationale essays, no pasted chat/review replies. (Doc comments on an exported API — TSDoc `@param`/`@returns`/`@deprecated` — are documentation, not narration, and are exempt.)
23
24#### Surgical Changes
25
26- Touch only what the task requires. Do not "improve" adjacent code, comments, or formatting.
27- Do not refactor things that are not broken.
28- Match existing style even if you would do it differently.
29- If you notice unrelated dead code, mention it — do not delete it.
30- Remove imports / variables / functions that **your** changes orphaned. Leave pre-existing dead code alone unless asked.
31- Every changed line must trace directly to the user's request.
32
33#### Goal-Driven Execution
34
35- Convert tasks into verifiable goals before coding:
36 - "Add validation" → "Write tests for invalid inputs, then make them pass."
37 - "Fix the bug" → "Write a test that reproduces it, then make it pass."
38 - "Refactor X" → "Ensure tests pass before and after."
39- For multi-step tasks, state a brief plan with explicit verification per step:
40
41```
421. [Step] → verify: [check]
432. [Step] → verify: [check]
44```
45
46### Operational Rules
47
48Project-specific tools, paths, and conventions.
49
50- **Keep it clear**: Write code that is easy to read, maintain, and explain.
51- **Read local READMEs first**: Before editing code in a directory, check for a `README.md` in that directory (and its parents) and read it — these files capture local conventions, invariants, and entry points that aren't obvious from the code alone.
52- **Fix upstream, don't hack downstream**: When a new feature hits an existing module's limitation, flag the upstream improvement for the user's decision before proposing a downstream workaround.
53- **Library-first, custom-last**: Before writing custom code, check library/framework docs for built-in options or existing solutions. Write custom code only when no adequate alternative exists.
54- **Build with Tailwind CSS & Shadcn UI**: Use components from `@cherrystudio/ui` (located in `packages/ui`, Shadcn UI + Tailwind CSS) for every new UI component.
55- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
56- **Access paths centrally**: Use `application.getPath('namespace.key', filename?)` for all main-process filesystem paths—never call `app.getPath()`, `os.homedir()`, or construct paths ad-hoc. Import the singleton via `import { application } from '@application'`.
57- **Lint, test, and format before completion**: Coding tasks are only complete after running `pnpm lint`, `pnpm test`, and `pnpm format` successfully.
58- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat(data-api):`, `fix(lifecycle):`, `refactor(quick-assistant):`, `docs(testing):`, `chore(deps):`, `test(window-manager):`). Scope must be a specific kebab-case module, never generic like `main` — when `git log` conflicts with this rule, this rule wins.
59- **Sign commits and sign off**: Every commit must be both cryptographically signed and DCO-signed off. Use `git commit -S --signoff` (not `--signoff` alone), verify the commit object contains a `gpgsig` header with `git cat-file commit HEAD`, and verify the pushed PR commits show `Verified` on GitHub.
60- **Target the right branch**: `main` is the default branch for all active development — submit features, refactors, optimizations, and fixes here.
61
62## Development
63
64### Commands
65
66Run `pnpm install` first (Node and pnpm versions are pinned in `package.json` — let it enforce them). For every other script, read `package.json` — the ones you must know:
67
68- `pnpm lint` — oxlint + eslint fix + typecheck + i18n check + format (writes files)
69- `pnpm test` — run all Vitest tests
70- `pnpm format` — Biome format + lint (write mode)
71- `pnpm build:check` — **REQUIRED before commits**. If it fails on i18n sort, run `pnpm i18n:sync` first; on formatting, run `pnpm format` first; on broken doc links, fix the link.
72- `pnpm test:lint` — the CI-equivalent lint gate: it denies oxlint warnings that `pnpm lint` / `pnpm build:check` silently tolerate; run it when CI must pass.
73
74### Testing
75
76- Tests run with Vitest 3 (see `vitest.config.*` for project setup).
77- **No behavior-pinning tests**: a test whose only assertion records what the code currently does — a snapshot of whatever came out, `toHaveBeenCalled` on a mock, an expected value re-derived the way the implementation derives it — has zero value. It cannot fail for a real reason, it breaks on every refactor, and it certifies existing bugs as "expected". Assert the contract instead: real input → the outcome the feature promises, plus the failure and edge cases. Before writing a test, state the bug it would catch; if you cannot, do not write it. **The existing suite is full of these** — delete the ones in a file you are already editing rather than keeping them green; a repo-wide purge is its own task, not a side effect of an unrelated PR.
78- **Frontend Tests — MUST READ**: [Frontend Testing Guidelines](docs/references/testing/frontend-testing.md).
79- **Test Mocking**: Use the unified mock system — do NOT create ad-hoc mocks for `application`, services, or data layers. See [tests/__mocks__/README.md](tests/__mocks__/README.md) for available mocks, usage patterns, and best practices.
80- **Database Tests**: For any service/handler/seeder that reads or writes SQLite, use `setupTestDatabase()` from `@test-helpers/db` — it provides a real file-backed DB with production migrations. Do NOT hand-write `CREATE TABLE` SQL, override `@application`, or stub Drizzle chains. See [docs/references/testing/database-testing.md](docs/references/testing/database-testing.md).
81
82### Patched Dependencies
83
84Before upgrading any dependency, check `patches/` for custom patches.
85
86## GitHub
87
88### Pull Requests
89
90Use the `gh-create-pr` skill. Fallback: read `.agents/skills/gh-create-pr/SKILL.md` directly.
91
92### Code Review
93
94When reviewing a GitHub PR, do NOT run `pnpm lint` / `pnpm test` / `pnpm format` locally — its CI already ran them; inspect via `gh` instead.
95
96### Issues
97
98Use the `gh-create-issue` skill. Fallback: read `.agents/skills/gh-create-issue/SKILL.md` directly.
99
100## Conventions
101
102### TypeScript
103
104- Cross-process types belong in `src/shared/`; renderer-only shared types in `src/renderer/types/` (see [Shared Layer Architecture](docs/references/shared-layer-architecture.md)).
105
106### Naming Conventions
107
108**MUST READ**: [docs/references/naming-conventions.md](docs/references/naming-conventions.md) — files, directories, identifiers, and singular/plural rules.
109
110### Logging
111
112```typescript
113import { loggerService } from "@logger";
114const logger = loggerService.withContext("moduleName");
115// Renderer only: loggerService.initWindowSource('windowName') first
116logger.info("message", CONTEXT);
117logger.warn("message");
118logger.error("message", error);
119```
120
121### Paths
122
123**MUST READ**: [src/main/core/paths/README.md](src/main/core/paths/README.md) — namespaces, naming, adding new keys, testing patterns. (Rule stated in Guiding Principle "Access paths centrally".)
124
125### i18n
126
127- All user-visible strings must use `i18next` — never hardcode UI strings
128- Run `pnpm i18n:check` to validate; `pnpm i18n:sync` to add missing keys
129- Locale files in `src/renderer/i18n/`
130
131### UI Design
132
133For any UI component or page style work, read [DESIGN.md](./DESIGN.md) first and follow its colors, fonts, spacing, and component specs strictly.
134
135## Architecture
136
137### Code Organization
138
139Where each file and directory belongs — read the doc for the process you're touching before adding code or opening a directory. Each process root's top level is a **closed set**: route new code into an existing category, never a new top-level directory ([Naming Conventions §4.8](docs/references/naming-conventions.md)).
140
141A directory's `index.ts` is a **barrel** — an enforced encapsulation boundary re-exporting one cohesive public API (internals private, outsiders import through it): re-export only (no logic / `export *`), no nesting, and it exists only if lint can seal off deep imports — else no barrel. `index.tsx` is always banned ([Naming Conventions §6.4](docs/references/naming-conventions.md)).
142
143- [Main Process Architecture](docs/references/main-process-architecture.md) — `src/main/` directories (`core`/`ipc`/`data`/`ai`/`features`/`services`/`utils`/`i18n`) and dependency direction.
144- [Renderer Architecture](docs/references/renderer-architecture.md) — `src/renderer/` two-axis (type × domain) layout and downward-only layering.
145- [Shared Layer Architecture](docs/references/shared-layer-architecture.md) — what belongs in `@shared` (cross-process + no mutable runtime state) and its closed top-level set.
146
147### Data
148
149**MUST READ**: [docs/references/data/README.md](docs/references/data/README.md) for system selection, architecture, and patterns.
150
151| System | Use Case | APIs |
152| ---------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
153| [BootConfig](docs/references/data/boot-config-overview.md) | Early boot settings (pre-lifecycle) | `bootConfigService.get()`, `usePreference('BootConfig.*')` |
154| [Cache](docs/references/data/cache-overview.md) | Temp data (can lose) | `useCache`, `useSharedCache`, `useSharedCacheValue`, `usePersistCache` |
155| [Preference](docs/references/data/preference-overview.md) | User settings | `usePreference` |
156| [DataApi](docs/references/data/data-api-overview.md) | Business data (**critical**) | `useQuery`, `useMutation` |
157
158Scope:
159
160- **BootConfig**: sync file-based; direct in main (pre-lifecycle), via `usePreference('BootConfig.*')` otherwise
161- **Cache**: memory / shared (cross-window) / persist tiers; memory + shared on both main and renderer; persist on both too but as **independent** stores (renderer = localStorage, main = JSON file at `{userData}/cache.json`), never shared — main additionally relays renderer persist sync between windows
162- **Preference**: cross-process (main + renderer); auto-syncs across windows
163- **DataApi**: SQLite-backed; no auto-sync, fetch on demand from renderer
164
165Database: SQLite via **better-sqlite3** + Drizzle ORM — the driver is **synchronous** (queries and transactions run inline with no `await`, unlike the app's otherwise-async data layers), so `getDb()` queries and `withWriteTx(fn)` callbacks must be written synchronously. Schemas in `src/main/data/db/schemas/`, migrations via `pnpm db:migrations:generate`
166
167**Write atomicity**: use `application.get('DbService').withWriteTx(fn)` to commit multiple writes (or a read-then-write) all-or-nothing in one synchronous `BEGIN IMMEDIATE` transaction; `fn` must be synchronous. A single write doesn't need it — better-sqlite3 runs each statement atomically on its one connection. See [Database Patterns — Write Serialization](docs/references/data/database-patterns.md#write-serialization-dbservicewritewritetx).
168
169**DataApi boundary rule**: DataApi is for SQLite-backed business data only. No database table → no DataApi endpoint; use IPC instead. See [Scope & Boundaries](docs/references/data/api-design-guidelines.md#dataapi-scope--boundaries).
170
171### IPC (IpcApi)
172
173**MUST READ**: [docs/references/ipc/README.md](docs/references/ipc/README.md) — paradigm boundary (RPC vs REST), schema/router/preload/facade layering, `IpcContext`, error model, security.
174
175Non-data command IPC (window/system/shell/notification/external/file) goes through **IpcApi** — the fifth subsystem alongside BootConfig/Cache/Preference/DataApi, RPC-over-IPC with single-point schemas (`schema + handler` to add a route; `ipcApi.request('namespace.action', input)` to call; `IpcApiService.broadcast`/`send` + `useIpcOn` for events). Legacy command IPC still coexists, so you'll encounter both. Decision: SQLite data → DataApi; user setting → Preference; losable/shared → Cache; everything else imperative → IpcApi.
176
177### Window Manager
178
179**MUST READ**: [docs/references/window-manager/README.md](docs/references/window-manager/README.md) — lifecycle modes, pool mechanics, API reference.
180
181All `BrowserWindow` goes through `WindowManager` with one of three modes (`default` / `singleton` / `pooled`), declared per type in `src/main/core/window/windowRegistry.ts`.
182
183- **Consumer API**: use only `open()` / `close()` — never `create()` / `destroy()` in business code.
184- **Attach listeners in `onWindowCreated`**, not after `open()` — reused windows skip the latter.
185- **Renderer reads init data via `useWindowInitData`**.
186
187### Main Process Services (Lifecycle)
188
189**MUST READ**: [docs/references/lifecycle/README.md](docs/references/lifecycle/README.md) — architecture, decision guides, usage patterns, and migration steps.
190
191All main-process services that own long-lived resources or register persistent side effects **must** use the lifecycle system:
192
193- **Extend `BaseService`**, apply `@Injectable`, `@ServicePhase`, `@DependsOn` decorators
194- **Register in `serviceRegistry.ts`** (`src/main/core/application/serviceRegistry.ts`) — one line per service
195- **Use `@DependsOn` for same-phase dependencies only** — do NOT declare dependencies on BeforeReady services (`PreferenceService`, `DbService`, `CacheService`, `DataApiService`) from WhenReady services; phase ordering is auto-enforced by the container
196- **Access via `application.get('Name')`** (or `getOptional()` for `@Conditional` services)
197- **Use `this.ipcHandle()` / `this.ipcOn()`** for IPC — auto-cleaned on stop/destroy, returns `Disposable`
198- **Use `this.registerInterval()`** for recurring timers — auto-unref'd, exception-isolated, auto-cleaned on stop/destroy, returns `Disposable`
199- **Use `this.registerDisposable()`** for cleanup tracking — accepts `Disposable` objects or `() => void` cleanup functions
200- **Use `Emitter<T>` / `Event<T>`** for inter-service events, **`Signal<T>`** for one-shot completion
201- **Implement `Activatable`** for services with heavy on-demand resources (IPC stays registered, resources load/release via `onActivate()`/`onDeactivate()`)
202- **Do NOT** use `new` or manual singleton patterns — the container manages instantiation, ordering, and shutdown
203
204For detailed code examples, see [Usage Guide](docs/references/lifecycle/lifecycle-usage.md). For migrating legacy services, see [Migration Guide](docs/references/lifecycle/lifecycle-migration-guide.md).
205
206### Non-Lifecycle Services (Direct-Import Singleton)
207
208Services without long-lived resources or persistent side effects: use **named export singleton** (`export const x = new X()`). No `getInstance()` patterns. See [Decision Guide](docs/references/lifecycle/lifecycle-decision-guide.md) for criteria.
209
210## Schema & Migration Rules
211
212The v2 refactor has landed. v1 data reaches v2 only through the migrators in `src/main/data/migration/v2/` — never add fallbacks, dual-writes, or guards for v1 save / read / loss.
213
214**The migration chain is no longer throwaway.** It was consolidated into a single clean initial migration and shipped with `v2.0.0-rc.1`, so `migrations/sqlite-drizzle/` now runs against databases holding real user rows. Never wipe or rewrite an already-shipped migration, and never tell a user to delete their database: schema changes go in as new appended migrations generated by `pnpm db:migrations:generate`. `src/main/data/db/schemas/` still changes freely — but every change must survive a migrate-forward on a populated database.
215
216**Resolving migration merge conflicts: regenerate, never rename.** When an upstream migration conflicts with your local one, delete your local `.sql` + its `meta/*_snapshot.json` and re-run `pnpm db:migrations:generate`. Renaming/renumbering instead silently reuses the snapshot's random `id`, forking the chain for everyone — and `drizzle-kit generate` still exits `0`; only `pnpm db:migrations:check` catches it. CI enforces both the chain check and a schema↔migration generate-and-diff step.
217
218### Data Classification Toolchain
219
220`v2-refactor-temp/tools/data-classify/` is the code generation pipeline for the v2 data layer; `classification.json` is the single source of truth (see its README). Four files are **auto-generated — NEVER edit them by hand**: `src/shared/data/preference/preferenceSchemas.ts`, `src/shared/data/bootConfig/bootConfigSchemas.ts`, and `PreferencesMappings.ts` + `BootConfigMappings.ts` in `src/main/data/migration/v2/migrators/mappings/`. To change them, edit `classification.json` or `target-key-definitions.json` (both in `data/`), then run `cd v2-refactor-temp/tools/data-classify && npm run generate`.
221
222### Breaking Changes Log
223
224When a v2 change is user-perceivable and affects how users use the app, add an entry under `v2-refactor-temp/docs/breaking-changes/`. See [v2-refactor-temp/docs/breaking-changes/README.md](v2-refactor-temp/docs/breaking-changes/README.md) for conventions.
225
226## Local Instructions
227
228If `CLAUDE.local.md` exists in the repository root (gitignored, may be absent), read it in full before acting on anything in this file — it holds the developer's private instructions and **OVERRIDES this file wherever they conflict**. Tools that auto-load it (e.g. Claude Code) need not re-read it.
229
@@ −1 +1 @@
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 [Defer Await Until Needed](#11-defer-await-until-needed)
25− - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
26− - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
27− - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
28− - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
29−2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
30− - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
31− - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
32− - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
33− - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
34− - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
35−3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
36− - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
37− - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
38− - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
39− - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level)
40− - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries)
41− - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition)
42− - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache)
43− - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations)
44−4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
45− - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
46− - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
47− - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
48− - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
49−5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
50− - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
51− - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
52− - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
53− - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)
54− - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
55− - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)
56− - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)
57− - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)
58− - 5.9 [Subscribe to Derived State](#59-subscribe-to-derived-state)
59− - 5.10 [Use Functional setState Updates](#510-use-functional-setstate-updates)
60− - 5.11 [Use Lazy State Initialization](#511-use-lazy-state-initialization)
61− - 5.12 [Use Transitions for Non-Urgent Updates](#512-use-transitions-for-non-urgent-updates)
62− - 5.13 [Use useRef for Transient Values](#513-use-useref-for-transient-values)
63−6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
64− - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
65− - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
66− - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
67− - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
68− - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
69− - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
70− - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
71− - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)
72− - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)
73− - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)
74− - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)
75−7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
76− - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
77− - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
78− - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
79− - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
80− - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
81− - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
82− - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
83− - 7.8 [Early Return from Functions](#78-early-return-from-functions)
84− - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
85− - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass)
86− - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort)
87− - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups)
88− - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability)
89−8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
90− - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
91− - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
92− - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
93−
94−---
95−
96−## 1. Eliminating Waterfalls
97−
98−**Impact: CRITICAL**
99−
100−Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
101−
102−### 1.1 Defer Await Until Needed
103−
104−**Impact: HIGH (avoids blocking unused code paths)**
105−
106−Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
107−
108−**Incorrect: blocks both branches**
109−
110−```typescript
111−async function handleRequest(userId: string, skipProcessing: boolean) {
112− const userData = await fetchUserData(userId)
113−
114− if (skipProcessing) {
115− // Returns immediately but still waited for userData
116− return { skipped: true }
117− }
118−
119− // Only this branch uses userData
120− return processUserData(userData)
121−}
122−```
123−
124−**Correct: only blocks when needed**
125−
126−```typescript
127−async function handleRequest(userId: string, skipProcessing: boolean) {
128− if (skipProcessing) {
129− // Returns immediately without waiting
130− return { skipped: true }
131− }
132−
133− // Fetch only when needed
134− const userData = await fetchUserData(userId)
135− return processUserData(userData)
136−}
137−```
138−
139−**Another example: early return optimization**
140−
141−```typescript
142−// Incorrect: always fetches permissions
143−async function updateResource(resourceId: string, userId: string) {
144− const permissions = await fetchPermissions(userId)
145− const resource = await getResource(resourceId)
146−
147− if (!resource) {
148− return { error: 'Not found' }
149− }
150−
151− if (!permissions.canEdit) {
152− return { error: 'Forbidden' }
153− }
154−
155− return await updateResourceData(resource, permissions)
156−}
157−
158−// Correct: fetches only when needed
159−async function updateResource(resourceId: string, userId: string) {
160− const resource = await getResource(resourceId)
161−
162− if (!resource) {
163− return { error: 'Not found' }
164− }
165−
166− const permissions = await fetchPermissions(userId)
167−
168− if (!permissions.canEdit) {
169− return { error: 'Forbidden' }
170− }
171−
172− return await updateResourceData(resource, permissions)
173−}
174−```
175−
176−This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
177−
178−### 1.2 Dependency-Based Parallelization
179−
180−**Impact: CRITICAL (2-10× improvement)**
181−
182−For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
183−
184−**Incorrect: profile waits for config unnecessarily**
185−
186−```typescript
187−const [user, config] = await Promise.all([
188− fetchUser(),
189− fetchConfig()
190−])
191−const profile = await fetchProfile(user.id)
192−```
193−
194−**Correct: config and profile run in parallel**
195−
196−```typescript
197−import { all } from 'better-all'
198−
199−const { user, config, profile } = await all({
200− async user() { return fetchUser() },
201− async config() { return fetchConfig() },
202− async profile() {
203− return fetchProfile((await this.$.user).id)
204− }
205−})
206−```
207−
208−**Alternative without extra dependencies:**
209−
210−```typescript
211−const userPromise = fetchUser()
212−const profilePromise = userPromise.then(user => fetchProfile(user.id))
213−
214−const [user, config, profile] = await Promise.all([
215− userPromise,
216− fetchConfig(),
217− profilePromise
218−])
219−```
220−
221−We can also create all the promises first, and do `Promise.all()` at the end.
222−
223−Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
224−
225−### 1.3 Prevent Waterfall Chains in API Routes
226−
227−**Impact: CRITICAL (2-10× improvement)**
228−
229−In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
230−
231−**Incorrect: config waits for auth, data waits for both**
232−
233−```typescript
234−export async function GET(request: Request) {
235− const session = await auth()
236− const config = await fetchConfig()
237− const data = await fetchData(session.user.id)
238− return Response.json({ data, config })
239−}
240−```
241−
242−**Correct: auth and config start immediately**
243−
244−```typescript
245−export async function GET(request: Request) {
246− const sessionPromise = auth()
247− const configPromise = fetchConfig()
248− const session = await sessionPromise
249− const [config, data] = await Promise.all([
250− configPromise,
251− fetchData(session.user.id)
252− ])
253− return Response.json({ data, config })
254−}
255−```
256−
257−For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
258−
259−### 1.4 Promise.all() for Independent Operations
260−
261−**Impact: CRITICAL (2-10× improvement)**
262−
263−When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
264−
265−**Incorrect: sequential execution, 3 round trips**
266−
267−```typescript
268−const user = await fetchUser()
269−const posts = await fetchPosts()
270−const comments = await fetchComments()
271−```
272−
273−**Correct: parallel execution, 1 round trip**
274−
275−```typescript
276−const [user, posts, comments] = await Promise.all([
277− fetchUser(),
278− fetchPosts(),
279− fetchComments()
280−])
281−```
282−
283−### 1.5 Strategic Suspense Boundaries
284−
285−**Impact: HIGH (faster initial paint)**
286−
287−Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
288−
289−**Incorrect: wrapper blocked by data fetching**
290−
291−```tsx
292−async function Page() {
293− const data = await fetchData() // Blocks entire page
294−
295− return (
296− <div>
297− <div>Sidebar</div>
298− <div>Header</div>
299− <div>
300− <DataDisplay data={data} />
301− </div>
302− <div>Footer</div>
303− </div>
304− )
305−}
306−```
307−
308−The entire layout waits for data even though only the middle section needs it.
309−
310−**Correct: wrapper shows immediately, data streams in**
311−
312−```tsx
313−function Page() {
314− return (
315− <div>
316− <div>Sidebar</div>
317− <div>Header</div>
318− <div>
319− <Suspense fallback={<Skeleton />}>
320− <DataDisplay />
321− </Suspense>
322− </div>
323− <div>Footer</div>
324− </div>
325− )
326−}
327−
328−async function DataDisplay() {
329− const data = await fetchData() // Only blocks this component
330− return <div>{data.content}</div>
331−}
332−```
333−
334−Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
335−
336−**Alternative: share promise across components**
337−
338−```tsx
339−function Page() {
340− // Start fetch immediately, but don't await
341− const dataPromise = fetchData()
342−
343− return (
344− <div>
345− <div>Sidebar</div>
346− <div>Header</div>
347− <Suspense fallback={<Skeleton />}>
348− <DataDisplay dataPromise={dataPromise} />
349− <DataSummary dataPromise={dataPromise} />
350− </Suspense>
351− <div>Footer</div>
352− </div>
353− )
354−}
355−
356−function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
357− const data = use(dataPromise) // Unwraps the promise
358− return <div>{data.content}</div>
359−}
360−
361−function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
362− const data = use(dataPromise) // Reuses the same promise
363− return <div>{data.summary}</div>
364−}
365−```
366−
367−Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
368−
369−**When NOT to use this pattern:**
370−
371−- Critical data needed for layout decisions (affects positioning)
372−
373−- SEO-critical content above the fold
374−
375−- Small, fast queries where suspense overhead isn't worth it
376−
377−- When you want to avoid layout shift (loading → content jump)
378−
379−**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
380−
381−---
382−
383−## 2. Bundle Size Optimization
384−
385−**Impact: CRITICAL**
386−
387−Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
388−
389−### 2.1 Avoid Barrel File Imports
390−
391−**Impact: CRITICAL (200-800ms import cost, slow builds)**
392−
393−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'`).
394−
395−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.
396−
397−**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.
398−
399−**Incorrect: imports entire library**
400−
401−```tsx
402−import { Check, X, Menu } from 'lucide-react'
403−// Loads 1,583 modules, takes ~2.8s extra in dev
404−// Runtime cost: 200-800ms on every cold start
405−
406−import { Button, TextField } from '@mui/material'
407−// Loads 2,225 modules, takes ~4.2s extra in dev
408−```
409−
410−**Correct: imports only what you need**
411−
412−```tsx
413−import Check from 'lucide-react/dist/esm/icons/check'
414−import X from 'lucide-react/dist/esm/icons/x'
415−import Menu from 'lucide-react/dist/esm/icons/menu'
416−// Loads only 3 modules (~2KB vs ~1MB)
417−
418−import Button from '@mui/material/Button'
419−import TextField from '@mui/material/TextField'
420−// Loads only what you use
421−```
422−
423−**Alternative: Next.js 13.5+**
424−
425−```js
426−// next.config.js - use optimizePackageImports
427−module.exports = {
428− experimental: {
429− optimizePackageImports: ['lucide-react', '@mui/material']
430− }
431−}
432−
433−// Then you can keep the ergonomic barrel imports:
434−import { Check, X, Menu } from 'lucide-react'
435−// Automatically transformed to direct imports at build time
436−```
437−
438−Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
439−
440−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`.
441−
442−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)
443−
444−### 2.2 Conditional Module Loading
445−
446−**Impact: HIGH (loads large data only when needed)**
447−
448−Load large data or modules only when a feature is activated.
449−
450−**Example: lazy-load animation frames**
451−
452−```tsx
453−function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
454− const [frames, setFrames] = useState<Frame[] | null>(null)
455−
456− useEffect(() => {
457− if (enabled && !frames && typeof window !== 'undefined') {
458− import('./animation-frames.js')
459− .then(mod => setFrames(mod.frames))
460− .catch(() => setEnabled(false))
461− }
462− }, [enabled, frames, setEnabled])
463−
464− if (!frames) return <Skeleton />
465− return <Canvas frames={frames} />
466−}
467−```
468−
469−The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
470−
471−### 2.3 Defer Non-Critical Third-Party Libraries
472−
473−**Impact: MEDIUM (loads after hydration)**
474−
475−Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
476−
477−**Incorrect: blocks initial bundle**
478−
479−```tsx
480−import { Analytics } from '@vercel/analytics/react'
481−
482−export default function RootLayout({ children }) {
483− return (
484− <html>
485− <body>
486− {children}
487− <Analytics />
488− </body>
489− </html>
490− )
491−}
492−```
493−
494−**Correct: loads after hydration**
495−
496−```tsx
497−import dynamic from 'next/dynamic'
498−
499−const Analytics = dynamic(
500− () => import('@vercel/analytics/react').then(m => m.Analytics),
501− { ssr: false }
502−)
503−
504−export default function RootLayout({ children }) {
505− return (
506− <html>
507− <body>
508− {children}
509− <Analytics />
510− </body>
511− </html>
512− )
513−}
514−```
515−
516−### 2.4 Dynamic Imports for Heavy Components
517−
518−**Impact: CRITICAL (directly affects TTI and LCP)**
519−
520−Use `next/dynamic` to lazy-load large components not needed on initial render.
521−
522−**Incorrect: Monaco bundles with main chunk ~300KB**
523−
524−```tsx
525−import { MonacoEditor } from './monaco-editor'
526−
527−function CodePanel({ code }: { code: string }) {
528− return <MonacoEditor value={code} />
529−}
530−```
531−
532−**Correct: Monaco loads on demand**
533−
534−```tsx
535−import dynamic from 'next/dynamic'
536−
537−const MonacoEditor = dynamic(
538− () => import('./monaco-editor').then(m => m.MonacoEditor),
539− { ssr: false }
540−)
541−
542−function CodePanel({ code }: { code: string }) {
543− return <MonacoEditor value={code} />
544−}
545−```
546−
547−### 2.5 Preload Based on User Intent
548−
549−**Impact: MEDIUM (reduces perceived latency)**
550−
551−Preload heavy bundles before they're needed to reduce perceived latency.
552−
553−**Example: preload on hover/focus**
554−
555−```tsx
556−function EditorButton({ onClick }: { onClick: () => void }) {
557− const preload = () => {
558− if (typeof window !== 'undefined') {
559− void import('./monaco-editor')
560− }
561− }
562−
563− return (
564− <button
565− onMouseEnter={preload}
566− onFocus={preload}
567− onClick={onClick}
568− >
569− Open Editor
570− </button>
571− )
572−}
573−```
574−
575−**Example: preload when feature flag is enabled**
576−
577−```tsx
578−function FlagsProvider({ children, flags }: Props) {
579− useEffect(() => {
580− if (flags.editorEnabled && typeof window !== 'undefined') {
581− void import('./monaco-editor').then(mod => mod.init())
582− }
583− }, [flags.editorEnabled])
584−
585− return <FlagsContext.Provider value={flags}>
586− {children}
587− </FlagsContext.Provider>
588−}
589−```
590−
591−The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
592−
593−---
594−
595−## 3. Server-Side Performance
596−
597−**Impact: HIGH**
598−
599−Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
600−
601−### 3.1 Authenticate Server Actions Like API Routes
602−
603−**Impact: CRITICAL (prevents unauthorized access to server mutations)**
604−
605−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.
606−
607−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."
608−
609−**Incorrect: no authentication check**
610−
611−```typescript
612−'use server'
613−
614−export async function deleteUser(userId: string) {
615− // Anyone can call this! No auth check
616− await db.user.delete({ where: { id: userId } })
617− return { success: true }
618−}
619−```
620−
621−**Correct: authentication inside the action**
622−
623−```typescript
624−'use server'
625−
626−import { verifySession } from '@/lib/auth'
627−import { unauthorized } from '@/lib/errors'
628−
629−export async function deleteUser(userId: string) {
630− // Always check auth inside the action
631− const session = await verifySession()
632−
633− if (!session) {
634− throw unauthorized('Must be logged in')
635− }
636−
637− // Check authorization too
638− if (session.user.role !== 'admin' && session.user.id !== userId) {
639− throw unauthorized('Cannot delete other users')
640− }
641−
642− await db.user.delete({ where: { id: userId } })
643− return { success: true }
644−}
645−```
646−
647−**With input validation:**
648−
649−```typescript
650−'use server'
651−
652−import { verifySession } from '@/lib/auth'
653−import { z } from 'zod'
654−
655−const updateProfileSchema = z.object({
656− userId: z.string().uuid(),
657− name: z.string().min(1).max(100),
658− email: z.string().email()
659−})
660−
661−export async function updateProfile(data: unknown) {
662− // Validate input first
663− const validated = updateProfileSchema.parse(data)
664−
665− // Then authenticate
666− const session = await verifySession()
667− if (!session) {
668− throw new Error('Unauthorized')
669− }
670−
671− // Then authorize
672− if (session.user.id !== validated.userId) {
673− throw new Error('Can only update own profile')
674− }
675−
676− // Finally perform the mutation
677− await db.user.update({
678− where: { id: validated.userId },
679− data: {
680− name: validated.name,
681− email: validated.email
682− }
683− })
684−
685− return { success: true }
686−}
687−```
688−
689−Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
690−
691−### 3.2 Avoid Duplicate Serialization in RSC Props
692−
693−**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
694−
695−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.
696−
697−**Incorrect: duplicates array**
698−
699−```tsx
700−// RSC: sends 6 strings (2 arrays × 3 items)
701−<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
702−```
703−
704−**Correct: sends 3 strings**
705−
706−```tsx
707−// RSC: send once
708−<ClientList usernames={usernames} />
709−
710−// Client: transform there
711−'use client'
712−const sorted = useMemo(() => [...usernames].sort(), [usernames])
713−```
714−
715−**Nested deduplication behavior:**
716−
717−```tsx
718−// string[] - duplicates everything
719−usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
720−
721−// object[] - duplicates array structure only
722−users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
723−```
724−
725−Deduplication works recursively. Impact varies by data type:
726−
727−- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
728−
729−- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
730−
731−**Operations breaking deduplication: create new references**
732−
733−- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
734−
735−- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
736−
737−**More examples:**
738−
739−```tsx
740−// ❌ Bad
741−<C users={users} active={users.filter(u => u.active)} />
742−<C product={product} productName={product.name} />
743−
744−// ✅ Good
745−<C users={users} />
746−<C product={product} />
747−// Do filtering/destructuring in client
748−```
749−
750−**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
751−
752−### 3.3 Cross-Request LRU Caching
753−
754−**Impact: HIGH (caches across requests)**
755−
756−`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
757−
758−**Implementation:**
759−
760−```typescript
761−import { LRUCache } from 'lru-cache'
762−
763−const cache = new LRUCache<string, any>({
764− max: 1000,
765− ttl: 5 * 60 * 1000 // 5 minutes
766−})
767−
768−export async function getUser(id: string) {
769− const cached = cache.get(id)
770− if (cached) return cached
771−
772− const user = await db.user.findUnique({ where: { id } })
773− cache.set(id, user)
774− return user
775−}
776−
777−// Request 1: DB query, result cached
778−// Request 2: cache hit, no DB query
779−```
780−
781−Use when sequential user actions hit multiple endpoints needing the same data within seconds.
782−
783−**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.
784−
785−**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
786−
787−Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
788−
789−### 3.4 Hoist Static I/O to Module Level
790−
791−**Impact: HIGH (avoids repeated file/network I/O per request)**
792−
793−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.
794−
795−**Incorrect: reads font file on every request**
796−
797−**Correct: loads once at module initialization**
798−
799−**Alternative: synchronous file reads with Node.js fs**
800−
801−**General Node.js example: loading config or templates**
802−
803−**When to use this pattern:**
804−
805−- Loading fonts for OG image generation
806−
807−- Loading static logos, icons, or watermarks
808−
809−- Reading configuration files that don't change at runtime
810−
811−- Loading email templates or other static templates
812−
813−- Any static asset that's the same across all requests
814−
815−**When NOT to use this pattern:**
816−
817−- Assets that vary per request or user
818−
819−- Files that may change during runtime (use caching with TTL instead)
820−
821−- Large files that would consume too much memory if kept loaded
822−
823−- Sensitive data that shouldn't persist in memory
824−
825−**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.
826−
827−**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
828−
829−### 3.5 Minimize Serialization at RSC Boundaries
830−
831−**Impact: HIGH (reduces data transfer size)**
832−
833−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.
834−
835−**Incorrect: serializes all 50 fields**
836−
837−```tsx
838−async function Page() {
839− const user = await fetchUser() // 50 fields
840− return <Profile user={user} />
841−}
842−
843−'use client'
844−function Profile({ user }: { user: User }) {
845− return <div>{user.name}</div> // uses 1 field
846−}
847−```
848−
849−**Correct: serializes only 1 field**
850−
851−```tsx
852−async function Page() {
853− const user = await fetchUser()
854− return <Profile name={user.name} />
855−}
856−
857−'use client'
858−function Profile({ name }: { name: string }) {
859− return <div>{name}</div>
860−}
861−```
862−
863−### 3.6 Parallel Data Fetching with Component Composition
864−
865−**Impact: CRITICAL (eliminates server-side waterfalls)**
866−
867−React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
868−
869−**Incorrect: Sidebar waits for Page's fetch to complete**
870−
871−```tsx
872−export default async function Page() {
873− const header = await fetchHeader()
874− return (
875− <div>
876− <div>{header}</div>
877− <Sidebar />
878− </div>
879− )
880−}
881−
882−async function Sidebar() {
883− const items = await fetchSidebarItems()
884− return <nav>{items.map(renderItem)}</nav>
885−}
886−```
887−
888−**Correct: both fetch simultaneously**
889−
890−```tsx
891−async function Header() {
892− const data = await fetchHeader()
893− return <div>{data}</div>
894−}
895−
896−async function Sidebar() {
897− const items = await fetchSidebarItems()
898− return <nav>{items.map(renderItem)}</nav>
899−}
900−
901−export default function Page() {
902− return (
903− <div>
904− <Header />
905− <Sidebar />
906− </div>
907− )
908−}
909−```
910−
911−**Alternative with children prop:**
912−
913−```tsx
914−async function Header() {
915− const data = await fetchHeader()
916− return <div>{data}</div>
917−}
918−
919−async function Sidebar() {
920− const items = await fetchSidebarItems()
921− return <nav>{items.map(renderItem)}</nav>
922−}
923−
924−function Layout({ children }: { children: ReactNode }) {
925− return (
926− <div>
927− <Header />
928− {children}
929− </div>
930− )
931−}
932−
933−export default function Page() {
934− return (
935− <Layout>
936− <Sidebar />
937− </Layout>
938− )
939−}
940−```
941−
942−### 3.7 Per-Request Deduplication with React.cache()
943−
944−**Impact: MEDIUM (deduplicates within request)**
945−
946−Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
947−
948−**Usage:**
949−
950−```typescript
951−import { cache } from 'react'
952−
953−export const getCurrentUser = cache(async () => {
954− const session = await auth()
955− if (!session?.user?.id) return null
956− return await db.user.findUnique({
957− where: { id: session.user.id }
958− })
959−})
960−```
961−
962−Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
963−
964−**Avoid inline objects as arguments:**
965−
966−`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
967−
968−**Incorrect: always cache miss**
969−
970−```typescript
971−const getUser = cache(async (params: { uid: number }) => {
972− return await db.user.findUnique({ where: { id: params.uid } })
973−})
974−
975−// Each call creates new object, never hits cache
976−getUser({ uid: 1 })
977−getUser({ uid: 1 }) // Cache miss, runs query again
978−```
979−
980−**Correct: cache hit**
981−
982−```typescript
983−const params = { uid: 1 }
984−getUser(params) // Query runs
985−getUser(params) // Cache hit (same reference)
986−```
987−
988−If you must pass objects, pass the same reference:
989−
990−**Next.js-Specific Note:**
991−
992−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:
993−
994−- Database queries (Prisma, Drizzle, etc.)
995−
996−- Heavy computations
997−
998−- Authentication checks
999−
1000−- File system operations
1001−
1002−- Any non-fetch async work
1003−
1004−Use `React.cache()` to deduplicate these operations across your component tree.
1005−
1006−Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
1007−
1008−### 3.8 Use after() for Non-Blocking Operations
1009−
1010−**Impact: MEDIUM (faster response times)**
1011−
1012−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.
1013−
1014−**Incorrect: blocks response**
1015−
1016−```tsx
1017−import { logUserAction } from '@/app/utils'
1018−
1019−export async function POST(request: Request) {
1020− // Perform mutation
1021− await updateDatabase(request)
1022−
1023− // Logging blocks the response
1024− const userAgent = request.headers.get('user-agent') || 'unknown'
1025− await logUserAction({ userAgent })
1026−
1027− return new Response(JSON.stringify({ status: 'success' }), {
1028− status: 200,
1029− headers: { 'Content-Type': 'application/json' }
1030− })
1031−}
1032−```
1033−
1034−**Correct: non-blocking**
1035−
1036−```tsx
1037−import { after } from 'next/server'
1038−import { headers, cookies } from 'next/headers'
1039−import { logUserAction } from '@/app/utils'
1040−
1041−export async function POST(request: Request) {
1042− // Perform mutation
1043− await updateDatabase(request)
1044−
1045− // Log after response is sent
1046− after(async () => {
1047− const userAgent = (await headers()).get('user-agent') || 'unknown'
1048− const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
1049−
1050− logUserAction({ sessionCookie, userAgent })
1051− })
1052−
1053− return new Response(JSON.stringify({ status: 'success' }), {
1054− status: 200,
1055− headers: { 'Content-Type': 'application/json' }
1056− })
1057−}
1058−```
1059−
1060−The response is sent immediately while logging happens in the background.
1061−
1062−**Common use cases:**
1063−
1064−- Analytics tracking
1065−
1066−- Audit logging
1067−
1068−- Sending notifications
1069−
1070−- Cache invalidation
1071−
1072−- Cleanup tasks
1073−
1074−**Important notes:**
1075−
1076−- `after()` runs even if the response fails or redirects
1077−
1078−- Works in Server Actions, Route Handlers, and Server Components
1079−
1080−Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1081−
1082−---
1083−
1084−## 4. Client-Side Data Fetching
1085−
1086−**Impact: MEDIUM-HIGH**
1087−
1088−Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1089−
1090−### 4.1 Deduplicate Global Event Listeners
1091−
1092−**Impact: LOW (single listener for N components)**
1093−
1094−Use `useSWRSubscription()` to share global event listeners across component instances.
1095−
1096−**Incorrect: N instances = N listeners**
1097−
1098−```tsx
1099−function useKeyboardShortcut(key: string, callback: () => void) {
1100− useEffect(() => {
1101− const handler = (e: KeyboardEvent) => {
1102− if (e.metaKey && e.key === key) {
1103− callback()
1104− }
1105− }
1106− window.addEventListener('keydown', handler)
1107− return () => window.removeEventListener('keydown', handler)
1108− }, [key, callback])
1109−}
1110−```
1111−
1112−When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1113−
1114−**Correct: N instances = 1 listener**
1115−
1116−```tsx
1117−import useSWRSubscription from 'swr/subscription'
1118−
1119−// Module-level Map to track callbacks per key
1120−const keyCallbacks = new Map<string, Set<() => void>>()
1121−
1122−function useKeyboardShortcut(key: string, callback: () => void) {
1123− // Register this callback in the Map
1124− useEffect(() => {
1125− if (!keyCallbacks.has(key)) {
1126− keyCallbacks.set(key, new Set())
1127− }
1128− keyCallbacks.get(key)!.add(callback)
1129−
1130− return () => {
1131− const set = keyCallbacks.get(key)
1132− if (set) {
1133− set.delete(callback)
1134− if (set.size === 0) {
1135− keyCallbacks.delete(key)
1136− }
1137− }
1138− }
1139− }, [key, callback])
1140−
1141− useSWRSubscription('global-keydown', () => {
1142− const handler = (e: KeyboardEvent) => {
1143− if (e.metaKey && keyCallbacks.has(e.key)) {
1144− keyCallbacks.get(e.key)!.forEach(cb => cb())
1145− }
1146− }
1147− window.addEventListener('keydown', handler)
1148− return () => window.removeEventListener('keydown', handler)
1149− })
1150−}
1151−
1152−function Profile() {
1153− // Multiple shortcuts will share the same listener
1154− useKeyboardShortcut('p', () => { /* ... */ })
1155− useKeyboardShortcut('k', () => { /* ... */ })
1156− // ...
1157−}
1158−```
1159−
1160−### 4.2 Use Passive Event Listeners for Scrolling Performance
1161−
1162−**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
1163−
1164−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.
1165−
1166−**Incorrect:**
1167−
1168−```typescript
1169−useEffect(() => {
1170− const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1171− const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1172−
1173− document.addEventListener('touchstart', handleTouch)
1174− document.addEventListener('wheel', handleWheel)
1175−
1176− return () => {
1177− document.removeEventListener('touchstart', handleTouch)
1178− document.removeEventListener('wheel', handleWheel)
1179− }
1180−}, [])
1181−```
1182−
1183−**Correct:**
1184−
1185−```typescript
1186−useEffect(() => {
1187− const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1188− const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1189−
1190− document.addEventListener('touchstart', handleTouch, { passive: true })
1191− document.addEventListener('wheel', handleWheel, { passive: true })
1192−
1193− return () => {
1194− document.removeEventListener('touchstart', handleTouch)
1195− document.removeEventListener('wheel', handleWheel)
1196− }
1197−}, [])
1198−```
1199−
1200−**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1201−
1202−**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1203−
1204−### 4.3 Use SWR for Automatic Deduplication
1205−
1206−**Impact: MEDIUM-HIGH (automatic deduplication)**
1207−
1208−SWR enables request deduplication, caching, and revalidation across component instances.
1209−
1210−**Incorrect: no deduplication, each instance fetches**
1211−
1212−```tsx
1213−function UserList() {
1214− const [users, setUsers] = useState([])
1215− useEffect(() => {
1216− fetch('/api/users')
1217− .then(r => r.json())
1218− .then(setUsers)
1219− }, [])
1220−}
1221−```
1222−
1223−**Correct: multiple instances share one request**
1224−
1225−```tsx
1226−import useSWR from 'swr'
1227−
1228−function UserList() {
1229− const { data: users } = useSWR('/api/users', fetcher)
1230−}
1231−```
1232−
1233−**For immutable data:**
1234−
1235−```tsx
1236−import { useImmutableSWR } from '@/lib/swr'
1237−
1238−function StaticContent() {
1239− const { data } = useImmutableSWR('/api/config', fetcher)
1240−}
1241−```
1242−
1243−**For mutations:**
1244−
1245−```tsx
1246−import { useSWRMutation } from 'swr/mutation'
1247−
1248−function UpdateButton() {
1249− const { trigger } = useSWRMutation('/api/user', updateUser)
1250− return <button onClick={() => trigger()}>Update</button>
1251−}
1252−```
1253−
1254−Reference: [https://swr.vercel.app](https://swr.vercel.app)
1255−
1256−### 4.4 Version and Minimize localStorage Data
1257−
1258−**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
1259−
1260−Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1261−
1262−**Incorrect:**
1263−
1264−```typescript
1265−// No version, stores everything, no error handling
1266−localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
1267−const data = localStorage.getItem('userConfig')
1268−```
1269−
1270−**Correct:**
1271−
1272−```typescript
1273−const VERSION = 'v2'
1274−
1275−function saveConfig(config: { theme: string; language: string }) {
1276− try {
1277− localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1278− } catch {
1279− // Throws in incognito/private browsing, quota exceeded, or disabled
1280− }
1281−}
1282−
1283−function loadConfig() {
1284− try {
1285− const data = localStorage.getItem(`userConfig:${VERSION}`)
1286− return data ? JSON.parse(data) : null
1287− } catch {
1288− return null
1289− }
1290−}
1291−
1292−// Migration from v1 to v2
1293−function migrate() {
1294− try {
1295− const v1 = localStorage.getItem('userConfig:v1')
1296− if (v1) {
1297− const old = JSON.parse(v1)
1298− saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
1299− localStorage.removeItem('userConfig:v1')
1300− }
1301− } catch {}
1302−}
1303−```
1304−
1305−**Store minimal fields from server responses:**
1306−
1307−```typescript
1308−// User object has 20+ fields, only store what UI needs
1309−function cachePrefs(user: FullUser) {
1310− try {
1311− localStorage.setItem('prefs:v1', JSON.stringify({
1312− theme: user.preferences.theme,
1313− notifications: user.preferences.notifications
1314− }))
1315− } catch {}
1316−}
1317−```
1318−
1319−**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1320−
1321−**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1322−
1323−---
1324−
1325−## 5. Re-render Optimization
1326−
1327−**Impact: MEDIUM**
1328−
1329−Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1330−
1331−### 5.1 Calculate Derived State During Rendering
1332−
1333−**Impact: MEDIUM (avoids redundant renders and state drift)**
1334−
1335−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.
1336−
1337−**Incorrect: redundant state and effect**
1338−
1339−```tsx
1340−function Form() {
1341− const [firstName, setFirstName] = useState('First')
1342− const [lastName, setLastName] = useState('Last')
1343− const [fullName, setFullName] = useState('')
1344−
1345− useEffect(() => {
1346− setFullName(firstName + ' ' + lastName)
1347− }, [firstName, lastName])
1348−
1349− return <p>{fullName}</p>
1350−}
1351−```
1352−
1353−**Correct: derive during render**
1354−
1355−```tsx
1356−function Form() {
1357− const [firstName, setFirstName] = useState('First')
1358− const [lastName, setLastName] = useState('Last')
1359− const fullName = firstName + ' ' + lastName
1360−
1361− return <p>{fullName}</p>
1362−}
1363−```
1364−
1365−Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
1366−
1367−### 5.2 Defer State Reads to Usage Point
1368−
1369−**Impact: MEDIUM (avoids unnecessary subscriptions)**
1370−
1371−Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1372−
1373−**Incorrect: subscribes to all searchParams changes**
1374−
1375−```tsx
1376−function ShareButton({ chatId }: { chatId: string }) {
1377− const searchParams = useSearchParams()
1378−
1379− const handleShare = () => {
1380− const ref = searchParams.get('ref')
1381− shareChat(chatId, { ref })
1382− }
1383−
1384− return <button onClick={handleShare}>Share</button>
1385−}
1386−```
1387−
1388−**Correct: reads on demand, no subscription**
1389−
1390−```tsx
1391−function ShareButton({ chatId }: { chatId: string }) {
1392− const handleShare = () => {
1393− const params = new URLSearchParams(window.location.search)
1394− const ref = params.get('ref')
1395− shareChat(chatId, { ref })
1396− }
1397−
1398− return <button onClick={handleShare}>Share</button>
1399−}
1400−```
1401−
1402−### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
1403−
1404−**Impact: LOW-MEDIUM (wasted computation on every render)**
1405−
1406−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`.
1407−
1408−Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1409−
1410−**Incorrect:**
1411−
1412−```tsx
1413−function Header({ user, notifications }: Props) {
1414− const isLoading = useMemo(() => {
1415− return user.isLoading || notifications.isLoading
1416− }, [user.isLoading, notifications.isLoading])
1417−
1418− if (isLoading) return <Skeleton />
1419− // return some markup
1420−}
1421−```
1422−
1423−**Correct:**
1424−
1425−```tsx
1426−function Header({ user, notifications }: Props) {
1427− const isLoading = user.isLoading || notifications.isLoading
1428−
1429− if (isLoading) return <Skeleton />
1430− // return some markup
1431−}
1432−```
1433−
1434−### 5.4 Don't Define Components Inside Components
1435−
1436−**Impact: HIGH (prevents remount on every render)**
1437−
1438−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.
1439−
1440−A common reason developers do this is to access parent variables without passing props. Always pass props instead.
1441−
1442−**Incorrect: remounts on every render**
1443−
1444−```tsx
1445−function UserProfile({ user, theme }) {
1446− // Defined inside to access `theme` - BAD
1447− const Avatar = () => (
1448− <img
1449− src={user.avatarUrl}
1450− className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1451− />
1452− )
1453−
1454− // Defined inside to access `user` - BAD
1455− const Stats = () => (
1456− <div>
1457− <span>{user.followers} followers</span>
1458− <span>{user.posts} posts</span>
1459− </div>
1460− )
1461−
1462− return (
1463− <div>
1464− <Avatar />
1465− <Stats />
1466− </div>
1467− )
1468−}
1469−```
1470−
1471−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.
1472−
1473−**Correct: pass props instead**
1474−
1475−```tsx
1476−function Avatar({ src, theme }: { src: string; theme: string }) {
1477− return (
1478− <img
1479− src={src}
1480− className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}
1481− />
1482− )
1483−}
1484−
1485−function Stats({ followers, posts }: { followers: number; posts: number }) {
1486− return (
1487− <div>
1488− <span>{followers} followers</span>
1489− <span>{posts} posts</span>
1490− </div>
1491− )
1492−}
1493−
1494−function UserProfile({ user, theme }) {
1495− return (
1496− <div>
1497− <Avatar src={user.avatarUrl} theme={theme} />
1498− <Stats followers={user.followers} posts={user.posts} />
1499− </div>
1500− )
1501−}
1502−```
1503−
1504−**Symptoms of this bug:**
1505−
1506−- Input fields lose focus on every keystroke
1507−
1508−- Animations restart unexpectedly
1509−
1510−- `useEffect` cleanup/setup runs on every parent render
1511−
1512−- Scroll position resets inside the component
1513−
1514−### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1515−
1516−**Impact: MEDIUM (restores memoization by using a constant for default value)**
1517−
1518−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()`.
1519−
1520−To address this issue, extract the default value into a constant.
1521−
1522−**Incorrect: `onClick` has different values on every rerender**
1523−
1524−```tsx
1525−const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1526− // ...
1527−})
1528−
1529−// Used without optional onClick
1530−<UserAvatar />
1531−```
1532−
1533−**Correct: stable default value**
1534−
1535−```tsx
1536−const NOOP = () => {};
1537−
1538−const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1539− // ...
1540−})
1541−
1542−// Used without optional onClick
1543−<UserAvatar />
1544−```
1545−
1546−### 5.6 Extract to Memoized Components
1547−
1548−**Impact: MEDIUM (enables early returns)**
1549−
1550−Extract expensive work into memoized components to enable early returns before computation.
1551−
1552−**Incorrect: computes avatar even when loading**
1553−
1554−```tsx
1555−function Profile({ user, loading }: Props) {
1556− const avatar = useMemo(() => {
1557− const id = computeAvatarId(user)
1558− return <Avatar id={id} />
1559− }, [user])
1560−
1561− if (loading) return <Skeleton />
1562− return <div>{avatar}</div>
1563−}
1564−```
1565−
1566−**Correct: skips computation when loading**
1567−
1568−```tsx
1569−const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1570− const id = useMemo(() => computeAvatarId(user), [user])
1571− return <Avatar id={id} />
1572−})
1573−
1574−function Profile({ user, loading }: Props) {
1575− if (loading) return <Skeleton />
1576− return (
1577− <div>
1578− <UserAvatar user={user} />
1579− </div>
1580− )
1581−}
1582−```
1583−
1584−**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.
1585−
1586−### 5.7 Narrow Effect Dependencies
1587−
1588−**Impact: LOW (minimizes effect re-runs)**
1589−
1590−Specify primitive dependencies instead of objects to minimize effect re-runs.
1591−
1592−**Incorrect: re-runs on any user field change**
1593−
1594−```tsx
1595−useEffect(() => {
1596− console.log(user.id)
1597−}, [user])
1598−```
1599−
1600−**Correct: re-runs only when id changes**
1601−
1602−```tsx
1603−useEffect(() => {
1604− console.log(user.id)
1605−}, [user.id])
1606−```
1607−
1608−**For derived state, compute outside effect:**
1609−
1610−```tsx
1611−// Incorrect: runs on width=767, 766, 765...
1612−useEffect(() => {
1613− if (width < 768) {
1614− enableMobileMode()
1615− }
1616−}, [width])
1617−
1618−// Correct: runs only on boolean transition
1619−const isMobile = width < 768
1620−useEffect(() => {
1621− if (isMobile) {
1622− enableMobileMode()
1623− }
1624−}, [isMobile])
1625−```
1626−
1627−### 5.8 Put Interaction Logic in Event Handlers
1628−
1629−**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
1630−
1631−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.
1632−
1633−**Incorrect: event modeled as state + effect**
1634−
1635−```tsx
1636−function Form() {
1637− const [submitted, setSubmitted] = useState(false)
1638− const theme = useContext(ThemeContext)
1639−
1640− useEffect(() => {
1641− if (submitted) {
1642− post('/api/register')
1643− showToast('Registered', theme)
1644− }
1645− }, [submitted, theme])
1646−
1647− return <button onClick={() => setSubmitted(true)}>Submit</button>
1648−}
1649−```
1650−
1651−**Correct: do it in the handler**
1652−
1653−```tsx
1654−function Form() {
1655− const theme = useContext(ThemeContext)
1656−
1657− function handleSubmit() {
1658− post('/api/register')
1659− showToast('Registered', theme)
1660− }
1661−
1662− return <button onClick={handleSubmit}>Submit</button>
1663−}
1664−```
1665−
1666−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)
1667−
1668−### 5.9 Subscribe to Derived State
1669−
1670−**Impact: MEDIUM (reduces re-render frequency)**
1671−
1672−Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1673−
1674−**Incorrect: re-renders on every pixel change**
1675−
1676−```tsx
1677−function Sidebar() {
1678− const width = useWindowWidth() // updates continuously
1679− const isMobile = width < 768
1680− return <nav className={isMobile ? 'mobile' : 'desktop'} />
1681−}
1682−```
1683−
1684−**Correct: re-renders only when boolean changes**
1685−
1686−```tsx
1687−function Sidebar() {
1688− const isMobile = useMediaQuery('(max-width: 767px)')
1689− return <nav className={isMobile ? 'mobile' : 'desktop'} />
1690−}
1691−```
1692−
1693−### 5.10 Use Functional setState Updates
1694−
1695−**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
1696−
1697−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.
1698−
1699−**Incorrect: requires state as dependency**
1700−
1701−```tsx
1702−function TodoList() {
1703− const [items, setItems] = useState(initialItems)
1704−
1705− // Callback must depend on items, recreated on every items change
1706− const addItems = useCallback((newItems: Item[]) => {
1707− setItems([...items, ...newItems])
1708− }, [items]) // ❌ items dependency causes recreations
1709−
1710− // Risk of stale closure if dependency is forgotten
1711− const removeItem = useCallback((id: string) => {
1712− setItems(items.filter(item => item.id !== id))
1713− }, []) // ❌ Missing items dependency - will use stale items!
1714−
1715− return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1716−}
1717−```
1718−
1719−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.
1720−
1721−**Correct: stable callbacks, no stale closures**
1722−
1723−```tsx
1724−function TodoList() {
1725− const [items, setItems] = useState(initialItems)
1726−
1727− // Stable callback, never recreated
1728− const addItems = useCallback((newItems: Item[]) => {
1729− setItems(curr => [...curr, ...newItems])
1730− }, []) // ✅ No dependencies needed
1731−
1732− // Always uses latest state, no stale closure risk
1733− const removeItem = useCallback((id: string) => {
1734− setItems(curr => curr.filter(item => item.id !== id))
1735− }, []) // ✅ Safe and stable
1736−
1737− return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1738−}
1739−```
1740−
1741−**Benefits:**
1742−
1743−1. **Stable callback references** - Callbacks don't need to be recreated when state changes
1744−
1745−2. **No stale closures** - Always operates on the latest state value
1746−
1747−3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
1748−
1749−4. **Prevents bugs** - Eliminates the most common source of React closure bugs
1750−
1751−**When to use functional updates:**
1752−
1753−- Any setState that depends on the current state value
1754−
1755−- Inside useCallback/useMemo when state is needed
1756−
1757−- Event handlers that reference state
1758−
1759−- Async operations that update state
1760−
1761−**When direct updates are fine:**
1762−
1763−- Setting state to a static value: `setCount(0)`
1764−
1765−- Setting state from props/arguments only: `setName(newName)`
1766−
1767−- State doesn't depend on previous value
1768−
1769−**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.
1770−
1771−### 5.11 Use Lazy State Initialization
1772−
1773−**Impact: MEDIUM (wasted computation on every render)**
1774−
1775−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.
1776−
1777−**Incorrect: runs on every render**
1778−
1779−```tsx
1780−function FilteredList({ items }: { items: Item[] }) {
1781− // buildSearchIndex() runs on EVERY render, even after initialization
1782− const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
1783− const [query, setQuery] = useState('')
1784−
1785− // When query changes, buildSearchIndex runs again unnecessarily
1786− return <SearchResults index={searchIndex} query={query} />
1787−}
1788−
1789−function UserProfile() {
1790− // JSON.parse runs on every render
1791− const [settings, setSettings] = useState(
1792− JSON.parse(localStorage.getItem('settings') || '{}')
1793− )
1794−
1795− return <SettingsForm settings={settings} onChange={setSettings} />
1796−}
1797−```
1798−
1799−**Correct: runs only once**
1800−
1801−```tsx
1802−function FilteredList({ items }: { items: Item[] }) {
1803− // buildSearchIndex() runs ONLY on initial render
1804− const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
1805− const [query, setQuery] = useState('')
1806−
1807− return <SearchResults index={searchIndex} query={query} />
1808−}
1809−
1810−function UserProfile() {
1811− // JSON.parse runs only on initial render
1812− const [settings, setSettings] = useState(() => {
1813− const stored = localStorage.getItem('settings')
1814− return stored ? JSON.parse(stored) : {}
1815− })
1816−
1817− return <SettingsForm settings={settings} onChange={setSettings} />
1818−}
1819−```
1820−
1821−Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
1822−
1823−For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
1824−
1825−### 5.12 Use Transitions for Non-Urgent Updates
1826−
1827−**Impact: MEDIUM (maintains UI responsiveness)**
1828−
1829−Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
1830−
1831−**Incorrect: blocks UI on every scroll**
1832−
1833−```tsx
1834−function ScrollTracker() {
1835− const [scrollY, setScrollY] = useState(0)
1836− useEffect(() => {
1837− const handler = () => setScrollY(window.scrollY)
1838− window.addEventListener('scroll', handler, { passive: true })
1839− return () => window.removeEventListener('scroll', handler)
1840− }, [])
1841−}
1842−```
1843−
1844−**Correct: non-blocking updates**
1845−
1846−```tsx
1847−import { startTransition } from 'react'
1848−
1849−function ScrollTracker() {
1850− const [scrollY, setScrollY] = useState(0)
1851− useEffect(() => {
1852− const handler = () => {
1853− startTransition(() => setScrollY(window.scrollY))
1854− }
1855− window.addEventListener('scroll', handler, { passive: true })
1856− return () => window.removeEventListener('scroll', handler)
1857− }, [])
1858−}
1859−```
1860−
1861−### 5.13 Use useRef for Transient Values
1862−
1863−**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
1864−
1865−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.
1866−
1867−**Incorrect: renders every update**
1868−
1869−```tsx
1870−function Tracker() {
1871− const [lastX, setLastX] = useState(0)
1872−
1873− useEffect(() => {
1874− const onMove = (e: MouseEvent) => setLastX(e.clientX)
1875− window.addEventListener('mousemove', onMove)
1876− return () => window.removeEventListener('mousemove', onMove)
1877− }, [])
1878−
1879− return (
1880− <div
1881− style={{
1882− position: 'fixed',
1883− top: 0,
1884− left: lastX,
1885− width: 8,
1886− height: 8,
1887− background: 'black',
1888− }}
1889− />
1890− )
1891−}
1892−```
1893−
1894−**Correct: no re-render for tracking**
1895−
1896−```tsx
1897−function Tracker() {
1898− const lastXRef = useRef(0)
1899− const dotRef = useRef<HTMLDivElement>(null)
1900−
1901− useEffect(() => {
1902− const onMove = (e: MouseEvent) => {
1903− lastXRef.current = e.clientX
1904− const node = dotRef.current
1905− if (node) {
1906− node.style.transform = `translateX(${e.clientX}px)`
1907− }
1908− }
1909− window.addEventListener('mousemove', onMove)
1910− return () => window.removeEventListener('mousemove', onMove)
1911− }, [])
1912−
1913− return (
1914− <div
1915− ref={dotRef}
1916− style={{
1917− position: 'fixed',
1918− top: 0,
1919− left: 0,
1920− width: 8,
1921− height: 8,
1922− background: 'black',
1923− transform: 'translateX(0px)',
1924− }}
1925− />
1926− )
1927−}
1928−```
1929−
1930−---
1931−
1932−## 6. Rendering Performance
1933−
1934−**Impact: MEDIUM**
1935−
1936−Optimizing the rendering process reduces the work the browser needs to do.
1937−
1938−### 6.1 Animate SVG Wrapper Instead of SVG Element
1939−
1940−**Impact: LOW (enables hardware acceleration)**
1941−
1942−Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
1943−
1944−**Incorrect: animating SVG directly - no hardware acceleration**
1945−
1946−```tsx
1947−function LoadingSpinner() {
1948− return (
1949− <svg
1950− className="animate-spin"
1951− width="24"
1952− height="24"
1953− viewBox="0 0 24 24"
1954− >
1955− <circle cx="12" cy="12" r="10" stroke="currentColor" />
1956− </svg>
1957− )
1958−}
1959−```
1960−
1961−**Correct: animating wrapper div - hardware accelerated**
1962−
1963−```tsx
1964−function LoadingSpinner() {
1965− return (
1966− <div className="animate-spin">
1967− <svg
1968− width="24"
1969− height="24"
1970− viewBox="0 0 24 24"
1971− >
1972− <circle cx="12" cy="12" r="10" stroke="currentColor" />
1973− </svg>
1974− </div>
1975− )
1976−}
1977−```
1978−
1979−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.
1980−
1981−### 6.2 CSS content-visibility for Long Lists
1982−
1983−**Impact: HIGH (faster initial render)**
1984−
1985−Apply `content-visibility: auto` to defer off-screen rendering.
1986−
1987−**CSS:**
1988−
1989−```css
1990−.message-item {
1991− content-visibility: auto;
1992− contain-intrinsic-size: 0 80px;
1993−}
1994−```
1995−
1996−**Example:**
1997−
1998−```tsx
1999−function MessageList({ messages }: { messages: Message[] }) {
2000− return (
2001− <div className="overflow-y-auto h-screen">
2002− {messages.map(msg => (
2003− <div key={msg.id} className="message-item">
2004− <Avatar user={msg.author} />
2005− <div>{msg.content}</div>
2006− </div>
2007− ))}
2008− </div>
2009− )
2010−}
2011−```
2012−
2013−For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
2014−
2015−### 6.3 Hoist Static JSX Elements
2016−
2017−**Impact: LOW (avoids re-creation)**
2018−
2019−Extract static JSX outside components to avoid re-creation.
2020−
2021−**Incorrect: recreates element every render**
2022−
2023−```tsx
2024−function LoadingSkeleton() {
2025− return <div className="animate-pulse h-20 bg-gray-200" />
2026−}
2027−
2028−function Container() {
2029− return (
2030− <div>
2031− {loading && <LoadingSkeleton />}
2032− </div>
2033− )
2034−}
2035−```
2036−
2037−**Correct: reuses same element**
2038−
2039−```tsx
2040−const loadingSkeleton = (
2041− <div className="animate-pulse h-20 bg-gray-200" />
2042−)
2043−
2044−function Container() {
2045− return (
2046− <div>
2047− {loading && loadingSkeleton}
2048− </div>
2049− )
2050−}
2051−```
2052−
2053−This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
2054−
2055−**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.
2056−
2057−### 6.4 Optimize SVG Precision
2058−
2059−**Impact: LOW (reduces file size)**
2060−
2061−Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
2062−
2063−**Incorrect: excessive precision**
2064−
2065−```svg
2066−<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
2067−```
2068−
2069−**Correct: 1 decimal place**
2070−
2071−```svg
2072−<path d="M 10.3 20.8 L 30.9 40.2" />
2073−```
2074−
2075−**Automate with SVGO:**
2076−
2077−```bash
2078−npx svgo --precision=1 --multipass icon.svg
2079−```
2080−
2081−### 6.5 Prevent Hydration Mismatch Without Flickering
2082−
2083−**Impact: MEDIUM (avoids visual flicker and hydration errors)**
2084−
2085−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.
2086−
2087−**Incorrect: breaks SSR**
2088−
2089−```tsx
2090−function ThemeWrapper({ children }: { children: ReactNode }) {
2091− // localStorage is not available on server - throws error
2092− const theme = localStorage.getItem('theme') || 'light'
2093−
2094− return (
2095− <div className={theme}>
2096− {children}
2097− </div>
2098− )
2099−}
2100−```
2101−
2102−Server-side rendering will fail because `localStorage` is undefined.
2103−
2104−**Incorrect: visual flickering**
2105−
2106−```tsx
2107−function ThemeWrapper({ children }: { children: ReactNode }) {
2108− const [theme, setTheme] = useState('light')
2109−
2110− useEffect(() => {
2111− // Runs after hydration - causes visible flash
2112− const stored = localStorage.getItem('theme')
2113− if (stored) {
2114− setTheme(stored)
2115− }
2116− }, [])
2117−
2118− return (
2119− <div className={theme}>
2120− {children}
2121− </div>
2122− )
2123−}
2124−```
2125−
2126−Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
2127−
2128−**Correct: no flicker, no hydration mismatch**
2129−
2130−```tsx
2131−function ThemeWrapper({ children }: { children: ReactNode }) {
2132− return (
2133− <>
2134− <div id="theme-wrapper">
2135− {children}
2136− </div>
2137− <script
2138− dangerouslySetInnerHTML={{
2139− __html: `
2140− (function() {
2141− try {
2142− var theme = localStorage.getItem('theme') || 'light';
2143− var el = document.getElementById('theme-wrapper');
2144− if (el) el.className = theme;
2145− } catch (e) {}
2146− })();
2147− `,
2148− }}
2149− />
2150− </>
2151− )
2152−}
2153−```
2154−
2155−The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
2156−
2157−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.
2158−
2159−### 6.6 Suppress Expected Hydration Mismatches
2160−
2161−**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
2162−
2163−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.
2164−
2165−**Incorrect: known mismatch warnings**
2166−
2167−```tsx
2168−function Timestamp() {
2169− return <span>{new Date().toLocaleString()}</span>
2170−}
2171−```
2172−
2173−**Correct: suppress expected mismatch only**
2174−
2175−```tsx
2176−function Timestamp() {
2177− return (
2178− <span suppressHydrationWarning>
2179− {new Date().toLocaleString()}
2180− </span>
2181− )
2182−}
2183−```
2184−
2185−### 6.7 Use Activity Component for Show/Hide
2186−
2187−**Impact: MEDIUM (preserves state/DOM)**
2188−
2189−Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2190−
2191−**Usage:**
2192−
2193−```tsx
2194−import { Activity } from 'react'
2195−
2196−function Dropdown({ isOpen }: Props) {
2197− return (
2198− <Activity mode={isOpen ? 'visible' : 'hidden'}>
2199− <ExpensiveMenu />
2200− </Activity>
2201− )
2202−}
2203−```
2204−
2205−Avoids expensive re-renders and state loss.
2206−
2207−### 6.8 Use defer or async on Script Tags
2208−
2209−**Impact: HIGH (eliminates render-blocking)**
2210−
2211−Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
2212−
2213−- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
2214−
2215−- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
2216−
2217−Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
2218−
2219−**Incorrect: blocks rendering**
2220−
2221−```tsx
2222−export default function Document() {
2223− return (
2224− <html>
2225− <head>
2226− <script src="https://example.com/analytics.js" />
2227− <script src="/scripts/utils.js" />
2228− </head>
2229− <body>{/* content */}</body>
2230− </html>
2231− )
2232−}
2233−```
2234−
2235−**Correct: non-blocking**
2236−
2237−```tsx
2238−import Script from 'next/script'
2239−
2240−export default function Page() {
2241− return (
2242− <>
2243− <Script src="https://example.com/analytics.js" strategy="afterInteractive" />
2244− <Script src="/scripts/utils.js" strategy="beforeInteractive" />
2245− </>
2246− )
2247−}
2248−```
2249−
2250−**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
2251−
2252−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)
2253−
2254−### 6.9 Use Explicit Conditional Rendering
2255−
2256−**Impact: LOW (prevents rendering 0 or NaN)**
2257−
2258−Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2259−
2260−**Incorrect: renders "0" when count is 0**
2261−
2262−```tsx
2263−function Badge({ count }: { count: number }) {
2264− return (
2265− <div>
2266− {count && <span className="badge">{count}</span>}
2267− </div>
2268− )
2269−}
2270−
2271−// When count = 0, renders: <div>0</div>
2272−// When count = 5, renders: <div><span class="badge">5</span></div>
2273−```
2274−
2275−**Correct: renders nothing when count is 0**
2276−
2277−```tsx
2278−function Badge({ count }: { count: number }) {
2279− return (
2280− <div>
2281− {count > 0 ? <span className="badge">{count}</span> : null}
2282− </div>
2283− )
2284−}
2285−
2286−// When count = 0, renders: <div></div>
2287−// When count = 5, renders: <div><span class="badge">5</span></div>
2288−```
2289−
2290−### 6.10 Use React DOM Resource Hints
2291−
2292−**Impact: HIGH (reduces load time for critical resources)**
2293−
2294−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.
2295−
2296−- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
2297−
2298−- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
2299−
2300−- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
2301−
2302−- **`preloadModule(href)`**: Fetch an ES module you'll use soon
2303−
2304−- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
2305−
2306−- **`preinitModule(href)`**: Fetch and evaluate an ES module
2307−
2308−**Example: preconnect to third-party APIs**
2309−
2310−```tsx
2311−import { preconnect, prefetchDNS } from 'react-dom'
2312−
2313−export default function App() {
2314− prefetchDNS('https://analytics.example.com')
2315− preconnect('https://api.example.com')
2316−
2317− return <main>{/* content */}</main>
2318−}
2319−```
2320−
2321−**Example: preload critical fonts and styles**
2322−
2323−```tsx
2324−import { preload, preinit } from 'react-dom'
2325−
2326−export default function RootLayout({ children }) {
2327− // Preload font file
2328− preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
2329−
2330− // Fetch and apply critical stylesheet immediately
2331− preinit('/styles/critical.css', { as: 'style' })
2332−
2333− return (
2334− <html>
2335− <body>{children}</body>
2336− </html>
2337− )
2338−}
2339−```
2340−
2341−**Example: preload modules for code-split routes**
2342−
2343−```tsx
2344−import { preloadModule, preinitModule } from 'react-dom'
2345−
2346−function Navigation() {
2347− const preloadDashboard = () => {
2348− preloadModule('/dashboard.js', { as: 'script' })
2349− }
2350−
2351− return (
2352− <nav>
2353− <a href="/dashboard" onMouseEnter={preloadDashboard}>
2354− Dashboard
2355− </a>
2356− </nav>
2357− )
2358−}
2359−```
2360−
2361−**When to use each:**
2362−
2363−| API | Use case |
2364−
2365−|-----|----------|
2366−
2367−| `prefetchDNS` | Third-party domains you'll connect to later |
2368−
2369−| `preconnect` | APIs or CDNs you'll fetch from immediately |
2370−
2371−| `preload` | Critical resources needed for current page |
2372−
2373−| `preloadModule` | JS modules for likely next navigation |
2374−
2375−| `preinit` | Stylesheets/scripts that must execute early |
2376−
2377−| `preinitModule` | ES modules that must execute early |
2378−
2379−Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)
2380−
2381−### 6.11 Use useTransition Over Manual Loading States
2382−
2383−**Impact: LOW (reduces re-renders and improves code clarity)**
2384−
2385−Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2386−
2387−**Incorrect: manual loading state**
2388−
2389−```tsx
2390−function SearchResults() {
2391− const [query, setQuery] = useState('')
2392− const [results, setResults] = useState([])
2393− const [isLoading, setIsLoading] = useState(false)
2394−
2395− const handleSearch = async (value: string) => {
2396− setIsLoading(true)
2397− setQuery(value)
2398− const data = await fetchResults(value)
2399− setResults(data)
2400− setIsLoading(false)
2401− }
2402−
2403− return (
2404− <>
2405− <input onChange={(e) => handleSearch(e.target.value)} />
2406− {isLoading && <Spinner />}
2407− <ResultsList results={results} />
2408− </>
2409− )
2410−}
2411−```
2412−
2413−**Correct: useTransition with built-in pending state**
2414−
2415−```tsx
2416−import { useTransition, useState } from 'react'
2417−
2418−function SearchResults() {
2419− const [query, setQuery] = useState('')
2420− const [results, setResults] = useState([])
2421− const [isPending, startTransition] = useTransition()
2422−
2423− const handleSearch = (value: string) => {
2424− setQuery(value) // Update input immediately
2425−
2426− startTransition(async () => {
2427− // Fetch and update results
2428− const data = await fetchResults(value)
2429− setResults(data)
2430− })
2431− }
2432−
2433− return (
2434− <>
2435− <input onChange={(e) => handleSearch(e.target.value)} />
2436− {isPending && <Spinner />}
2437− <ResultsList results={results} />
2438− </>
2439− )
2440−}
2441−```
2442−
2443−**Benefits:**
2444−
2445−- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2446−
2447−- **Error resilience**: Pending state correctly resets even if the transition throws
2448−
2449−- **Better responsiveness**: Keeps the UI responsive during updates
2450−
2451−- **Interrupt handling**: New transitions automatically cancel pending ones
2452−
2453−Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
2454−
2455−---
2456−
2457−## 7. JavaScript Performance
2458−
2459−**Impact: LOW-MEDIUM**
2460−
2461−Micro-optimizations for hot paths can add up to meaningful improvements.
2462−
2463−### 7.1 Avoid Layout Thrashing
2464−
2465−**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
2466−
2467−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.
2468−
2469−**This is OK: browser batches style changes**
2470−
2471−```typescript
2472−function updateElementStyles(element: HTMLElement) {
2473− // Each line invalidates style, but browser batches the recalculation
2474− element.style.width = '100px'
2475− element.style.height = '200px'
2476− element.style.backgroundColor = 'blue'
2477− element.style.border = '1px solid black'
2478−}
2479−```
2480−
2481−**Incorrect: interleaved reads and writes force reflows**
2482−
2483−```typescript
2484−function layoutThrashing(element: HTMLElement) {
2485− element.style.width = '100px'
2486− const width = element.offsetWidth // Forces reflow
2487− element.style.height = '200px'
2488− const height = element.offsetHeight // Forces another reflow
2489−}
2490−```
2491−
2492−**Correct: batch writes, then read once**
2493−
2494−```typescript
2495−function updateElementStyles(element: HTMLElement) {
2496− // Batch all writes together
2497− element.style.width = '100px'
2498− element.style.height = '200px'
2499− element.style.backgroundColor = 'blue'
2500− element.style.border = '1px solid black'
2501−
2502− // Read after all writes are done (single reflow)
2503− const { width, height } = element.getBoundingClientRect()
2504−}
2505−```
2506−
2507−**Correct: batch reads, then writes**
2508−
2509−```typescript
2510−function updateElementStyles(element: HTMLElement) {
2511− element.classList.add('highlighted-box')
2512−
2513− const { width, height } = element.getBoundingClientRect()
2514−}
2515−```
2516−
2517−**Better: use CSS classes**
2518−
2519−**React example:**
2520−
2521−```tsx
2522−// Incorrect: interleaving style changes with layout queries
2523−function Box({ isHighlighted }: { isHighlighted: boolean }) {
2524− const ref = useRef<HTMLDivElement>(null)
2525−
2526− useEffect(() => {
2527− if (ref.current && isHighlighted) {
2528− ref.current.style.width = '100px'
2529− const width = ref.current.offsetWidth // Forces layout
2530− ref.current.style.height = '200px'
2531− }
2532− }, [isHighlighted])
2533−
2534− return <div ref={ref}>Content</div>
2535−}
2536−
2537−// Correct: toggle class
2538−function Box({ isHighlighted }: { isHighlighted: boolean }) {
2539− return (
2540− <div className={isHighlighted ? 'highlighted-box' : ''}>
2541− Content
2542− </div>
2543− )
2544−}
2545−```
2546−
2547−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.
2548−
2549−See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2550−
2551−### 7.2 Build Index Maps for Repeated Lookups
2552−
2553−**Impact: LOW-MEDIUM (1M ops to 2K ops)**
2554−
2555−Multiple `.find()` calls by the same key should use a Map.
2556−
2557−**Incorrect (O(n) per lookup):**
2558−
2559−```typescript
2560−function processOrders(orders: Order[], users: User[]) {
2561− return orders.map(order => ({
2562− ...order,
2563− user: users.find(u => u.id === order.userId)
2564− }))
2565−}
2566−```
2567−
2568−**Correct (O(1) per lookup):**
2569−
2570−```typescript
2571−function processOrders(orders: Order[], users: User[]) {
2572− const userById = new Map(users.map(u => [u.id, u]))
2573−
2574− return orders.map(order => ({
2575− ...order,
2576− user: userById.get(order.userId)
2577− }))
2578−}
2579−```
2580−
2581−Build map once (O(n)), then all lookups are O(1).
2582−
2583−For 1000 orders × 1000 users: 1M ops → 2K ops.
2584−
2585−### 7.3 Cache Property Access in Loops
2586−
2587−**Impact: LOW-MEDIUM (reduces lookups)**
2588−
2589−Cache object property lookups in hot paths.
2590−
2591−**Incorrect: 3 lookups × N iterations**
2592−
2593−```typescript
2594−for (let i = 0; i < arr.length; i++) {
2595− process(obj.config.settings.value)
2596−}
2597−```
2598−
2599−**Correct: 1 lookup total**
2600−
2601−```typescript
2602−const value = obj.config.settings.value
2603−const len = arr.length
2604−for (let i = 0; i < len; i++) {
2605− process(value)
2606−}
2607−```
2608−
2609−### 7.4 Cache Repeated Function Calls
2610−
2611−**Impact: MEDIUM (avoid redundant computation)**
2612−
2613−Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2614−
2615−**Incorrect: redundant computation**
2616−
2617−```typescript
2618−function ProjectList({ projects }: { projects: Project[] }) {
2619− return (
2620− <div>
2621− {projects.map(project => {
2622− // slugify() called 100+ times for same project names
2623− const slug = slugify(project.name)
2624−
2625− return <ProjectCard key={project.id} slug={slug} />
2626− })}
2627− </div>
2628− )
2629−}
2630−```
2631−
2632−**Correct: cached results**
2633−
2634−```typescript
2635−// Module-level cache
2636−const slugifyCache = new Map<string, string>()
2637−
2638−function cachedSlugify(text: string): string {
2639− if (slugifyCache.has(text)) {
2640− return slugifyCache.get(text)!
2641− }
2642− const result = slugify(text)
2643− slugifyCache.set(text, result)
2644− return result
2645−}
2646−
2647−function ProjectList({ projects }: { projects: Project[] }) {
2648− return (
2649− <div>
2650− {projects.map(project => {
2651− // Computed only once per unique project name
2652− const slug = cachedSlugify(project.name)
2653−
2654− return <ProjectCard key={project.id} slug={slug} />
2655− })}
2656− </div>
2657− )
2658−}
2659−```
2660−
2661−**Simpler pattern for single-value functions:**
2662−
2663−```typescript
2664−let isLoggedInCache: boolean | null = null
2665−
2666−function isLoggedIn(): boolean {
2667− if (isLoggedInCache !== null) {
2668− return isLoggedInCache
2669− }
2670−
2671− isLoggedInCache = document.cookie.includes('auth=')
2672− return isLoggedInCache
2673−}
2674−
2675−// Clear cache when auth changes
2676−function onAuthChange() {
2677− isLoggedInCache = null
2678−}
2679−```
2680−
2681−Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2682−
2683−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)
2684−
2685−### 7.5 Cache Storage API Calls
2686−
2687−**Impact: LOW-MEDIUM (reduces expensive I/O)**
2688−
2689−`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
2690−
2691−**Incorrect: reads storage on every call**
2692−
2693−```typescript
2694−function getTheme() {
2695− return localStorage.getItem('theme') ?? 'light'
2696−}
2697−// Called 10 times = 10 storage reads
2698−```
2699−
2700−**Correct: Map cache**
2701−
2702−```typescript
2703−const storageCache = new Map<string, string | null>()
2704−
2705−function getLocalStorage(key: string) {
2706− if (!storageCache.has(key)) {
2707− storageCache.set(key, localStorage.getItem(key))
2708− }
2709− return storageCache.get(key)
2710−}
2711−
2712−function setLocalStorage(key: string, value: string) {
2713− localStorage.setItem(key, value)
2714− storageCache.set(key, value) // keep cache in sync
2715−}
2716−```
2717−
2718−Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2719−
2720−**Cookie caching:**
2721−
2722−```typescript
2723−let cookieCache: Record<string, string> | null = null
2724−
2725−function getCookie(name: string) {
2726− if (!cookieCache) {
2727− cookieCache = Object.fromEntries(
2728− document.cookie.split('; ').map(c => c.split('='))
2729− )
2730− }
2731− return cookieCache[name]
2732−}
2733−```
2734−
2735−**Important: invalidate on external changes**
2736−
2737−```typescript
2738−window.addEventListener('storage', (e) => {
2739− if (e.key) storageCache.delete(e.key)
2740−})
2741−
2742−document.addEventListener('visibilitychange', () => {
2743− if (document.visibilityState === 'visible') {
2744− storageCache.clear()
2745− }
2746−})
2747−```
2748−
2749−If storage can change externally (another tab, server-set cookies), invalidate cache:
2750−
2751−### 7.6 Combine Multiple Array Iterations
2752−
2753−**Impact: LOW-MEDIUM (reduces iterations)**
2754−
2755−Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
2756−
2757−**Incorrect: 3 iterations**
2758−
2759−```typescript
2760−const admins = users.filter(u => u.isAdmin)
2761−const testers = users.filter(u => u.isTester)
2762−const inactive = users.filter(u => !u.isActive)
2763−```
2764−
2765−**Correct: 1 iteration**
2766−
2767−```typescript
2768−const admins: User[] = []
2769−const testers: User[] = []
2770−const inactive: User[] = []
2771−
2772−for (const user of users) {
2773− if (user.isAdmin) admins.push(user)
2774− if (user.isTester) testers.push(user)
2775− if (!user.isActive) inactive.push(user)
2776−}
2777−```
2778−
2779−### 7.7 Early Length Check for Array Comparisons
2780−
2781−**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
2782−
2783−When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
2784−
2785−In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
2786−
2787−**Incorrect: always runs expensive comparison**
2788−
2789−```typescript
2790−function hasChanges(current: string[], original: string[]) {
2791− // Always sorts and joins, even when lengths differ
2792− return current.sort().join() !== original.sort().join()
2793−}
2794−```
2795−
2796−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.
2797−
2798−**Correct (O(1) length check first):**
2799−
2800−```typescript
2801−function hasChanges(current: string[], original: string[]) {
2802− // Early return if lengths differ
2803− if (current.length !== original.length) {
2804− return true
2805− }
2806− // Only sort when lengths match
2807− const currentSorted = current.toSorted()
2808− const originalSorted = original.toSorted()
2809− for (let i = 0; i < currentSorted.length; i++) {
2810− if (currentSorted[i] !== originalSorted[i]) {
2811− return true
2812− }
2813− }
2814− return false
2815−}
2816−```
2817−
2818−This new approach is more efficient because:
2819−
2820−- It avoids the overhead of sorting and joining the arrays when lengths differ
2821−
2822−- It avoids consuming memory for the joined strings (especially important for large arrays)
2823−
2824−- It avoids mutating the original arrays
2825−
2826−- It returns early when a difference is found
2827−
2828−### 7.8 Early Return from Functions
2829−
2830−**Impact: LOW-MEDIUM (avoids unnecessary computation)**
2831−
2832−Return early when result is determined to skip unnecessary processing.
2833−
2834−**Incorrect: processes all items even after finding answer**
2835−
2836−```typescript
2837−function validateUsers(users: User[]) {
2838− let hasError = false
2839− let errorMessage = ''
2840−
2841− for (const user of users) {
2842− if (!user.email) {
2843− hasError = true
2844− errorMessage = 'Email required'
2845− }
2846− if (!user.name) {
2847− hasError = true
2848− errorMessage = 'Name required'
2849− }
2850− // Continues checking all users even after error found
2851− }
2852−
2853− return hasError ? { valid: false, error: errorMessage } : { valid: true }
2854−}
2855−```
2856−
2857−**Correct: returns immediately on first error**
2858−
2859−```typescript
2860−function validateUsers(users: User[]) {
2861− for (const user of users) {
2862− if (!user.email) {
2863− return { valid: false, error: 'Email required' }
2864− }
2865− if (!user.name) {
2866− return { valid: false, error: 'Name required' }
2867− }
2868− }
2869−
2870− return { valid: true }
2871−}
2872−```
2873−
2874−### 7.9 Hoist RegExp Creation
2875−
2876−**Impact: LOW-MEDIUM (avoids recreation)**
2877−
2878−Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
2879−
2880−**Incorrect: new RegExp every render**
2881−
2882−```tsx
2883−function Highlighter({ text, query }: Props) {
2884− const regex = new RegExp(`(${query})`, 'gi')
2885− const parts = text.split(regex)
2886− return <>{parts.map((part, i) => ...)}</>
2887−}
2888−```
2889−
2890−**Correct: memoize or hoist**
2891−
2892−```tsx
2893−const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2894−
2895−function Highlighter({ text, query }: Props) {
2896− const regex = useMemo(
2897− () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
2898− [query]
2899− )
2900− const parts = text.split(regex)
2901− return <>{parts.map((part, i) => ...)}</>
2902−}
2903−```
2904−
2905−**Warning: global regex has mutable state**
2906−
2907−```typescript
2908−const regex = /foo/g
2909−regex.test('foo') // true, lastIndex = 3
2910−regex.test('foo') // false, lastIndex = 0
2911−```
2912−
2913−Global regex (`/g`) has mutable `lastIndex` state:
2914−
2915−### 7.10 Use flatMap to Map and Filter in One Pass
2916−
2917−**Impact: LOW-MEDIUM (eliminates intermediate array)**
2918−
2919−Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
2920−
2921−**Incorrect: 2 iterations, intermediate array**
2922−
2923−```typescript
2924−const userNames = users
2925− .map(user => user.isActive ? user.name : null)
2926− .filter(Boolean)
2927−```
2928−
2929−**Correct: 1 iteration, no intermediate array**
2930−
2931−```typescript
2932−const userNames = users.flatMap(user =>
2933− user.isActive ? [user.name] : []
2934−)
2935−```
2936−
2937−**More examples:**
2938−
2939−```typescript
2940−// Extract valid emails from responses
2941−// Before
2942−const emails = responses
2943− .map(r => r.success ? r.data.email : null)
2944− .filter(Boolean)
2945−
2946−// After
2947−const emails = responses.flatMap(r =>
2948− r.success ? [r.data.email] : []
2949−)
2950−
2951−// Parse and filter valid numbers
2952−// Before
2953−const numbers = strings
2954− .map(s => parseInt(s, 10))
2955− .filter(n => !isNaN(n))
2956−
2957−// After
2958−const numbers = strings.flatMap(s => {
2959− const n = parseInt(s, 10)
2960− return isNaN(n) ? [] : [n]
2961−})
2962−```
2963−
2964−**When to use:**
2965−
2966−- Transforming items while filtering some out
2967−
2968−- Conditional mapping where some inputs produce no output
2969−
2970−- Parsing/validating where invalid inputs should be skipped
2971−
2972−### 7.11 Use Loop for Min/Max Instead of Sort
2973−
2974−**Impact: LOW (O(n) instead of O(n log n))**
2975−
2976−Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
2977−
2978−**Incorrect (O(n log n) - sort to find latest):**
2979−
2980−```typescript
2981−interface Project {
2982− id: string
2983− name: string
2984− updatedAt: number
2985−}
2986−
2987−function getLatestProject(projects: Project[]) {
2988− const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
2989− return sorted[0]
2990−}
2991−```
2992−
2993−Sorts the entire array just to find the maximum value.
2994−
2995−**Incorrect (O(n log n) - sort for oldest and newest):**
2996−
2997−```typescript
2998−function getOldestAndNewest(projects: Project[]) {
2999− const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
3000− return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
3001−}
3002−```
3003−
3004−Still sorts unnecessarily when only min/max are needed.
3005−
3006−**Correct (O(n) - single loop):**
3007−
3008−```typescript
3009−function getLatestProject(projects: Project[]) {
3010− if (projects.length === 0) return null
3011−
3012− let latest = projects[0]
3013−
3014− for (let i = 1; i < projects.length; i++) {
3015− if (projects[i].updatedAt > latest.updatedAt) {
3016− latest = projects[i]
3017− }
3018− }
3019−
3020− return latest
3021−}
3022−
3023−function getOldestAndNewest(projects: Project[]) {
3024− if (projects.length === 0) return { oldest: null, newest: null }
3025−
3026− let oldest = projects[0]
3027− let newest = projects[0]
3028−
3029− for (let i = 1; i < projects.length; i++) {
3030− if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
3031− if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
3032− }
3033−
3034− return { oldest, newest }
3035−}
3036−```
3037−
3038−Single pass through the array, no copying, no sorting.
3039−
3040−**Alternative: Math.min/Math.max for small arrays**
3041−
3042−```typescript
3043−const numbers = [5, 2, 8, 1, 9]
3044−const min = Math.min(...numbers)
3045−const max = Math.max(...numbers)
3046−```
3047−
3048−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.
3049−
3050−### 7.12 Use Set/Map for O(1) Lookups
3051−
3052−**Impact: LOW-MEDIUM (O(n) to O(1))**
3053−
3054−Convert arrays to Set/Map for repeated membership checks.
3055−
3056−**Incorrect (O(n) per check):**
3057−
3058−```typescript
3059−const allowedIds = ['a', 'b', 'c', ...]
3060−items.filter(item => allowedIds.includes(item.id))
3061−```
3062−
3063−**Correct (O(1) per check):**
3064−
3065−```typescript
3066−const allowedIds = new Set(['a', 'b', 'c', ...])
3067−items.filter(item => allowedIds.has(item.id))
3068−```
3069−
3070−### 7.13 Use toSorted() Instead of sort() for Immutability
3071−
3072−**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
3073−
3074−`.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.
3075−
3076−**Incorrect: mutates original array**
3077−
3078−```typescript
3079−function UserList({ users }: { users: User[] }) {
3080− // Mutates the users prop array!
3081− const sorted = useMemo(
3082− () => users.sort((a, b) => a.name.localeCompare(b.name)),
3083− [users]
3084− )
3085− return <div>{sorted.map(renderUser)}</div>
3086−}
3087−```
3088−
3089−**Correct: creates new array**
3090−
3091−```typescript
3092−function UserList({ users }: { users: User[] }) {
3093− // Creates new sorted array, original unchanged
3094− const sorted = useMemo(
3095− () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
3096− [users]
3097− )
3098− return <div>{sorted.map(renderUser)}</div>
3099−}
3100−```
3101−
3102−**Why this matters in React:**
3103−
3104−1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
3105−
3106−2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
3107−
3108−**Browser support: fallback for older browsers**
3109−
3110−```typescript
3111−// Fallback for older browsers
3112−const sorted = [...items].sort((a, b) => a.value - b.value)
3113−```
3114−
3115−`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
3116−
3117−**Other immutable array methods:**
3118−
3119−- `.toSorted()` - immutable sort
3120−
3121−- `.toReversed()` - immutable reverse
3122−
3123−- `.toSpliced()` - immutable splice
3124−
3125−- `.with()` - immutable element replacement
3126−
3127−---
3128−
3129−## 8. Advanced Patterns
3130−
3131−**Impact: LOW**
3132−
3133−Advanced patterns for specific cases that require careful implementation.
3134−
3135−### 8.1 Initialize App Once, Not Per Mount
3136−
3137−**Impact: LOW-MEDIUM (avoids duplicate init in development)**
3138−
3139−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.
3140−
3141−**Incorrect: runs twice in dev, re-runs on remount**
3142−
3143−```tsx
3144−function Comp() {
3145− useEffect(() => {
3146− loadFromStorage()
3147− checkAuthToken()
3148− }, [])
3149−
3150− // ...
3151−}
3152−```
3153−
3154−**Correct: once per app load**
3155−
3156−```tsx
3157−let didInit = false
3158−
3159−function Comp() {
3160− useEffect(() => {
3161− if (didInit) return
3162− didInit = true
3163− loadFromStorage()
3164− checkAuthToken()
3165− }, [])
3166−
3167− // ...
3168−}
3169−```
3170−
3171−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)
3172−
3173−### 8.2 Store Event Handlers in Refs
3174−
3175−**Impact: LOW (stable subscriptions)**
3176−
3177−Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
3178−
3179−**Incorrect: re-subscribes on every render**
3180−
3181−```tsx
3182−function useWindowEvent(event: string, handler: (e) => void) {
3183− useEffect(() => {
3184− window.addEventListener(event, handler)
3185− return () => window.removeEventListener(event, handler)
3186− }, [event, handler])
3187−}
3188−```
3189−
3190−**Correct: stable subscription**
3191−
3192−```tsx
3193−import { useEffectEvent } from 'react'
3194−
3195−function useWindowEvent(event: string, handler: (e) => void) {
3196− const onEvent = useEffectEvent(handler)
3197−
3198− useEffect(() => {
3199− window.addEventListener(event, onEvent)
3200− return () => window.removeEventListener(event, onEvent)
3201− }, [event])
3202−}
3203−```
3204−
3205−**Alternative: use `useEffectEvent` if you're on latest React:**
3206−
3207−`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
3208−
3209−### 8.3 useEffectEvent for Stable Callback Refs
3210−
3211−**Impact: LOW (prevents effect re-runs)**
3212−
3213−Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
3214−
3215−**Incorrect: effect re-runs on every callback change**
3216−
3217−```tsx
3218−function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3219− const [query, setQuery] = useState('')
3220−
3221− useEffect(() => {
3222− const timeout = setTimeout(() => onSearch(query), 300)
3223− return () => clearTimeout(timeout)
3224− }, [query, onSearch])
3225−}
3226−```
3227−
3228−**Correct: using React's useEffectEvent**
3229−
3230−```tsx
3231−import { useEffectEvent } from 'react';
3232−
3233−function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3234− const [query, setQuery] = useState('')
3235− const onSearchEvent = useEffectEvent(onSearch)
3236−
3237− useEffect(() => {
3238− const timeout = setTimeout(() => onSearchEvent(query), 300)
3239− return () => clearTimeout(timeout)
3240− }, [query])
3241−}
3242−```
3243−
3244−---
3245−
3246−## References
3247−
3248−1. [https://react.dev](https://react.dev)
3249−2. [https://nextjs.org](https://nextjs.org)
3250−3. [https://swr.vercel.app](https://swr.vercel.app)
3251−4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
3252−5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
3253−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)
3254−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)
1+## Guiding Principles (MUST FOLLOW)
2+
3+### Mindset
4+
5+How to approach any coding task in this repo.
6+
7+#### Think Before Coding
8+
9+- State assumptions explicitly. If uncertain, ask before implementing.
10+- When multiple interpretations exist, surface them — do not pick silently.
11+- If a simpler approach exists, say so. Push back when warranted.
12+- If something is unclear, stop. Name what is confusing. Ask.
13+
14+#### Simplicity First
15+
16+- Write the minimum code that solves the problem. Nothing speculative.
17+- No features beyond what was asked.
18+- No abstractions for single-use code.
19+- No "flexibility" or "configurability" that was not requested.
20+- No error handling for impossible scenarios.
21+- If you wrote 200 lines and it could be 50, rewrite it.
22+- Inline comments cap at 2 lines. Needing more means the code is a patch — fix the implementation instead of narrating it. Say *why*, never restate *what*; no changelogs, no rationale essays, no pasted chat/review replies. (Doc comments on an exported API — TSDoc `@param`/`@returns`/`@deprecated` — are documentation, not narration, and are exempt.)
23+
24+#### Surgical Changes
25+
26+- Touch only what the task requires. Do not "improve" adjacent code, comments, or formatting.
27+- Do not refactor things that are not broken.
28+- Match existing style even if you would do it differently.
29+- If you notice unrelated dead code, mention it — do not delete it.
30+- Remove imports / variables / functions that **your** changes orphaned. Leave pre-existing dead code alone unless asked.
31+- Every changed line must trace directly to the user's request.
32+
33+#### Goal-Driven Execution
34+
35+- Convert tasks into verifiable goals before coding:
36+ - "Add validation" → "Write tests for invalid inputs, then make them pass."
37+ - "Fix the bug" → "Write a test that reproduces it, then make it pass."
38+ - "Refactor X" → "Ensure tests pass before and after."
39+- For multi-step tasks, state a brief plan with explicit verification per step:
40+
41+```
42+1. [Step] → verify: [check]
43+2. [Step] → verify: [check]
44+```
45+
46+### Operational Rules
47+
48+Project-specific tools, paths, and conventions.
49+
50+- **Keep it clear**: Write code that is easy to read, maintain, and explain.
51+- **Read local READMEs first**: Before editing code in a directory, check for a `README.md` in that directory (and its parents) and read it — these files capture local conventions, invariants, and entry points that aren't obvious from the code alone.
52+- **Fix upstream, don't hack downstream**: When a new feature hits an existing module's limitation, flag the upstream improvement for the user's decision before proposing a downstream workaround.
53+- **Library-first, custom-last**: Before writing custom code, check library/framework docs for built-in options or existing solutions. Write custom code only when no adequate alternative exists.
54+- **Build with Tailwind CSS & Shadcn UI**: Use components from `@cherrystudio/ui` (located in `packages/ui`, Shadcn UI + Tailwind CSS) for every new UI component.
55+- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
56+- **Access paths centrally**: Use `application.getPath('namespace.key', filename?)` for all main-process filesystem paths—never call `app.getPath()`, `os.homedir()`, or construct paths ad-hoc. Import the singleton via `import { application } from '@application'`.
57+- **Lint, test, and format before completion**: Coding tasks are only complete after running `pnpm lint`, `pnpm test`, and `pnpm format` successfully.
58+- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat(data-api):`, `fix(lifecycle):`, `refactor(quick-assistant):`, `docs(testing):`, `chore(deps):`, `test(window-manager):`). Scope must be a specific kebab-case module, never generic like `main` — when `git log` conflicts with this rule, this rule wins.
59+- **Sign commits and sign off**: Every commit must be both cryptographically signed and DCO-signed off. Use `git commit -S --signoff` (not `--signoff` alone), verify the commit object contains a `gpgsig` header with `git cat-file commit HEAD`, and verify the pushed PR commits show `Verified` on GitHub.
60+- **Target the right branch**: `main` is the default branch for all active development — submit features, refactors, optimizations, and fixes here.
61+
62+## Development
63+
64+### Commands
65+
66+Run `pnpm install` first (Node and pnpm versions are pinned in `package.json` — let it enforce them). For every other script, read `package.json` — the ones you must know:
67+
68+- `pnpm lint` — oxlint + eslint fix + typecheck + i18n check + format (writes files)
69+- `pnpm test` — run all Vitest tests
70+- `pnpm format` — Biome format + lint (write mode)
71+- `pnpm build:check` — **REQUIRED before commits**. If it fails on i18n sort, run `pnpm i18n:sync` first; on formatting, run `pnpm format` first; on broken doc links, fix the link.
72+- `pnpm test:lint` — the CI-equivalent lint gate: it denies oxlint warnings that `pnpm lint` / `pnpm build:check` silently tolerate; run it when CI must pass.
73+
74+### Testing
75+
76+- Tests run with Vitest 3 (see `vitest.config.*` for project setup).
77+- **No behavior-pinning tests**: a test whose only assertion records what the code currently does — a snapshot of whatever came out, `toHaveBeenCalled` on a mock, an expected value re-derived the way the implementation derives it — has zero value. It cannot fail for a real reason, it breaks on every refactor, and it certifies existing bugs as "expected". Assert the contract instead: real input → the outcome the feature promises, plus the failure and edge cases. Before writing a test, state the bug it would catch; if you cannot, do not write it. **The existing suite is full of these** — delete the ones in a file you are already editing rather than keeping them green; a repo-wide purge is its own task, not a side effect of an unrelated PR.
78+- **Frontend Tests — MUST READ**: [Frontend Testing Guidelines](docs/references/testing/frontend-testing.md).
79+- **Test Mocking**: Use the unified mock system — do NOT create ad-hoc mocks for `application`, services, or data layers. See [tests/__mocks__/README.md](tests/__mocks__/README.md) for available mocks, usage patterns, and best practices.
80+- **Database Tests**: For any service/handler/seeder that reads or writes SQLite, use `setupTestDatabase()` from `@test-helpers/db` — it provides a real file-backed DB with production migrations. Do NOT hand-write `CREATE TABLE` SQL, override `@application`, or stub Drizzle chains. See [docs/references/testing/database-testing.md](docs/references/testing/database-testing.md).
81+
82+### Patched Dependencies
83+
84+Before upgrading any dependency, check `patches/` for custom patches.
85+
86+## GitHub
87+
88+### Pull Requests
89+
90+Use the `gh-create-pr` skill. Fallback: read `.agents/skills/gh-create-pr/SKILL.md` directly.
91+
92+### Code Review
93+
94+When reviewing a GitHub PR, do NOT run `pnpm lint` / `pnpm test` / `pnpm format` locally — its CI already ran them; inspect via `gh` instead.
95+
96+### Issues
97+
98+Use the `gh-create-issue` skill. Fallback: read `.agents/skills/gh-create-issue/SKILL.md` directly.
99+
100+## Conventions
101+
102+### TypeScript
103+
104+- Cross-process types belong in `src/shared/`; renderer-only shared types in `src/renderer/types/` (see [Shared Layer Architecture](docs/references/shared-layer-architecture.md)).
105+
106+### Naming Conventions
107+
108+**MUST READ**: [docs/references/naming-conventions.md](docs/references/naming-conventions.md) — files, directories, identifiers, and singular/plural rules.
109+
110+### Logging
111+
112+```typescript
113+import { loggerService } from "@logger";
114+const logger = loggerService.withContext("moduleName");
115+// Renderer only: loggerService.initWindowSource('windowName') first
116+logger.info("message", CONTEXT);
117+logger.warn("message");
118+logger.error("message", error);
119+```
120+
121+### Paths
122+
123+**MUST READ**: [src/main/core/paths/README.md](src/main/core/paths/README.md) — namespaces, naming, adding new keys, testing patterns. (Rule stated in Guiding Principle "Access paths centrally".)
124+
125+### i18n
126+
127+- All user-visible strings must use `i18next` — never hardcode UI strings
128+- Run `pnpm i18n:check` to validate; `pnpm i18n:sync` to add missing keys
129+- Locale files in `src/renderer/i18n/`
130+
131+### UI Design
132+
133+For any UI component or page style work, read [DESIGN.md](./DESIGN.md) first and follow its colors, fonts, spacing, and component specs strictly.
134+
135+## Architecture
136+
137+### Code Organization
138+
139+Where each file and directory belongs — read the doc for the process you're touching before adding code or opening a directory. Each process root's top level is a **closed set**: route new code into an existing category, never a new top-level directory ([Naming Conventions §4.8](docs/references/naming-conventions.md)).
140+
141+A directory's `index.ts` is a **barrel** — an enforced encapsulation boundary re-exporting one cohesive public API (internals private, outsiders import through it): re-export only (no logic / `export *`), no nesting, and it exists only if lint can seal off deep imports — else no barrel. `index.tsx` is always banned ([Naming Conventions §6.4](docs/references/naming-conventions.md)).
142+
143+- [Main Process Architecture](docs/references/main-process-architecture.md) — `src/main/` directories (`core`/`ipc`/`data`/`ai`/`features`/`services`/`utils`/`i18n`) and dependency direction.
144+- [Renderer Architecture](docs/references/renderer-architecture.md) — `src/renderer/` two-axis (type × domain) layout and downward-only layering.
145+- [Shared Layer Architecture](docs/references/shared-layer-architecture.md) — what belongs in `@shared` (cross-process + no mutable runtime state) and its closed top-level set.
146+
147+### Data
148+
149+**MUST READ**: [docs/references/data/README.md](docs/references/data/README.md) for system selection, architecture, and patterns.
150+
151+| System | Use Case | APIs |
152+| ---------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
153+| [BootConfig](docs/references/data/boot-config-overview.md) | Early boot settings (pre-lifecycle) | `bootConfigService.get()`, `usePreference('BootConfig.*')` |
154+| [Cache](docs/references/data/cache-overview.md) | Temp data (can lose) | `useCache`, `useSharedCache`, `useSharedCacheValue`, `usePersistCache` |
155+| [Preference](docs/references/data/preference-overview.md) | User settings | `usePreference` |
156+| [DataApi](docs/references/data/data-api-overview.md) | Business data (**critical**) | `useQuery`, `useMutation` |
157+
158+Scope:
159+
160+- **BootConfig**: sync file-based; direct in main (pre-lifecycle), via `usePreference('BootConfig.*')` otherwise
161+- **Cache**: memory / shared (cross-window) / persist tiers; memory + shared on both main and renderer; persist on both too but as **independent** stores (renderer = localStorage, main = JSON file at `{userData}/cache.json`), never shared — main additionally relays renderer persist sync between windows
162+- **Preference**: cross-process (main + renderer); auto-syncs across windows
163+- **DataApi**: SQLite-backed; no auto-sync, fetch on demand from renderer
164+
165+Database: SQLite via **better-sqlite3** + Drizzle ORM — the driver is **synchronous** (queries and transactions run inline with no `await`, unlike the app's otherwise-async data layers), so `getDb()` queries and `withWriteTx(fn)` callbacks must be written synchronously. Schemas in `src/main/data/db/schemas/`, migrations via `pnpm db:migrations:generate`
166+
167+**Write atomicity**: use `application.get('DbService').withWriteTx(fn)` to commit multiple writes (or a read-then-write) all-or-nothing in one synchronous `BEGIN IMMEDIATE` transaction; `fn` must be synchronous. A single write doesn't need it — better-sqlite3 runs each statement atomically on its one connection. See [Database Patterns — Write Serialization](docs/references/data/database-patterns.md#write-serialization-dbservicewritewritetx).
168+
169+**DataApi boundary rule**: DataApi is for SQLite-backed business data only. No database table → no DataApi endpoint; use IPC instead. See [Scope & Boundaries](docs/references/data/api-design-guidelines.md#dataapi-scope--boundaries).
170+
171+### IPC (IpcApi)
172+
173+**MUST READ**: [docs/references/ipc/README.md](docs/references/ipc/README.md) — paradigm boundary (RPC vs REST), schema/router/preload/facade layering, `IpcContext`, error model, security.
174+
175+Non-data command IPC (window/system/shell/notification/external/file) goes through **IpcApi** — the fifth subsystem alongside BootConfig/Cache/Preference/DataApi, RPC-over-IPC with single-point schemas (`schema + handler` to add a route; `ipcApi.request('namespace.action', input)` to call; `IpcApiService.broadcast`/`send` + `useIpcOn` for events). Legacy command IPC still coexists, so you'll encounter both. Decision: SQLite data → DataApi; user setting → Preference; losable/shared → Cache; everything else imperative → IpcApi.
176+
177+### Window Manager
178+
179+**MUST READ**: [docs/references/window-manager/README.md](docs/references/window-manager/README.md) — lifecycle modes, pool mechanics, API reference.
180+
181+All `BrowserWindow` goes through `WindowManager` with one of three modes (`default` / `singleton` / `pooled`), declared per type in `src/main/core/window/windowRegistry.ts`.
182+
183+- **Consumer API**: use only `open()` / `close()` — never `create()` / `destroy()` in business code.
184+- **Attach listeners in `onWindowCreated`**, not after `open()` — reused windows skip the latter.
185+- **Renderer reads init data via `useWindowInitData`**.
186+
187+### Main Process Services (Lifecycle)
188+
189+**MUST READ**: [docs/references/lifecycle/README.md](docs/references/lifecycle/README.md) — architecture, decision guides, usage patterns, and migration steps.
190+
191+All main-process services that own long-lived resources or register persistent side effects **must** use the lifecycle system:
192+
193+- **Extend `BaseService`**, apply `@Injectable`, `@ServicePhase`, `@DependsOn` decorators
194+- **Register in `serviceRegistry.ts`** (`src/main/core/application/serviceRegistry.ts`) — one line per service
195+- **Use `@DependsOn` for same-phase dependencies only** — do NOT declare dependencies on BeforeReady services (`PreferenceService`, `DbService`, `CacheService`, `DataApiService`) from WhenReady services; phase ordering is auto-enforced by the container
196+- **Access via `application.get('Name')`** (or `getOptional()` for `@Conditional` services)
197+- **Use `this.ipcHandle()` / `this.ipcOn()`** for IPC — auto-cleaned on stop/destroy, returns `Disposable`
198+- **Use `this.registerInterval()`** for recurring timers — auto-unref'd, exception-isolated, auto-cleaned on stop/destroy, returns `Disposable`
199+- **Use `this.registerDisposable()`** for cleanup tracking — accepts `Disposable` objects or `() => void` cleanup functions
200+- **Use `Emitter<T>` / `Event<T>`** for inter-service events, **`Signal<T>`** for one-shot completion
201+- **Implement `Activatable`** for services with heavy on-demand resources (IPC stays registered, resources load/release via `onActivate()`/`onDeactivate()`)
202+- **Do NOT** use `new` or manual singleton patterns — the container manages instantiation, ordering, and shutdown
203+
204+For detailed code examples, see [Usage Guide](docs/references/lifecycle/lifecycle-usage.md). For migrating legacy services, see [Migration Guide](docs/references/lifecycle/lifecycle-migration-guide.md).
205+
206+### Non-Lifecycle Services (Direct-Import Singleton)
207+
208+Services without long-lived resources or persistent side effects: use **named export singleton** (`export const x = new X()`). No `getInstance()` patterns. See [Decision Guide](docs/references/lifecycle/lifecycle-decision-guide.md) for criteria.
209+
210+## Schema & Migration Rules
211+
212+The v2 refactor has landed. v1 data reaches v2 only through the migrators in `src/main/data/migration/v2/` — never add fallbacks, dual-writes, or guards for v1 save / read / loss.
213+
214+**The migration chain is no longer throwaway.** It was consolidated into a single clean initial migration and shipped with `v2.0.0-rc.1`, so `migrations/sqlite-drizzle/` now runs against databases holding real user rows. Never wipe or rewrite an already-shipped migration, and never tell a user to delete their database: schema changes go in as new appended migrations generated by `pnpm db:migrations:generate`. `src/main/data/db/schemas/` still changes freely — but every change must survive a migrate-forward on a populated database.
215+
216+**Resolving migration merge conflicts: regenerate, never rename.** When an upstream migration conflicts with your local one, delete your local `.sql` + its `meta/*_snapshot.json` and re-run `pnpm db:migrations:generate`. Renaming/renumbering instead silently reuses the snapshot's random `id`, forking the chain for everyone — and `drizzle-kit generate` still exits `0`; only `pnpm db:migrations:check` catches it. CI enforces both the chain check and a schema↔migration generate-and-diff step.
217+
218+### Data Classification Toolchain
219+
220+`v2-refactor-temp/tools/data-classify/` is the code generation pipeline for the v2 data layer; `classification.json` is the single source of truth (see its README). Four files are **auto-generated — NEVER edit them by hand**: `src/shared/data/preference/preferenceSchemas.ts`, `src/shared/data/bootConfig/bootConfigSchemas.ts`, and `PreferencesMappings.ts` + `BootConfigMappings.ts` in `src/main/data/migration/v2/migrators/mappings/`. To change them, edit `classification.json` or `target-key-definitions.json` (both in `data/`), then run `cd v2-refactor-temp/tools/data-classify && npm run generate`.
221+
222+### Breaking Changes Log
223+
224+When a v2 change is user-perceivable and affects how users use the app, add an entry under `v2-refactor-temp/docs/breaking-changes/`. See [v2-refactor-temp/docs/breaking-changes/README.md](v2-refactor-temp/docs/breaking-changes/README.md) for conventions.
225+
226+## Local Instructions
227+
228+If `CLAUDE.local.md` exists in the repository root (gitignored, may be absent), read it in full before acting on anything in this file — it holds the developer's private instructions and **OVERRIDES this file wherever they conflict**. Tools that auto-load it (e.g. Claude Code) need not re-read it.
3255229
