AGENTS.md
.claude/skills/web-frontend/references/react-best-practices/AGENTS.mdAGENTS.md
Quality
65/100
Scores the file, not the repository.Length
13,099 words
82 headings · 174 code blocksRepository
0
— · pushed 1 days agoLast changed
3 days ago
First indexed 3 days ago.1# React Best Practices23**Version 1.0.0**4Vercel Engineering5January 202667> **Note:**8> This document is mainly for agents and LLMs to follow when maintaining,9> generating, or refactoring React and Next.js codebases. Humans10> may also find it useful, but guidance here is optimized for automation11> and consistency by AI-assisted workflows.1213---1415## Abstract1617Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.1819---2021## Table of Contents22231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**24 - 1.1 [Check Cheap Conditions Before Async Flags](#11-check-cheap-conditions-before-async-flags)25 - 1.2 [Defer Await Until Needed](#12-defer-await-until-needed)26 - 1.3 [Dependency-Based Parallelization](#13-dependency-based-parallelization)27 - 1.4 [Prevent Waterfall Chains in API Routes](#14-prevent-waterfall-chains-in-api-routes)28 - 1.5 [Promise.all() for Independent Operations](#15-promiseall-for-independent-operations)29 - 1.6 [Strategic Suspense Boundaries](#16-strategic-suspense-boundaries)302. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**31 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)32 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)33 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)34 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)35 - 2.5 [Prefer Statically Analyzable Paths](#25-prefer-statically-analyzable-paths)36 - 2.6 [Preload Based on User Intent](#26-preload-based-on-user-intent)373. [Server-Side Performance](#3-server-side-performance) — **HIGH**38 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)39 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)40 - 3.3 [Avoid Shared Module State for Request Data](#33-avoid-shared-module-state-for-request-data)41 - 3.4 [Cross-Request LRU Caching](#34-cross-request-lru-caching)42 - 3.5 [Hoist Static I/O to Module Level](#35-hoist-static-io-to-module-level)43 - 3.6 [Minimize Serialization at RSC Boundaries](#36-minimize-serialization-at-rsc-boundaries)44 - 3.7 [Parallel Data Fetching with Component Composition](#37-parallel-data-fetching-with-component-composition)45 - 3.8 [Parallel Nested Data Fetching](#38-parallel-nested-data-fetching)46 - 3.9 [Per-Request Deduplication with React.cache()](#39-per-request-deduplication-with-reactcache)47 - 3.10 [Use after() for Non-Blocking Operations](#310-use-after-for-non-blocking-operations)484. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**49 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)50 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)51 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)52 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)535. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**54 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)55 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)56 - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)57 - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)58 - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)59 - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)60 - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)61 - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)62 - 5.9 [Split Combined Hook Computations](#59-split-combined-hook-computations)63 - 5.10 [Subscribe to Derived State](#510-subscribe-to-derived-state)64 - 5.11 [Use Functional setState Updates](#511-use-functional-setstate-updates)65 - 5.12 [Use Lazy State Initialization](#512-use-lazy-state-initialization)66 - 5.13 [Use Transitions for Non-Urgent Updates](#513-use-transitions-for-non-urgent-updates)67 - 5.14 [Use useDeferredValue for Expensive Derived Renders](#514-use-usedeferredvalue-for-expensive-derived-renders)68 - 5.15 [Use useRef for Transient Values](#515-use-useref-for-transient-values)696. [Rendering Performance](#6-rendering-performance) — **MEDIUM**70 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)71 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)72 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)73 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)74 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)75 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)76 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)77 - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)78 - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)79 - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)80 - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)817. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**82 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)83 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)84 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)85 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)86 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)87 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)88 - 7.7 [Defer Non-Critical Work with requestIdleCallback](#77-defer-non-critical-work-with-requestidlecallback)89 - 7.8 [Early Length Check for Array Comparisons](#78-early-length-check-for-array-comparisons)90 - 7.9 [Early Return from Functions](#79-early-return-from-functions)91 - 7.10 [Hoist RegExp Creation](#710-hoist-regexp-creation)92 - 7.11 [Use flatMap to Map and Filter in One Pass](#711-use-flatmap-to-map-and-filter-in-one-pass)93 - 7.12 [Use Loop for Min/Max Instead of Sort](#712-use-loop-for-minmax-instead-of-sort)94 - 7.13 [Use Set/Map for O(1) Lookups](#713-use-setmap-for-o1-lookups)95 - 7.14 [Use toSorted() Instead of sort() for Immutability](#714-use-tosorted-instead-of-sort-for-immutability)968. [Advanced Patterns](#8-advanced-patterns) — **LOW**97 - 8.1 [Do Not Put Effect Events in Dependency Arrays](#81-do-not-put-effect-events-in-dependency-arrays)98 - 8.2 [Initialize App Once, Not Per Mount](#82-initialize-app-once-not-per-mount)99 - 8.3 [Store Event Handlers in Refs](#83-store-event-handlers-in-refs)100 - 8.4 [useEffectEvent for Stable Callback Refs](#84-useeffectevent-for-stable-callback-refs)101102---103104## 1. Eliminating Waterfalls105106**Impact: CRITICAL**107108Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.109110### 1.1 Check Cheap Conditions Before Async Flags111112**Impact: HIGH (avoids unnecessary async work when a synchronous guard already fails)**113114When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.115116This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.117118**Incorrect:**119120```typescript121const someFlag = await getFlag();122123if (someFlag && someCondition) {124 // ...125}126```127128**Correct:**129130```typescript131if (someCondition) {132 const someFlag = await getFlag();133 if (someFlag) {134 // ...135 }136}137```138139This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.140141Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.142143### 1.2 Defer Await Until Needed144145**Impact: HIGH (avoids blocking unused code paths)**146147Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.148149**Incorrect: blocks both branches**150151```typescript152async function handleRequest(userId: string, skipProcessing: boolean) {153 const userData = await fetchUserData(userId);154155 if (skipProcessing) {156 // Returns immediately but still waited for userData157 return { skipped: true };158 }159160 // Only this branch uses userData161 return processUserData(userData);162}163```164165**Correct: only blocks when needed**166167```typescript168async function handleRequest(userId: string, skipProcessing: boolean) {169 if (skipProcessing) {170 // Returns immediately without waiting171 return { skipped: true };172 }173174 // Fetch only when needed175 const userData = await fetchUserData(userId);176 return processUserData(userData);177}178```179180**Another example: early return optimization**181182```typescript183// Incorrect: always fetches permissions184async function updateResource(resourceId: string, userId: string) {185 const permissions = await fetchPermissions(userId);186 const resource = await getResource(resourceId);187188 if (!resource) {189 return { error: "Not found" };190 }191192 if (!permissions.canEdit) {193 return { error: "Forbidden" };194 }195196 return await updateResourceData(resource, permissions);197}198199// Correct: fetches only when needed200async function updateResource(resourceId: string, userId: string) {201 const resource = await getResource(resourceId);202203 if (!resource) {204 return { error: "Not found" };205 }206207 const permissions = await fetchPermissions(userId);208209 if (!permissions.canEdit) {210 return { error: "Forbidden" };211 }212213 return await updateResourceData(resource, permissions);214}215```216217This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.218219For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).220221### 1.3 Dependency-Based Parallelization222223**Impact: CRITICAL (2-10× improvement)**224225For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.226227**Incorrect: profile waits for config unnecessarily**228229```typescript230const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);231const profile = await fetchProfile(user.id);232```233234**Correct: config and profile run in parallel**235236```typescript237import { all } from "better-all";238239const { user, config, profile } = await all({240 async user() {241 return fetchUser();242 },243 async config() {244 return fetchConfig();245 },246 async profile() {247 return fetchProfile((await this.$.user).id);248 },249});250```251252**Alternative without extra dependencies:**253254```typescript255const userPromise = fetchUser();256const profilePromise = userPromise.then((user) => fetchProfile(user.id));257258const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]);259```260261We can also create all the promises first, and do `Promise.all()` at the end.262263Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)264265### 1.4 Prevent Waterfall Chains in API Routes266267**Impact: CRITICAL (2-10× improvement)**268269In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.270271**Incorrect: config waits for auth, data waits for both**272273```typescript274export async function GET(request: Request) {275 const session = await auth();276 const config = await fetchConfig();277 const data = await fetchData(session.user.id);278 return Response.json({ data, config });279}280```281282**Correct: auth and config start immediately**283284```typescript285export async function GET(request: Request) {286 const sessionPromise = auth();287 const configPromise = fetchConfig();288 const session = await sessionPromise;289 const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);290 return Response.json({ data, config });291}292```293294For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).295296### 1.5 Promise.all() for Independent Operations297298**Impact: CRITICAL (2-10× improvement)**299300When async operations have no interdependencies, execute them concurrently using `Promise.all()`.301302**Incorrect: sequential execution, 3 round trips**303304```typescript305const user = await fetchUser();306const posts = await fetchPosts();307const comments = await fetchComments();308```309310**Correct: parallel execution, 1 round trip**311312```typescript313const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);314```315316### 1.6 Strategic Suspense Boundaries317318**Impact: HIGH (faster initial paint)**319320Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.321322**Incorrect: wrapper blocked by data fetching**323324```tsx325async function Page() {326 const data = await fetchData(); // Blocks entire page327328 return (329 <div>330 <div>Sidebar</div>331 <div>Header</div>332 <div>333 <DataDisplay data={data} />334 </div>335 <div>Footer</div>336 </div>337 );338}339```340341The entire layout waits for data even though only the middle section needs it.342343**Correct: wrapper shows immediately, data streams in**344345```tsx346function Page() {347 return (348 <div>349 <div>Sidebar</div>350 <div>Header</div>351 <div>352 <Suspense fallback={<Skeleton />}>353 <DataDisplay />354 </Suspense>355 </div>356 <div>Footer</div>357 </div>358 );359}360361async function DataDisplay() {362 const data = await fetchData(); // Only blocks this component363 return <div>{data.content}</div>;364}365```366367Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.368369**Alternative: share promise across components**370371```tsx372function Page() {373 // Start fetch immediately, but don't await374 const dataPromise = fetchData();375376 return (377 <div>378 <div>Sidebar</div>379 <div>Header</div>380 <Suspense fallback={<Skeleton />}>381 <DataDisplay dataPromise={dataPromise} />382 <DataSummary dataPromise={dataPromise} />383 </Suspense>384 <div>Footer</div>385 </div>386 );387}388389function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {390 const data = use(dataPromise); // Unwraps the promise391 return <div>{data.content}</div>;392}393394function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {395 const data = use(dataPromise); // Reuses the same promise396 return <div>{data.summary}</div>;397}398```399400Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.401402**When NOT to use this pattern:**403404- Critical data needed for layout decisions (affects positioning)405406- SEO-critical content above the fold407408- Small, fast queries where suspense overhead isn't worth it409410- When you want to avoid layout shift (loading → content jump)411412**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.413414---415416## 2. Bundle Size Optimization417418**Impact: CRITICAL**419420Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.421422### 2.1 Avoid Barrel File Imports423424**Impact: CRITICAL (200-800ms import cost, slow builds)**425426Import 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'`).427428Popular 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.429430**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.431432**Incorrect: imports entire library**433434```tsx435import { Check, X, Menu } from "lucide-react";436// Loads 1,583 modules, takes ~2.8s extra in dev437// Runtime cost: 200-800ms on every cold start438439import { Button, TextField } from "@mui/material";440// Loads 2,225 modules, takes ~4.2s extra in dev441```442443**Correct - Next.js 13.5+ (recommended):**444445```tsx446// Keep the standard imports - Next.js transforms them to direct imports447import { Check, X, Menu } from "lucide-react";448// Full TypeScript support, no manual path wrangling449```450451This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.452453**Correct - Direct imports (non-Next.js projects):**454455```tsx456import Button from "@mui/material/Button";457import TextField from "@mui/material/TextField";458// Loads only what you use459```460461> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.462463These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.464465Libraries 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`.466467Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)468469### 2.2 Conditional Module Loading470471**Impact: HIGH (loads large data only when needed)**472473Load large data or modules only when a feature is activated.474475**Example: lazy-load animation frames**476477```tsx478function AnimationPlayer({479 enabled,480 setEnabled,481}: {482 enabled: boolean;483 setEnabled: React.Dispatch<React.SetStateAction<boolean>>;484}) {485 const [frames, setFrames] = useState<Frame[] | null>(null);486487 useEffect(() => {488 if (enabled && !frames && typeof window !== "undefined") {489 import("./animation-frames.js")490 .then((mod) => setFrames(mod.frames))491 .catch(() => setEnabled(false));492 }493 }, [enabled, frames, setEnabled]);494495 if (!frames) return <Skeleton />;496 return <Canvas frames={frames} />;497}498```499500The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.501502### 2.3 Defer Non-Critical Third-Party Libraries503504**Impact: MEDIUM (loads after hydration)**505506Analytics, logging, and error tracking don't block user interaction. Load them after hydration.507508**Incorrect: blocks initial bundle**509510```tsx511import { Analytics } from "@vercel/analytics/react";512513export default function RootLayout({ children }) {514 return (515 <html>516 <body>517 {children}518 <Analytics />519 </body>520 </html>521 );522}523```524525**Correct: loads after hydration**526527```tsx528import dynamic from "next/dynamic";529530const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {531 ssr: false,532});533534export default function RootLayout({ children }) {535 return (536 <html>537 <body>538 {children}539 <Analytics />540 </body>541 </html>542 );543}544```545546### 2.4 Dynamic Imports for Heavy Components547548**Impact: CRITICAL (directly affects TTI and LCP)**549550Use `next/dynamic` to lazy-load large components not needed on initial render.551552**Incorrect: Monaco bundles with main chunk ~300KB**553554```tsx555import { MonacoEditor } from "./monaco-editor";556557function CodePanel({ code }: { code: string }) {558 return <MonacoEditor value={code} />;559}560```561562**Correct: Monaco loads on demand**563564```tsx565import dynamic from "next/dynamic";566567const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {568 ssr: false,569});570571function CodePanel({ code }: { code: string }) {572 return <MonacoEditor value={code} />;573}574```575576### 2.5 Prefer Statically Analyzable Paths577578**Impact: HIGH (avoids accidental broad bundles and file traces)**579580Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.581582Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with `import()` or reading files in server/build code.583584When analysis becomes too broad, the cost is real:585586- Larger server bundles587588- Slower builds589590- Worse cold starts591592- More memory use593594**Incorrect: the bundler cannot tell what may be imported**595596```ts597const PAGE_MODULES = {598 home: "./pages/home",599 settings: "./pages/settings",600} as const;601602const Page = await import(PAGE_MODULES[pageName]);603```604605**Correct: use an explicit map of allowed modules**606607```ts608const PAGE_MODULES = {609 home: () => import("./pages/home"),610 settings: () => import("./pages/settings"),611} as const;612613const Page = await PAGE_MODULES[pageName]();614```615616**Incorrect: a 2-value enum still hides the final path from static analysis**617618```ts619const baseDir = path.join(process.cwd(), "content/" + contentKind);620```621622**Correct: make each final path literal at the callsite**623624```ts625const baseDir =626 kind === ContentKind.Blog627 ? path.join(process.cwd(), "content/blog")628 : path.join(process.cwd(), "content/docs");629```630631In Next.js server code, this matters for output file tracing too. `path.join(process.cwd(), someVar)` can widen the traced file set because Next.js statically analyze `import`, `require`, and `fs` usage.632633Reference: [https://nextjs.org/docs/app/api-reference/config/next-config-js/output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output), [https://nextjs.org/learn/seo/dynamic-imports](https://nextjs.org/learn/seo/dynamic-imports), [https://vite.dev/guide/features.html](https://vite.dev/guide/features.html), [https://esbuild.github.io/api/](https://esbuild.github.io/api/), [https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars](https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars), [https://webpack.js.org/guides/dependency-management/](https://webpack.js.org/guides/dependency-management/)634635### 2.6 Preload Based on User Intent636637**Impact: MEDIUM (reduces perceived latency)**638639Preload heavy bundles before they're needed to reduce perceived latency.640641**Example: preload on hover/focus**642643```tsx644function EditorButton({ onClick }: { onClick: () => void }) {645 const preload = () => {646 if (typeof window !== "undefined") {647 void import("./monaco-editor");648 }649 };650651 return (652 <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>653 Open Editor654 </button>655 );656}657```658659**Example: preload when feature flag is enabled**660661```tsx662function FlagsProvider({ children, flags }: Props) {663 useEffect(() => {664 if (flags.editorEnabled && typeof window !== "undefined") {665 void import("./monaco-editor").then((mod) => mod.init());666 }667 }, [flags.editorEnabled]);668669 return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;670}671```672673The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.674675---676677## 3. Server-Side Performance678679**Impact: HIGH**680681Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.682683### 3.1 Authenticate Server Actions Like API Routes684685**Impact: CRITICAL (prevents unauthorized access to server mutations)**686687Server 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.688689Next.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."690691**Incorrect: no authentication check**692693```typescript694"use server";695696export async function deleteUser(userId: string) {697 // Anyone can call this! No auth check698 await db.user.delete({ where: { id: userId } });699 return { success: true };700}701```702703**Correct: authentication inside the action**704705```typescript706"use server";707708import { verifySession } from "@/lib/auth";709import { unauthorized } from "@/lib/errors";710711export async function deleteUser(userId: string) {712 // Always check auth inside the action713 const session = await verifySession();714715 if (!session) {716 throw unauthorized("Must be logged in");717 }718719 // Check authorization too720 if (session.user.role !== "admin" && session.user.id !== userId) {721 throw unauthorized("Cannot delete other users");722 }723724 await db.user.delete({ where: { id: userId } });725 return { success: true };726}727```728729**With input validation:**730731```typescript732"use server";733734import { verifySession } from "@/lib/auth";735import { z } from "zod";736737const updateProfileSchema = z.object({738 userId: z.string().uuid(),739 name: z.string().min(1).max(100),740 email: z.string().email(),741});742743export async function updateProfile(data: unknown) {744 // Validate input first745 const validated = updateProfileSchema.parse(data);746747 // Then authenticate748 const session = await verifySession();749 if (!session) {750 throw new Error("Unauthorized");751 }752753 // Then authorize754 if (session.user.id !== validated.userId) {755 throw new Error("Can only update own profile");756 }757758 // Finally perform the mutation759 await db.user.update({760 where: { id: validated.userId },761 data: {762 name: validated.name,763 email: validated.email,764 },765 });766767 return { success: true };768}769```770771Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)772773### 3.2 Avoid Duplicate Serialization in RSC Props774775**Impact: LOW (reduces network payload by avoiding duplicate serialization)**776777RSC→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.778779**Incorrect: duplicates array**780781```tsx782// RSC: sends 6 strings (2 arrays × 3 items)783<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />784```785786**Correct: sends 3 strings**787788```tsx789// RSC: send once790<ClientList usernames={usernames} />;791792// Client: transform there793("use client");794const sorted = useMemo(() => [...usernames].sort(), [usernames]);795```796797**Nested deduplication behavior:**798799```tsx800// string[] - duplicates everything801usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings802803// object[] - duplicates array structure only804users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)805```806807Deduplication works recursively. Impact varies by data type:808809- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated810811- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference812813**Operations breaking deduplication: create new references**814815- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`816817- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`818819**More examples:**820821```tsx822// ❌ Bad823<C users={users} active={users.filter(u => u.active)} />824<C product={product} productName={product.name} />825826// ✅ Good827<C users={users} />828<C product={product} />829// Do filtering/destructuring in client830```831832**Exception:** Pass derived data when transformation is expensive or client doesn't need original.833834### 3.3 Avoid Shared Module State for Request Data835836**Impact: HIGH (prevents concurrency bugs and request data leaks)**837838For React Server Components and client components rendered during SSR, avoid using mutable module-level variables to share request-scoped data. Server renders can run concurrently in the same process. If one render writes to shared module state and another render reads it, you can get race conditions, cross-request contamination, and security bugs where one user's data appears in another user's response.839840Treat module scope on the server as process-wide shared memory, not request-local state.841842**Incorrect: request data leaks across concurrent renders**843844```tsx845let currentUser: User | null = null;846847export default async function Page() {848 currentUser = await auth();849 return <Dashboard />;850}851852async function Dashboard() {853 return <div>{currentUser?.name}</div>;854}855```856857If two requests overlap, request A can set `currentUser`, then request B overwrites it before request A finishes rendering `Dashboard`.858859**Correct: keep request data local to the render tree**860861```tsx862export default async function Page() {863 const user = await auth();864 return <Dashboard user={user} />;865}866867function Dashboard({ user }: { user: User | null }) {868 return <div>{user?.name}</div>;869}870```871872Safe exceptions:873874- Immutable static assets or config loaded once at module scope875876- Shared caches intentionally designed for cross-request reuse and keyed correctly877878- Process-wide singletons that do not store request- or user-specific mutable data879880For static assets and config, see [Hoist Static I/O to Module Level](./server-hoist-static-io.md).881882### 3.4 Cross-Request LRU Caching883884**Impact: HIGH (caches across requests)**885886`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.887888**Implementation:**889890```typescript891import { LRUCache } from "lru-cache";892893const cache = new LRUCache<string, any>({894 max: 1000,895 ttl: 5 * 60 * 1000, // 5 minutes896});897898export async function getUser(id: string) {899 const cached = cache.get(id);900 if (cached) return cached;901902 const user = await db.user.findUnique({ where: { id } });903 cache.set(id, user);904 return user;905}906907// Request 1: DB query, result cached908// Request 2: cache hit, no DB query909```910911Use when sequential user actions hit multiple endpoints needing the same data within seconds.912913**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.914915**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.916917Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)918919### 3.5 Hoist Static I/O to Module Level920921**Impact: HIGH (avoids repeated file/network I/O per request)**922923When 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.924925**Incorrect: reads font file on every request**926927```typescript928// app/api/og/route.tsx929import { ImageResponse } from 'next/og'930931export async function GET(request: Request) {932 // Runs on EVERY request - expensive!933 const fontData = await fetch(934 new URL('./fonts/Inter.ttf', import.meta.url)935 ).then(res => res.arrayBuffer())936937 const logoData = await fetch(938 new URL('./images/logo.png', import.meta.url)939 ).then(res => res.arrayBuffer())940941 return new ImageResponse(942 <div style={{ fontFamily: 'Inter' }}>943 <img src={logoData} />944 Hello World945 </div>,946 { fonts: [{ name: 'Inter', data: fontData }] }947 )948}949```950951**Correct: loads once at module initialization**952953```typescript954// app/api/og/route.tsx955import { ImageResponse } from 'next/og'956957// Module-level: runs ONCE when module is first imported958const fontData = fetch(959 new URL('./fonts/Inter.ttf', import.meta.url)960).then(res => res.arrayBuffer())961962const logoData = fetch(963 new URL('./images/logo.png', import.meta.url)964).then(res => res.arrayBuffer())965966export async function GET(request: Request) {967 // Await the already-started promises968 const [font, logo] = await Promise.all([fontData, logoData])969970 return new ImageResponse(971 <div style={{ fontFamily: 'Inter' }}>972 <img src={logo} />973 Hello World974 </div>,975 { fonts: [{ name: 'Inter', data: font }] }976 )977}978```979980**Correct: synchronous fs at module level**981982```typescript983// app/api/og/route.tsx984import { ImageResponse } from 'next/og'985import { readFileSync } from 'fs'986import { join } from 'path'987988// Synchronous read at module level - blocks only during module init989const fontData = readFileSync(990 join(process.cwd(), 'public/fonts/Inter.ttf')991)992993const logoData = readFileSync(994 join(process.cwd(), 'public/images/logo.png')995)996997export async function GET(request: Request) {998 return new ImageResponse(999 <div style={{ fontFamily: 'Inter' }}>1000 <img src={logoData} />1001 Hello World1002 </div>,1003 { fonts: [{ name: 'Inter', data: fontData }] }1004 )1005}1006```10071008**Incorrect: reads config on every call**10091010```typescript1011import fs from "node:fs/promises";10121013export async function processRequest(data: Data) {1014 const config = JSON.parse(await fs.readFile("./config.json", "utf-8"));1015 const template = await fs.readFile("./template.html", "utf-8");10161017 return render(template, data, config);1018}1019```10201021**Correct: hoists config and template to module level**10221023```typescript1024import fs from "node:fs/promises";10251026const configPromise = fs.readFile("./config.json", "utf-8").then(JSON.parse);1027const templatePromise = fs.readFile("./template.html", "utf-8");10281029export async function processRequest(data: Data) {1030 const [config, template] = await Promise.all([configPromise, templatePromise]);10311032 return render(template, data, config);1033}1034```10351036When to use this pattern:10371038- Loading fonts for OG image generation10391040- Loading static logos, icons, or watermarks10411042- Reading configuration files that don't change at runtime10431044- Loading email templates or other static templates10451046- Any static asset that's the same across all requests10471048When not to use this pattern:10491050- Assets that vary per request or user10511052- Files that may change during runtime (use caching with TTL instead)10531054- Large files that would consume too much memory if kept loaded10551056- Sensitive data that shouldn't persist in memory10571058With 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.10591060In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.10611062### 3.6 Minimize Serialization at RSC Boundaries10631064**Impact: HIGH (reduces data transfer size)**10651066The 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.10671068**Incorrect: serializes all 50 fields**10691070```tsx1071async function Page() {1072 const user = await fetchUser(); // 50 fields1073 return <Profile user={user} />;1074}10751076("use client");1077function Profile({ user }: { user: User }) {1078 return <div>{user.name}</div>; // uses 1 field1079}1080```10811082**Correct: serializes only 1 field**10831084```tsx1085async function Page() {1086 const user = await fetchUser();1087 return <Profile name={user.name} />;1088}10891090("use client");1091function Profile({ name }: { name: string }) {1092 return <div>{name}</div>;1093}1094```10951096### 3.7 Parallel Data Fetching with Component Composition10971098**Impact: CRITICAL (eliminates server-side waterfalls)**10991100React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.11011102**Incorrect: Sidebar waits for Page's fetch to complete**11031104```tsx1105export default async function Page() {1106 const header = await fetchHeader();1107 return (1108 <div>1109 <div>{header}</div>1110 <Sidebar />1111 </div>1112 );1113}11141115async function Sidebar() {1116 const items = await fetchSidebarItems();1117 return <nav>{items.map(renderItem)}</nav>;1118}1119```11201121**Correct: both fetch simultaneously**11221123```tsx1124async function Header() {1125 const data = await fetchHeader();1126 return <div>{data}</div>;1127}11281129async function Sidebar() {1130 const items = await fetchSidebarItems();1131 return <nav>{items.map(renderItem)}</nav>;1132}11331134export default function Page() {1135 return (1136 <div>1137 <Header />1138 <Sidebar />1139 </div>1140 );1141}1142```11431144**Alternative with children prop:**11451146```tsx1147async function Header() {1148 const data = await fetchHeader();1149 return <div>{data}</div>;1150}11511152async function Sidebar() {1153 const items = await fetchSidebarItems();1154 return <nav>{items.map(renderItem)}</nav>;1155}11561157function Layout({ children }: { children: ReactNode }) {1158 return (1159 <div>1160 <Header />1161 {children}1162 </div>1163 );1164}11651166export default function Page() {1167 return (1168 <Layout>1169 <Sidebar />1170 </Layout>1171 );1172}1173```11741175### 3.8 Parallel Nested Data Fetching11761177**Impact: CRITICAL (eliminates server-side waterfalls)**11781179When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.11801181**Incorrect: a single slow item blocks all nested fetches**11821183```tsx1184const chats = await Promise.all(chatIds.map((id) => getChat(id)));11851186const chatAuthors = await Promise.all(chats.map((chat) => getUser(chat.author)));1187```11881189If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.11901191**Correct: each item chains its own nested fetch**11921193```tsx1194const chatAuthors = await Promise.all(1195 chatIds.map((id) => getChat(id).then((chat) => getUser(chat.author))),1196);1197```11981199Each item independently chains `getChat` → `getUser`, so a slow chat doesn't block author fetches for the others.12001201### 3.9 Per-Request Deduplication with React.cache()12021203**Impact: MEDIUM (deduplicates within request)**12041205Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.12061207**Usage:**12081209```typescript1210import { cache } from "react";12111212export const getCurrentUser = cache(async () => {1213 const session = await auth();1214 if (!session?.user?.id) return null;1215 return await db.user.findUnique({1216 where: { id: session.user.id },1217 });1218});1219```12201221Within a single request, multiple calls to `getCurrentUser()` execute the query only once.12221223**Avoid inline objects as arguments:**12241225`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.12261227**Incorrect: always cache miss**12281229```typescript1230const getUser = cache(async (params: { uid: number }) => {1231 return await db.user.findUnique({ where: { id: params.uid } });1232});12331234// Each call creates new object, never hits cache1235getUser({ uid: 1 });1236getUser({ uid: 1 }); // Cache miss, runs query again1237```12381239**Correct: cache hit**12401241```typescript1242const params = { uid: 1 };1243getUser(params); // Query runs1244getUser(params); // Cache hit (same reference)1245```12461247If you must pass objects, pass the same reference:12481249**Next.js-Specific Note:**12501251In 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:12521253- Database queries (Prisma, Drizzle, etc.)12541255- Heavy computations12561257- Authentication checks12581259- File system operations12601261- Any non-fetch async work12621263Use `React.cache()` to deduplicate these operations across your component tree.12641265Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)12661267### 3.10 Use after() for Non-Blocking Operations12681269**Impact: MEDIUM (faster response times)**12701271Use 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.12721273**Incorrect: blocks response**12741275```tsx1276import { logUserAction } from "@/app/utils";12771278export async function POST(request: Request) {1279 // Perform mutation1280 await updateDatabase(request);12811282 // Logging blocks the response1283 const userAgent = request.headers.get("user-agent") || "unknown";1284 await logUserAction({ userAgent });12851286 return new Response(JSON.stringify({ status: "success" }), {1287 status: 200,1288 headers: { "Content-Type": "application/json" },1289 });1290}1291```12921293**Correct: non-blocking**12941295```tsx1296import { after } from "next/server";1297import { headers, cookies } from "next/headers";1298import { logUserAction } from "@/app/utils";12991300export async function POST(request: Request) {1301 // Perform mutation1302 await updateDatabase(request);13031304 // Log after response is sent1305 after(async () => {1306 const userAgent = (await headers()).get("user-agent") || "unknown";1307 const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous";13081309 logUserAction({ sessionCookie, userAgent });1310 });13111312 return new Response(JSON.stringify({ status: "success" }), {1313 status: 200,1314 headers: { "Content-Type": "application/json" },1315 });1316}1317```13181319The response is sent immediately while logging happens in the background.13201321**Common use cases:**13221323- Analytics tracking13241325- Audit logging13261327- Sending notifications13281329- Cache invalidation13301331- Cleanup tasks13321333**Important notes:**13341335- `after()` runs even if the response fails or redirects13361337- Works in Server Actions, Route Handlers, and Server Components13381339Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)13401341---13421343## 4. Client-Side Data Fetching13441345**Impact: MEDIUM-HIGH**13461347Automatic deduplication and efficient data fetching patterns reduce redundant network requests.13481349### 4.1 Deduplicate Global Event Listeners13501351**Impact: LOW (single listener for N components)**13521353Use `useSWRSubscription()` to share global event listeners across component instances.13541355**Incorrect: N instances = N listeners**13561357```tsx1358function useKeyboardShortcut(key: string, callback: () => void) {1359 useEffect(() => {1360 const handler = (e: KeyboardEvent) => {1361 if (e.metaKey && e.key === key) {1362 callback();1363 }1364 };1365 window.addEventListener("keydown", handler);1366 return () => window.removeEventListener("keydown", handler);1367 }, [key, callback]);1368}1369```13701371When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.13721373**Correct: N instances = 1 listener**13741375```tsx1376import useSWRSubscription from "swr/subscription";13771378// Module-level Map to track callbacks per key1379const keyCallbacks = new Map<string, Set<() => void>>();13801381function useKeyboardShortcut(key: string, callback: () => void) {1382 // Register this callback in the Map1383 useEffect(() => {1384 if (!keyCallbacks.has(key)) {1385 keyCallbacks.set(key, new Set());1386 }1387 keyCallbacks.get(key)!.add(callback);13881389 return () => {1390 const set = keyCallbacks.get(key);1391 if (set) {1392 set.delete(callback);1393 if (set.size === 0) {1394 keyCallbacks.delete(key);1395 }1396 }1397 };1398 }, [key, callback]);13991400 useSWRSubscription("global-keydown", () => {1401 const handler = (e: KeyboardEvent) => {1402 if (e.metaKey && keyCallbacks.has(e.key)) {1403 keyCallbacks.get(e.key)!.forEach((cb) => cb());1404 }1405 };1406 window.addEventListener("keydown", handler);1407 return () => window.removeEventListener("keydown", handler);1408 });1409}14101411function Profile() {1412 // Multiple shortcuts will share the same listener1413 useKeyboardShortcut("p", () => {1414 /* ... */1415 });1416 useKeyboardShortcut("k", () => {1417 /* ... */1418 });1419 // ...1420}1421```14221423### 4.2 Use Passive Event Listeners for Scrolling Performance14241425**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**14261427Add `{ 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.14281429**Incorrect:**14301431```typescript1432useEffect(() => {1433 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);1434 const handleWheel = (e: WheelEvent) => console.log(e.deltaY);14351436 document.addEventListener("touchstart", handleTouch);1437 document.addEventListener("wheel", handleWheel);14381439 return () => {1440 document.removeEventListener("touchstart", handleTouch);1441 document.removeEventListener("wheel", handleWheel);1442 };1443}, []);1444```14451446**Correct:**14471448```typescript1449useEffect(() => {1450 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);1451 const handleWheel = (e: WheelEvent) => console.log(e.deltaY);14521453 document.addEventListener("touchstart", handleTouch, { passive: true });1454 document.addEventListener("wheel", handleWheel, { passive: true });14551456 return () => {1457 document.removeEventListener("touchstart", handleTouch);1458 document.removeEventListener("wheel", handleWheel);1459 };1460}, []);1461```14621463**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.14641465**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.14661467### 4.3 Use SWR for Automatic Deduplication14681469**Impact: MEDIUM-HIGH (automatic deduplication)**14701471SWR enables request deduplication, caching, and revalidation across component instances.14721473**Incorrect: no deduplication, each instance fetches**14741475```tsx1476function UserList() {1477 const [users, setUsers] = useState([]);1478 useEffect(() => {1479 fetch("/api/users")1480 .then((r) => r.json())1481 .then(setUsers);1482 }, []);1483}1484```14851486**Correct: multiple instances share one request**14871488```tsx1489import useSWR from "swr";14901491function UserList() {1492 const { data: users } = useSWR("/api/users", fetcher);1493}1494```14951496**For immutable data:**14971498```tsx1499import { useImmutableSWR } from "@/lib/swr";15001501function StaticContent() {1502 const { data } = useImmutableSWR("/api/config", fetcher);1503}1504```15051506**For mutations:**15071508```tsx1509import { useSWRMutation } from "swr/mutation";15101511function UpdateButton() {1512 const { trigger } = useSWRMutation("/api/user", updateUser);1513 return <button onClick={() => trigger()}>Update</button>;1514}1515```15161517Reference: [https://swr.vercel.app](https://swr.vercel.app)15181519### 4.4 Version and Minimize localStorage Data15201521**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**15221523Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.15241525**Incorrect:**15261527```typescript1528// No version, stores everything, no error handling1529localStorage.setItem("userConfig", JSON.stringify(fullUserObject));1530const data = localStorage.getItem("userConfig");1531```15321533**Correct:**15341535```typescript1536const VERSION = "v2";15371538function saveConfig(config: { theme: string; language: string }) {1539 try {1540 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));1541 } catch {1542 // Throws in incognito/private browsing, quota exceeded, or disabled1543 }1544}15451546function loadConfig() {1547 try {1548 const data = localStorage.getItem(`userConfig:${VERSION}`);1549 return data ? JSON.parse(data) : null;1550 } catch {1551 return null;1552 }1553}15541555// Migration from v1 to v21556function migrate() {1557 try {1558 const v1 = localStorage.getItem("userConfig:v1");1559 if (v1) {1560 const old = JSON.parse(v1);1561 saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang });1562 localStorage.removeItem("userConfig:v1");1563 }1564 } catch {}1565}1566```15671568**Store minimal fields from server responses:**15691570```typescript1571// User object has 20+ fields, only store what UI needs1572function cachePrefs(user: FullUser) {1573 try {1574 localStorage.setItem(1575 "prefs:v1",1576 JSON.stringify({1577 theme: user.preferences.theme,1578 notifications: user.preferences.notifications,1579 }),1580 );1581 } catch {}1582}1583```15841585**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.15861587**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.15881589---15901591## 5. Re-render Optimization15921593**Impact: MEDIUM**15941595Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.15961597### 5.1 Calculate Derived State During Rendering15981599**Impact: MEDIUM (avoids redundant renders and state drift)**16001601If 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.16021603**Incorrect: redundant state and effect**16041605```tsx1606function Form() {1607 const [firstName, setFirstName] = useState("First");1608 const [lastName, setLastName] = useState("Last");1609 const [fullName, setFullName] = useState("");16101611 useEffect(() => {1612 setFullName(firstName + " " + lastName);1613 }, [firstName, lastName]);16141615 return <p>{fullName}</p>;1616}1617```16181619**Correct: derive during render**16201621```tsx1622function Form() {1623 const [firstName, setFirstName] = useState("First");1624 const [lastName, setLastName] = useState("Last");1625 const fullName = firstName + " " + lastName;16261627 return <p>{fullName}</p>;1628}1629```16301631Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)16321633### 5.2 Defer State Reads to Usage Point16341635**Impact: MEDIUM (avoids unnecessary subscriptions)**16361637Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.16381639**Incorrect: subscribes to all searchParams changes**16401641```tsx1642function ShareButton({ chatId }: { chatId: string }) {1643 const searchParams = useSearchParams();16441645 const handleShare = () => {1646 const ref = searchParams.get("ref");1647 shareChat(chatId, { ref });1648 };16491650 return <button onClick={handleShare}>Share</button>;1651}1652```16531654**Correct: reads on demand, no subscription**16551656```tsx1657function ShareButton({ chatId }: { chatId: string }) {1658 const handleShare = () => {1659 const params = new URLSearchParams(window.location.search);1660 const ref = params.get("ref");1661 shareChat(chatId, { ref });1662 };16631664 return <button onClick={handleShare}>Share</button>;1665}1666```16671668### 5.3 Do not wrap a simple expression with a primitive result type in useMemo16691670**Impact: LOW-MEDIUM (wasted computation on every render)**16711672When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.16731674Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.16751676**Incorrect:**16771678```tsx1679function Header({ user, notifications }: Props) {1680 const isLoading = useMemo(() => {1681 return user.isLoading || notifications.isLoading;1682 }, [user.isLoading, notifications.isLoading]);16831684 if (isLoading) return <Skeleton />;1685 // return some markup1686}1687```16881689**Correct:**16901691```tsx1692function Header({ user, notifications }: Props) {1693 const isLoading = user.isLoading || notifications.isLoading;16941695 if (isLoading) return <Skeleton />;1696 // return some markup1697}1698```16991700### 5.4 Don't Define Components Inside Components17011702**Impact: HIGH (prevents remount on every render)**17031704Defining 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.17051706A common reason developers do this is to access parent variables without passing props. Always pass props instead.17071708**Incorrect: remounts on every render**17091710```tsx1711function UserProfile({ user, theme }) {1712 // Defined inside to access `theme` - BAD1713 const Avatar = () => (1714 <img src={user.avatarUrl} className={theme === "dark" ? "avatar-dark" : "avatar-light"} />1715 );17161717 // Defined inside to access `user` - BAD1718 const Stats = () => (1719 <div>1720 <span>{user.followers} followers</span>1721 <span>{user.posts} posts</span>1722 </div>1723 );17241725 return (1726 <div>1727 <Avatar />1728 <Stats />1729 </div>1730 );1731}1732```17331734Every 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.17351736**Correct: pass props instead**17371738```tsx1739function Avatar({ src, theme }: { src: string; theme: string }) {1740 return <img src={src} className={theme === "dark" ? "avatar-dark" : "avatar-light"} />;1741}17421743function Stats({ followers, posts }: { followers: number; posts: number }) {1744 return (1745 <div>1746 <span>{followers} followers</span>1747 <span>{posts} posts</span>1748 </div>1749 );1750}17511752function UserProfile({ user, theme }) {1753 return (1754 <div>1755 <Avatar src={user.avatarUrl} theme={theme} />1756 <Stats followers={user.followers} posts={user.posts} />1757 </div>1758 );1759}1760```17611762**Symptoms of this bug:**17631764- Input fields lose focus on every keystroke17651766- Animations restart unexpectedly17671768- `useEffect` cleanup/setup runs on every parent render17691770- Scroll position resets inside the component17711772### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant17731774**Impact: MEDIUM (restores memoization by using a constant for default value)**17751776When 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()`.17771778To address this issue, extract the default value into a constant.17791780**Incorrect: `onClick` has different values on every rerender**17811782```tsx1783const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {1784 // ...1785})17861787// Used without optional onClick1788<UserAvatar />1789```17901791**Correct: stable default value**17921793```tsx1794const NOOP = () => {};17951796const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {1797 // ...1798})17991800// Used without optional onClick1801<UserAvatar />1802```18031804### 5.6 Extract to Memoized Components18051806**Impact: MEDIUM (enables early returns)**18071808Extract expensive work into memoized components to enable early returns before computation.18091810**Incorrect: computes avatar even when loading**18111812```tsx1813function Profile({ user, loading }: Props) {1814 const avatar = useMemo(() => {1815 const id = computeAvatarId(user);1816 return <Avatar id={id} />;1817 }, [user]);18181819 if (loading) return <Skeleton />;1820 return <div>{avatar}</div>;1821}1822```18231824**Correct: skips computation when loading**18251826```tsx1827const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1828 const id = useMemo(() => computeAvatarId(user), [user]);1829 return <Avatar id={id} />;1830});18311832function Profile({ user, loading }: Props) {1833 if (loading) return <Skeleton />;1834 return (1835 <div>1836 <UserAvatar user={user} />1837 </div>1838 );1839}1840```18411842**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.18431844### 5.7 Narrow Effect Dependencies18451846**Impact: LOW (minimizes effect re-runs)**18471848Specify primitive dependencies instead of objects to minimize effect re-runs.18491850**Incorrect: re-runs on any user field change**18511852```tsx1853useEffect(() => {1854 console.log(user.id);1855}, [user]);1856```18571858**Correct: re-runs only when id changes**18591860```tsx1861useEffect(() => {1862 console.log(user.id);1863}, [user.id]);1864```18651866**For derived state, compute outside effect:**18671868```tsx1869// Incorrect: runs on width=767, 766, 765...1870useEffect(() => {1871 if (width < 768) {1872 enableMobileMode();1873 }1874}, [width]);18751876// Correct: runs only on boolean transition1877const isMobile = width < 768;1878useEffect(() => {1879 if (isMobile) {1880 enableMobileMode();1881 }1882}, [isMobile]);1883```18841885### 5.8 Put Interaction Logic in Event Handlers18861887**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**18881889If 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.18901891**Incorrect: event modeled as state + effect**18921893```tsx1894function Form() {1895 const [submitted, setSubmitted] = useState(false);1896 const theme = useContext(ThemeContext);18971898 useEffect(() => {1899 if (submitted) {1900 post("/api/register");1901 showToast("Registered", theme);1902 }1903 }, [submitted, theme]);19041905 return <button onClick={() => setSubmitted(true)}>Submit</button>;1906}1907```19081909**Correct: do it in the handler**19101911```tsx1912function Form() {1913 const theme = useContext(ThemeContext);19141915 function handleSubmit() {1916 post("/api/register");1917 showToast("Registered", theme);1918 }19191920 return <button onClick={handleSubmit}>Submit</button>;1921}1922```19231924Reference: [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)19251926### 5.9 Split Combined Hook Computations19271928**Impact: MEDIUM (avoids recomputing independent steps)**19291930When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.19311932**Incorrect: changing `sortOrder` recomputes filtering**19331934```tsx1935const sortedProducts = useMemo(() => {1936 const filtered = products.filter((p) => p.category === category);1937 const sorted = filtered.toSorted((a, b) =>1938 sortOrder === "asc" ? a.price - b.price : b.price - a.price,1939 );1940 return sorted;1941}, [products, category, sortOrder]);1942```19431944**Correct: filtering only recomputes when products or category change**19451946```tsx1947const filteredProducts = useMemo(1948 () => products.filter((p) => p.category === category),1949 [products, category],1950);19511952const sortedProducts = useMemo(1953 () =>1954 filteredProducts.toSorted((a, b) =>1955 sortOrder === "asc" ? a.price - b.price : b.price - a.price,1956 ),1957 [filteredProducts, sortOrder],1958);1959```19601961This pattern also applies to `useEffect` when combining unrelated side effects:19621963**Incorrect: both effects run when either dependency changes**19641965```tsx1966useEffect(() => {1967 analytics.trackPageView(pathname);1968 document.title = `${pageTitle} | My App`;1969}, [pathname, pageTitle]);1970```19711972**Correct: effects run independently**19731974```tsx1975useEffect(() => {1976 analytics.trackPageView(pathname);1977}, [pathname]);19781979useEffect(() => {1980 document.title = `${pageTitle} | My App`;1981}, [pageTitle]);1982```19831984**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.19851986### 5.10 Subscribe to Derived State19871988**Impact: MEDIUM (reduces re-render frequency)**19891990Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.19911992**Incorrect: re-renders on every pixel change**19931994```tsx1995function Sidebar() {1996 const width = useWindowWidth(); // updates continuously1997 const isMobile = width < 768;1998 return <nav className={isMobile ? "mobile" : "desktop"} />;1999}2000```20012002**Correct: re-renders only when boolean changes**20032004```tsx2005function Sidebar() {2006 const isMobile = useMediaQuery("(max-width: 767px)");2007 return <nav className={isMobile ? "mobile" : "desktop"} />;2008}2009```20102011### 5.11 Use Functional setState Updates20122013**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**20142015When 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.20162017**Incorrect: requires state as dependency**20182019```tsx2020function TodoList() {2021 const [items, setItems] = useState(initialItems);20222023 // Callback must depend on items, recreated on every items change2024 const addItems = useCallback(2025 (newItems: Item[]) => {2026 setItems([...items, ...newItems]);2027 },2028 [items],2029 ); // ❌ items dependency causes recreations20302031 // Risk of stale closure if dependency is forgotten2032 const removeItem = useCallback((id: string) => {2033 setItems(items.filter((item) => item.id !== id));2034 }, []); // ❌ Missing items dependency - will use stale items!20352036 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;2037}2038```20392040The 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.20412042**Correct: stable callbacks, no stale closures**20432044```tsx2045function TodoList() {2046 const [items, setItems] = useState(initialItems);20472048 // Stable callback, never recreated2049 const addItems = useCallback((newItems: Item[]) => {2050 setItems((curr) => [...curr, ...newItems]);2051 }, []); // ✅ No dependencies needed20522053 // Always uses latest state, no stale closure risk2054 const removeItem = useCallback((id: string) => {2055 setItems((curr) => curr.filter((item) => item.id !== id));2056 }, []); // ✅ Safe and stable20572058 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;2059}2060```20612062**Benefits:**206320641. **Stable callback references** - Callbacks don't need to be recreated when state changes206520662. **No stale closures** - Always operates on the latest state value206720683. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks206920704. **Prevents bugs** - Eliminates the most common source of React closure bugs20712072**When to use functional updates:**20732074- Any setState that depends on the current state value20752076- Inside useCallback/useMemo when state is needed20772078- Event handlers that reference state20792080- Async operations that update state20812082**When direct updates are fine:**20832084- Setting state to a static value: `setCount(0)`20852086- Setting state from props/arguments only: `setName(newName)`20872088- State doesn't depend on previous value20892090**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.20912092### 5.12 Use Lazy State Initialization20932094**Impact: MEDIUM (wasted computation on every render)**20952096Pass 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.20972098**Incorrect: runs on every render**20992100```tsx2101function FilteredList({ items }: { items: Item[] }) {2102 // buildSearchIndex() runs on EVERY render, even after initialization2103 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));2104 const [query, setQuery] = useState("");21052106 // When query changes, buildSearchIndex runs again unnecessarily2107 return <SearchResults index={searchIndex} query={query} />;2108}21092110function UserProfile() {2111 // JSON.parse runs on every render2112 const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));21132114 return <SettingsForm settings={settings} onChange={setSettings} />;2115}2116```21172118**Correct: runs only once**21192120```tsx2121function FilteredList({ items }: { items: Item[] }) {2122 // buildSearchIndex() runs ONLY on initial render2123 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));2124 const [query, setQuery] = useState("");21252126 return <SearchResults index={searchIndex} query={query} />;2127}21282129function UserProfile() {2130 // JSON.parse runs only on initial render2131 const [settings, setSettings] = useState(() => {2132 const stored = localStorage.getItem("settings");2133 return stored ? JSON.parse(stored) : {};2134 });21352136 return <SettingsForm settings={settings} onChange={setSettings} />;2137}2138```21392140Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.21412142For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.21432144### 5.13 Use Transitions for Non-Urgent Updates21452146**Impact: MEDIUM (maintains UI responsiveness)**21472148Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.21492150**Incorrect: blocks UI on every scroll**21512152```tsx2153function ScrollTracker() {2154 const [scrollY, setScrollY] = useState(0);2155 useEffect(() => {2156 const handler = () => setScrollY(window.scrollY);2157 window.addEventListener("scroll", handler, { passive: true });2158 return () => window.removeEventListener("scroll", handler);2159 }, []);2160}2161```21622163**Correct: non-blocking updates**21642165```tsx2166import { startTransition } from "react";21672168function ScrollTracker() {2169 const [scrollY, setScrollY] = useState(0);2170 useEffect(() => {2171 const handler = () => {2172 startTransition(() => setScrollY(window.scrollY));2173 };2174 window.addEventListener("scroll", handler, { passive: true });2175 return () => window.removeEventListener("scroll", handler);2176 }, []);2177}2178```21792180### 5.14 Use useDeferredValue for Expensive Derived Renders21812182**Impact: MEDIUM (keeps input responsive during heavy computation)**21832184When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.21852186**Incorrect: input feels laggy while filtering**21872188```tsx2189function Search({ items }: { items: Item[] }) {2190 const [query, setQuery] = useState("");2191 const filtered = items.filter((item) => fuzzyMatch(item, query));21922193 return (2194 <>2195 <input value={query} onChange={(e) => setQuery(e.target.value)} />2196 <ResultsList results={filtered} />2197 </>2198 );2199}2200```22012202**Correct: input stays snappy, results render when ready**22032204```tsx2205function Search({ items }: { items: Item[] }) {2206 const [query, setQuery] = useState("");2207 const deferredQuery = useDeferredValue(query);2208 const filtered = useMemo(2209 () => items.filter((item) => fuzzyMatch(item, deferredQuery)),2210 [items, deferredQuery],2211 );2212 const isStale = query !== deferredQuery;22132214 return (2215 <>2216 <input value={query} onChange={(e) => setQuery(e.target.value)} />2217 <div style={{ opacity: isStale ? 0.7 : 1 }}>2218 <ResultsList results={filtered} />2219 </div>2220 </>2221 );2222}2223```22242225**When to use:**22262227- Filtering/searching large lists22282229- Expensive visualizations (charts, graphs) reacting to input22302231- Any derived state that causes noticeable render delays22322233**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.22342235Reference: [https://react.dev/reference/react/useDeferredValue](https://react.dev/reference/react/useDeferredValue)22362237### 5.15 Use useRef for Transient Values22382239**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**22402241When 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.22422243**Incorrect: renders every update**22442245```tsx2246function Tracker() {2247 const [lastX, setLastX] = useState(0);22482249 useEffect(() => {2250 const onMove = (e: MouseEvent) => setLastX(e.clientX);2251 window.addEventListener("mousemove", onMove);2252 return () => window.removeEventListener("mousemove", onMove);2253 }, []);22542255 return (2256 <div2257 style={{2258 position: "fixed",2259 top: 0,2260 left: lastX,2261 width: 8,2262 height: 8,2263 background: "black",2264 }}2265 />2266 );2267}2268```22692270**Correct: no re-render for tracking**22712272```tsx2273function Tracker() {2274 const lastXRef = useRef(0);2275 const dotRef = useRef<HTMLDivElement>(null);22762277 useEffect(() => {2278 const onMove = (e: MouseEvent) => {2279 lastXRef.current = e.clientX;2280 const node = dotRef.current;2281 if (node) {2282 node.style.transform = `translateX(${e.clientX}px)`;2283 }2284 };2285 window.addEventListener("mousemove", onMove);2286 return () => window.removeEventListener("mousemove", onMove);2287 }, []);22882289 return (2290 <div2291 ref={dotRef}2292 style={{2293 position: "fixed",2294 top: 0,2295 left: 0,2296 width: 8,2297 height: 8,2298 background: "black",2299 transform: "translateX(0px)",2300 }}2301 />2302 );2303}2304```23052306---23072308## 6. Rendering Performance23092310**Impact: MEDIUM**23112312Optimizing the rendering process reduces the work the browser needs to do.23132314### 6.1 Animate SVG Wrapper Instead of SVG Element23152316**Impact: LOW (enables hardware acceleration)**23172318Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.23192320**Incorrect: animating SVG directly - no hardware acceleration**23212322```tsx2323function LoadingSpinner() {2324 return (2325 <svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">2326 <circle cx="12" cy="12" r="10" stroke="currentColor" />2327 </svg>2328 );2329}2330```23312332**Correct: animating wrapper div - hardware accelerated**23332334```tsx2335function LoadingSpinner() {2336 return (2337 <div className="animate-spin">2338 <svg width="24" height="24" viewBox="0 0 24 24">2339 <circle cx="12" cy="12" r="10" stroke="currentColor" />2340 </svg>2341 </div>2342 );2343}2344```23452346This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.23472348### 6.2 CSS content-visibility for Long Lists23492350**Impact: HIGH (faster initial render)**23512352Apply `content-visibility: auto` to defer off-screen rendering.23532354**CSS:**23552356```css2357.message-item {2358 content-visibility: auto;2359 contain-intrinsic-size: 0 80px;2360}2361```23622363**Example:**23642365```tsx2366function MessageList({ messages }: { messages: Message[] }) {2367 return (2368 <div className="overflow-y-auto h-screen">2369 {messages.map((msg) => (2370 <div key={msg.id} className="message-item">2371 <Avatar user={msg.author} />2372 <div>{msg.content}</div>2373 </div>2374 ))}2375 </div>2376 );2377}2378```23792380For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).23812382### 6.3 Hoist Static JSX Elements23832384**Impact: LOW (avoids re-creation)**23852386Extract static JSX outside components to avoid re-creation.23872388**Incorrect: recreates element every render**23892390```tsx2391function LoadingSkeleton() {2392 return <div className="animate-pulse h-20 bg-gray-200" />;2393}23942395function Container() {2396 return <div>{loading && <LoadingSkeleton />}</div>;2397}2398```23992400**Correct: reuses same element**24012402```tsx2403const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;24042405function Container() {2406 return <div>{loading && loadingSkeleton}</div>;2407}2408```24092410This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.24112412**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.24132414### 6.4 Optimize SVG Precision24152416**Impact: LOW (reduces file size)**24172418Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.24192420**Incorrect: excessive precision**24212422```svg2423<path d="M 10.293847 20.847362 L 30.938472 40.192837" />2424```24252426**Correct: 1 decimal place**24272428```svg2429<path d="M 10.3 20.8 L 30.9 40.2" />2430```24312432**Automate with SVGO:**24332434```bash2435npx svgo --precision=1 --multipass icon.svg2436```24372438### 6.5 Prevent Hydration Mismatch Without Flickering24392440**Impact: MEDIUM (avoids visual flicker and hydration errors)**24412442When 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.24432444**Incorrect: breaks SSR**24452446```tsx2447function ThemeWrapper({ children }: { children: ReactNode }) {2448 // localStorage is not available on server - throws error2449 const theme = localStorage.getItem("theme") || "light";24502451 return <div className={theme}>{children}</div>;2452}2453```24542455Server-side rendering will fail because `localStorage` is undefined.24562457**Incorrect: visual flickering**24582459```tsx2460function ThemeWrapper({ children }: { children: ReactNode }) {2461 const [theme, setTheme] = useState("light");24622463 useEffect(() => {2464 // Runs after hydration - causes visible flash2465 const stored = localStorage.getItem("theme");2466 if (stored) {2467 setTheme(stored);2468 }2469 }, []);24702471 return <div className={theme}>{children}</div>;2472}2473```24742475Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.24762477**Correct: no flicker, no hydration mismatch**24782479```tsx2480function ThemeWrapper({ children }: { children: ReactNode }) {2481 return (2482 <>2483 <div id="theme-wrapper">{children}</div>2484 <script2485 dangerouslySetInnerHTML={{2486 __html: `2487 (function() {2488 try {2489 var theme = localStorage.getItem('theme') || 'light';2490 var el = document.getElementById('theme-wrapper');2491 if (el) el.className = theme;2492 } catch (e) {}2493 })();2494 `,2495 }}2496 />2497 </>2498 );2499}2500```25012502The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.25032504This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.25052506### 6.6 Suppress Expected Hydration Mismatches25072508**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**25092510In 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.25112512**Incorrect: known mismatch warnings**25132514```tsx2515function Timestamp() {2516 return <span>{new Date().toLocaleString()}</span>;2517}2518```25192520**Correct: suppress expected mismatch only**25212522```tsx2523function Timestamp() {2524 return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;2525}2526```25272528### 6.7 Use Activity Component for Show/Hide25292530**Impact: MEDIUM (preserves state/DOM)**25312532Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.25332534**Usage:**25352536```tsx2537import { Activity } from "react";25382539function Dropdown({ isOpen }: Props) {2540 return (2541 <Activity mode={isOpen ? "visible" : "hidden"}>2542 <ExpensiveMenu />2543 </Activity>2544 );2545}2546```25472548Avoids expensive re-renders and state loss.25492550### 6.8 Use defer or async on Script Tags25512552**Impact: HIGH (eliminates render-blocking)**25532554Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.25552556- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order25572558- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order25592560Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.25612562**Incorrect: blocks rendering**25632564```tsx2565export default function Document() {2566 return (2567 <html>2568 <head>2569 <script src="https://example.com/analytics.js" />2570 <script src="/scripts/utils.js" />2571 </head>2572 <body>{/* content */}</body>2573 </html>2574 );2575}2576```25772578**Correct: non-blocking**25792580```tsx2581import Script from "next/script";25822583export default function Page() {2584 return (2585 <>2586 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />2587 <Script src="/scripts/utils.js" strategy="beforeInteractive" />2588 </>2589 );2590}2591```25922593**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:25942595Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)25962597### 6.9 Use Explicit Conditional Rendering25982599**Impact: LOW (prevents rendering 0 or NaN)**26002601Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.26022603**Incorrect: renders "0" when count is 0**26042605```tsx2606function Badge({ count }: { count: number }) {2607 return <div>{count && <span className="badge">{count}</span>}</div>;2608}26092610// When count = 0, renders: <div>0</div>2611// When count = 5, renders: <div><span class="badge">5</span></div>2612```26132614**Correct: renders nothing when count is 0**26152616```tsx2617function Badge({ count }: { count: number }) {2618 return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;2619}26202621// When count = 0, renders: <div></div>2622// When count = 5, renders: <div><span class="badge">5</span></div>2623```26242625### 6.10 Use React DOM Resource Hints26262627**Impact: HIGH (reduces load time for critical resources)**26282629React 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.26302631- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to26322633- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server26342635- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon26362637- **`preloadModule(href)`**: Fetch an ES module you'll use soon26382639- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script26402641- **`preinitModule(href)`**: Fetch and evaluate an ES module26422643**Example: preconnect to third-party APIs**26442645```tsx2646import { preconnect, prefetchDNS } from "react-dom";26472648export default function App() {2649 prefetchDNS("https://analytics.example.com");2650 preconnect("https://api.example.com");26512652 return <main>{/* content */}</main>;2653}2654```26552656**Example: preload critical fonts and styles**26572658```tsx2659import { preload, preinit } from "react-dom";26602661export default function RootLayout({ children }) {2662 // Preload font file2663 preload("/fonts/inter.woff2", { as: "font", type: "font/woff2", crossOrigin: "anonymous" });26642665 // Fetch and apply critical stylesheet immediately2666 preinit("/styles/critical.css", { as: "style" });26672668 return (2669 <html>2670 <body>{children}</body>2671 </html>2672 );2673}2674```26752676**Example: preload modules for code-split routes**26772678```tsx2679import { preloadModule, preinitModule } from "react-dom";26802681function Navigation() {2682 const preloadDashboard = () => {2683 preloadModule("/dashboard.js", { as: "script" });2684 };26852686 return (2687 <nav>2688 <a href="/dashboard" onMouseEnter={preloadDashboard}>2689 Dashboard2690 </a>2691 </nav>2692 );2693}2694```26952696**When to use each:**26972698| API | Use case |26992700|-----|----------|27012702| `prefetchDNS` | Third-party domains you'll connect to later |27032704| `preconnect` | APIs or CDNs you'll fetch from immediately |27052706| `preload` | Critical resources needed for current page |27072708| `preloadModule` | JS modules for likely next navigation |27092710| `preinit` | Stylesheets/scripts that must execute early |27112712| `preinitModule` | ES modules that must execute early |27132714Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)27152716### 6.11 Use useTransition Over Manual Loading States27172718**Impact: LOW (reduces re-renders and improves code clarity)**27192720Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.27212722**Incorrect: manual loading state**27232724```tsx2725function SearchResults() {2726 const [query, setQuery] = useState("");2727 const [results, setResults] = useState([]);2728 const [isLoading, setIsLoading] = useState(false);27292730 const handleSearch = async (value: string) => {2731 setIsLoading(true);2732 setQuery(value);2733 const data = await fetchResults(value);2734 setResults(data);2735 setIsLoading(false);2736 };27372738 return (2739 <>2740 <input onChange={(e) => handleSearch(e.target.value)} />2741 {isLoading && <Spinner />}2742 <ResultsList results={results} />2743 </>2744 );2745}2746```27472748**Correct: useTransition with built-in pending state**27492750```tsx2751import { useTransition, useState } from "react";27522753function SearchResults() {2754 const [query, setQuery] = useState("");2755 const [results, setResults] = useState([]);2756 const [isPending, startTransition] = useTransition();27572758 const handleSearch = (value: string) => {2759 setQuery(value); // Update input immediately27602761 startTransition(async () => {2762 // Fetch and update results2763 const data = await fetchResults(value);2764 setResults(data);2765 });2766 };27672768 return (2769 <>2770 <input onChange={(e) => handleSearch(e.target.value)} />2771 {isPending && <Spinner />}2772 <ResultsList results={results} />2773 </>2774 );2775}2776```27772778**Benefits:**27792780- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`27812782- **Error resilience**: Pending state correctly resets even if the transition throws27832784- **Better responsiveness**: Keeps the UI responsive during updates27852786- **Interrupt handling**: New transitions automatically cancel pending ones27872788Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)27892790---27912792## 7. JavaScript Performance27932794**Impact: LOW-MEDIUM**27952796Micro-optimizations for hot paths can add up to meaningful improvements.27972798### 7.1 Avoid Layout Thrashing27992800**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**28012802Avoid 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.28032804**This is OK: browser batches style changes**28052806```typescript2807function updateElementStyles(element: HTMLElement) {2808 // Each line invalidates style, but browser batches the recalculation2809 element.style.width = "100px";2810 element.style.height = "200px";2811 element.style.backgroundColor = "blue";2812 element.style.border = "1px solid black";2813}2814```28152816**Incorrect: interleaved reads and writes force reflows**28172818```typescript2819function layoutThrashing(element: HTMLElement) {2820 element.style.width = "100px";2821 const width = element.offsetWidth; // Forces reflow2822 element.style.height = "200px";2823 const height = element.offsetHeight; // Forces another reflow2824}2825```28262827**Correct: batch writes, then read once**28282829```typescript2830function updateElementStyles(element: HTMLElement) {2831 // Batch all writes together2832 element.style.width = "100px";2833 element.style.height = "200px";2834 element.style.backgroundColor = "blue";2835 element.style.border = "1px solid black";28362837 // Read after all writes are done (single reflow)2838 const { width, height } = element.getBoundingClientRect();2839}2840```28412842**Correct: batch reads, then writes**28432844```typescript2845function updateElementStyles(element: HTMLElement) {2846 element.classList.add("highlighted-box");28472848 const { width, height } = element.getBoundingClientRect();2849}2850```28512852**Better: use CSS classes**28532854**React example:**28552856```tsx2857// Incorrect: interleaving style changes with layout queries2858function Box({ isHighlighted }: { isHighlighted: boolean }) {2859 const ref = useRef<HTMLDivElement>(null);28602861 useEffect(() => {2862 if (ref.current && isHighlighted) {2863 ref.current.style.width = "100px";2864 const width = ref.current.offsetWidth; // Forces layout2865 ref.current.style.height = "200px";2866 }2867 }, [isHighlighted]);28682869 return <div ref={ref}>Content</div>;2870}28712872// Correct: toggle class2873function Box({ isHighlighted }: { isHighlighted: boolean }) {2874 return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>;2875}2876```28772878Prefer 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.28792880See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.28812882### 7.2 Build Index Maps for Repeated Lookups28832884**Impact: LOW-MEDIUM (1M ops to 2K ops)**28852886Multiple `.find()` calls by the same key should use a Map.28872888**Incorrect (O(n) per lookup):**28892890```typescript2891function processOrders(orders: Order[], users: User[]) {2892 return orders.map((order) => ({2893 ...order,2894 user: users.find((u) => u.id === order.userId),2895 }));2896}2897```28982899**Correct (O(1) per lookup):**29002901```typescript2902function processOrders(orders: Order[], users: User[]) {2903 const userById = new Map(users.map((u) => [u.id, u]));29042905 return orders.map((order) => ({2906 ...order,2907 user: userById.get(order.userId),2908 }));2909}2910```29112912Build map once (O(n)), then all lookups are O(1).29132914For 1000 orders × 1000 users: 1M ops → 2K ops.29152916### 7.3 Cache Property Access in Loops29172918**Impact: LOW-MEDIUM (reduces lookups)**29192920Cache object property lookups in hot paths.29212922**Incorrect: 3 lookups × N iterations**29232924```typescript2925for (let i = 0; i < arr.length; i++) {2926 process(obj.config.settings.value);2927}2928```29292930**Correct: 1 lookup total**29312932```typescript2933const value = obj.config.settings.value;2934const len = arr.length;2935for (let i = 0; i < len; i++) {2936 process(value);2937}2938```29392940### 7.4 Cache Repeated Function Calls29412942**Impact: MEDIUM (avoid redundant computation)**29432944Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.29452946**Incorrect: redundant computation**29472948```typescript2949function ProjectList({ projects }: { projects: Project[] }) {2950 return (2951 <div>2952 {projects.map(project => {2953 // slugify() called 100+ times for same project names2954 const slug = slugify(project.name)29552956 return <ProjectCard key={project.id} slug={slug} />2957 })}2958 </div>2959 )2960}2961```29622963**Correct: cached results**29642965```typescript2966// Module-level cache2967const slugifyCache = new Map<string, string>()29682969function cachedSlugify(text: string): string {2970 if (slugifyCache.has(text)) {2971 return slugifyCache.get(text)!2972 }2973 const result = slugify(text)2974 slugifyCache.set(text, result)2975 return result2976}29772978function ProjectList({ projects }: { projects: Project[] }) {2979 return (2980 <div>2981 {projects.map(project => {2982 // Computed only once per unique project name2983 const slug = cachedSlugify(project.name)29842985 return <ProjectCard key={project.id} slug={slug} />2986 })}2987 </div>2988 )2989}2990```29912992**Simpler pattern for single-value functions:**29932994```typescript2995let isLoggedInCache: boolean | null = null;29962997function isLoggedIn(): boolean {2998 if (isLoggedInCache !== null) {2999 return isLoggedInCache;3000 }30013002 isLoggedInCache = document.cookie.includes("auth=");3003 return isLoggedInCache;3004}30053006// Clear cache when auth changes3007function onAuthChange() {3008 isLoggedInCache = null;3009}3010```30113012Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.30133014Reference: [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)30153016### 7.5 Cache Storage API Calls30173018**Impact: LOW-MEDIUM (reduces expensive I/O)**30193020`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.30213022**Incorrect: reads storage on every call**30233024```typescript3025function getTheme() {3026 return localStorage.getItem("theme") ?? "light";3027}3028// Called 10 times = 10 storage reads3029```30303031**Correct: Map cache**30323033```typescript3034const storageCache = new Map<string, string | null>();30353036function getLocalStorage(key: string) {3037 if (!storageCache.has(key)) {3038 storageCache.set(key, localStorage.getItem(key));3039 }3040 return storageCache.get(key);3041}30423043function setLocalStorage(key: string, value: string) {3044 localStorage.setItem(key, value);3045 storageCache.set(key, value); // keep cache in sync3046}3047```30483049Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.30503051**Cookie caching:**30523053```typescript3054let cookieCache: Record<string, string> | null = null;30553056function getCookie(name: string) {3057 if (!cookieCache) {3058 cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));3059 }3060 return cookieCache[name];3061}3062```30633064**Important: invalidate on external changes**30653066```typescript3067window.addEventListener("storage", (e) => {3068 if (e.key) storageCache.delete(e.key);3069});30703071document.addEventListener("visibilitychange", () => {3072 if (document.visibilityState === "visible") {3073 storageCache.clear();3074 }3075});3076```30773078If storage can change externally (another tab, server-set cookies), invalidate cache:30793080### 7.6 Combine Multiple Array Iterations30813082**Impact: LOW-MEDIUM (reduces iterations)**30833084Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.30853086**Incorrect: 3 iterations**30873088```typescript3089const admins = users.filter((u) => u.isAdmin);3090const testers = users.filter((u) => u.isTester);3091const inactive = users.filter((u) => !u.isActive);3092```30933094**Correct: 1 iteration**30953096```typescript3097const admins: User[] = [];3098const testers: User[] = [];3099const inactive: User[] = [];31003101for (const user of users) {3102 if (user.isAdmin) admins.push(user);3103 if (user.isTester) testers.push(user);3104 if (!user.isActive) inactive.push(user);3105}3106```31073108### 7.7 Defer Non-Critical Work with requestIdleCallback31093110**Impact: MEDIUM (keeps UI responsive during background tasks)**31113112Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.31133114**Incorrect: blocks main thread during user interaction**31153116```typescript3117function handleSearch(query: string) {3118 const results = searchItems(query);3119 setResults(results);31203121 // These block the main thread immediately3122 analytics.track("search", { query });3123 saveToRecentSearches(query);3124 prefetchTopResults(results.slice(0, 3));3125}3126```31273128**Correct: defers non-critical work to idle time**31293130```typescript3131function handleSearch(query: string) {3132 const results = searchItems(query);3133 setResults(results);31343135 // Defer non-critical work to idle periods3136 requestIdleCallback(() => {3137 analytics.track("search", { query });3138 });31393140 requestIdleCallback(() => {3141 saveToRecentSearches(query);3142 });31433144 requestIdleCallback(() => {3145 prefetchTopResults(results.slice(0, 3));3146 });3147}3148```31493150**With timeout for required work:**31513152```typescript3153// Ensure analytics fires within 2 seconds even if browser stays busy3154requestIdleCallback(() => analytics.track("page_view", { path: location.pathname }), {3155 timeout: 2000,3156});3157```31583159**Chunking large tasks:**31603161```typescript3162function processLargeDataset(items: Item[]) {3163 let index = 0;31643165 function processChunk(deadline: IdleDeadline) {3166 // Process items while we have idle time (aim for <50ms chunks)3167 while (index < items.length && deadline.timeRemaining() > 0) {3168 processItem(items[index]);3169 index++;3170 }31713172 // Schedule next chunk if more items remain3173 if (index < items.length) {3174 requestIdleCallback(processChunk);3175 }3176 }31773178 requestIdleCallback(processChunk);3179}3180```31813182**With fallback for unsupported browsers:**31833184```typescript3185const scheduleIdleWork = window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1));31863187scheduleIdleWork(() => {3188 // Non-critical work3189});3190```31913192**When to use:**31933194- Analytics and telemetry31953196- Saving state to localStorage/IndexedDB31973198- Prefetching resources for likely next actions31993200- Processing non-urgent data transformations32013202- Lazy initialization of non-critical features32033204**When NOT to use:**32053206- User-initiated actions that need immediate feedback32073208- Rendering updates the user is waiting for32093210- Time-sensitive operations32113212### 7.8 Early Length Check for Array Comparisons32133214**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**32153216When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.32173218In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).32193220**Incorrect: always runs expensive comparison**32213222```typescript3223function hasChanges(current: string[], original: string[]) {3224 // Always sorts and joins, even when lengths differ3225 return current.sort().join() !== original.sort().join();3226}3227```32283229Two 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.32303231**Correct (O(1) length check first):**32323233```typescript3234function hasChanges(current: string[], original: string[]) {3235 // Early return if lengths differ3236 if (current.length !== original.length) {3237 return true;3238 }3239 // Only sort when lengths match3240 const currentSorted = current.toSorted();3241 const originalSorted = original.toSorted();3242 for (let i = 0; i < currentSorted.length; i++) {3243 if (currentSorted[i] !== originalSorted[i]) {3244 return true;3245 }3246 }3247 return false;3248}3249```32503251This new approach is more efficient because:32523253- It avoids the overhead of sorting and joining the arrays when lengths differ32543255- It avoids consuming memory for the joined strings (especially important for large arrays)32563257- It avoids mutating the original arrays32583259- It returns early when a difference is found32603261### 7.9 Early Return from Functions32623263**Impact: LOW-MEDIUM (avoids unnecessary computation)**32643265Return early when result is determined to skip unnecessary processing.32663267**Incorrect: processes all items even after finding answer**32683269```typescript3270function validateUsers(users: User[]) {3271 let hasError = false;3272 let errorMessage = "";32733274 for (const user of users) {3275 if (!user.email) {3276 hasError = true;3277 errorMessage = "Email required";3278 }3279 if (!user.name) {3280 hasError = true;3281 errorMessage = "Name required";3282 }3283 // Continues checking all users even after error found3284 }32853286 return hasError ? { valid: false, error: errorMessage } : { valid: true };3287}3288```32893290**Correct: returns immediately on first error**32913292```typescript3293function validateUsers(users: User[]) {3294 for (const user of users) {3295 if (!user.email) {3296 return { valid: false, error: "Email required" };3297 }3298 if (!user.name) {3299 return { valid: false, error: "Name required" };3300 }3301 }33023303 return { valid: true };3304}3305```33063307### 7.10 Hoist RegExp Creation33083309**Impact: LOW-MEDIUM (avoids recreation)**33103311Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.33123313**Incorrect: new RegExp every render**33143315```tsx3316function Highlighter({ text, query }: Props) {3317 const regex = new RegExp(`(${query})`, 'gi')3318 const parts = text.split(regex)3319 return <>{parts.map((part, i) => ...)}</>3320}3321```33223323**Correct: memoize or hoist**33243325```tsx3326const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/33273328function Highlighter({ text, query }: Props) {3329 const regex = useMemo(3330 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),3331 [query]3332 )3333 const parts = text.split(regex)3334 return <>{parts.map((part, i) => ...)}</>3335}3336```33373338**Warning: global regex has mutable state**33393340```typescript3341const regex = /foo/g;3342regex.test("foo"); // true, lastIndex = 33343regex.test("foo"); // false, lastIndex = 03344```33453346Global regex (`/g`) has mutable `lastIndex` state:33473348### 7.11 Use flatMap to Map and Filter in One Pass33493350**Impact: LOW-MEDIUM (eliminates intermediate array)**33513352Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.33533354**Incorrect: 2 iterations, intermediate array**33553356```typescript3357const userNames = users.map((user) => (user.isActive ? user.name : null)).filter(Boolean);3358```33593360**Correct: 1 iteration, no intermediate array**33613362```typescript3363const userNames = users.flatMap((user) => (user.isActive ? [user.name] : []));3364```33653366**More examples:**33673368```typescript3369// Extract valid emails from responses3370// Before3371const emails = responses.map((r) => (r.success ? r.data.email : null)).filter(Boolean);33723373// After3374const emails = responses.flatMap((r) => (r.success ? [r.data.email] : []));33753376// Parse and filter valid numbers3377// Before3378const numbers = strings.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));33793380// After3381const numbers = strings.flatMap((s) => {3382 const n = parseInt(s, 10);3383 return isNaN(n) ? [] : [n];3384});3385```33863387**When to use:**33883389- Transforming items while filtering some out33903391- Conditional mapping where some inputs produce no output33923393- Parsing/validating where invalid inputs should be skipped33943395### 7.12 Use Loop for Min/Max Instead of Sort33963397**Impact: LOW (O(n) instead of O(n log n))**33983399Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.34003401**Incorrect (O(n log n) - sort to find latest):**34023403```typescript3404interface Project {3405 id: string;3406 name: string;3407 updatedAt: number;3408}34093410function getLatestProject(projects: Project[]) {3411 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);3412 return sorted[0];3413}3414```34153416Sorts the entire array just to find the maximum value.34173418**Incorrect (O(n log n) - sort for oldest and newest):**34193420```typescript3421function getOldestAndNewest(projects: Project[]) {3422 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);3423 return { oldest: sorted[0], newest: sorted[sorted.length - 1] };3424}3425```34263427Still sorts unnecessarily when only min/max are needed.34283429**Correct (O(n) - single loop):**34303431```typescript3432function getLatestProject(projects: Project[]) {3433 if (projects.length === 0) return null;34343435 let latest = projects[0];34363437 for (let i = 1; i < projects.length; i++) {3438 if (projects[i].updatedAt > latest.updatedAt) {3439 latest = projects[i];3440 }3441 }34423443 return latest;3444}34453446function getOldestAndNewest(projects: Project[]) {3447 if (projects.length === 0) return { oldest: null, newest: null };34483449 let oldest = projects[0];3450 let newest = projects[0];34513452 for (let i = 1; i < projects.length; i++) {3453 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];3454 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];3455 }34563457 return { oldest, newest };3458}3459```34603461Single pass through the array, no copying, no sorting.34623463**Alternative: Math.min/Math.max for small arrays**34643465```typescript3466const numbers = [5, 2, 8, 1, 9];3467const min = Math.min(...numbers);3468const max = Math.max(...numbers);3469```34703471This 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.34723473### 7.13 Use Set/Map for O(1) Lookups34743475**Impact: LOW-MEDIUM (O(n) to O(1))**34763477Convert arrays to Set/Map for repeated membership checks.34783479**Incorrect (O(n) per check):**34803481```typescript3482const allowedIds = ['a', 'b', 'c', ...]3483items.filter(item => allowedIds.includes(item.id))3484```34853486**Correct (O(1) per check):**34873488```typescript3489const allowedIds = new Set(['a', 'b', 'c', ...])3490items.filter(item => allowedIds.has(item.id))3491```34923493### 7.14 Use toSorted() Instead of sort() for Immutability34943495**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**34963497`.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.34983499**Incorrect: mutates original array**35003501```typescript3502function UserList({ users }: { users: User[] }) {3503 // Mutates the users prop array!3504 const sorted = useMemo(3505 () => users.sort((a, b) => a.name.localeCompare(b.name)),3506 [users]3507 )3508 return <div>{sorted.map(renderUser)}</div>3509}3510```35113512**Correct: creates new array**35133514```typescript3515function UserList({ users }: { users: User[] }) {3516 // Creates new sorted array, original unchanged3517 const sorted = useMemo(3518 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),3519 [users]3520 )3521 return <div>{sorted.map(renderUser)}</div>3522}3523```35243525**Why this matters in React:**352635271. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only352835292. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior35303531**Browser support: fallback for older browsers**35323533```typescript3534// Fallback for older browsers3535const sorted = [...items].sort((a, b) => a.value - b.value);3536```35373538`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:35393540**Other immutable array methods:**35413542- `.toSorted()` - immutable sort35433544- `.toReversed()` - immutable reverse35453546- `.toSpliced()` - immutable splice35473548- `.with()` - immutable element replacement35493550---35513552## 8. Advanced Patterns35533554**Impact: LOW**35553556Advanced patterns for specific cases that require careful implementation.35573558### 8.1 Do Not Put Effect Events in Dependency Arrays35593560**Impact: LOW (avoids unnecessary effect re-runs and lint errors)**35613562Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.35633564**Incorrect: Effect Event added as a dependency**35653566```tsx3567import { useEffect, useEffectEvent } from "react";35683569function ChatRoom({ roomId, onConnected }: { roomId: string; onConnected: () => void }) {3570 const handleConnected = useEffectEvent(onConnected);35713572 useEffect(() => {3573 const connection = createConnection(roomId);3574 connection.on("connected", handleConnected);3575 connection.connect();35763577 return () => connection.disconnect();3578 }, [roomId, handleConnected]);3579}3580```35813582Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.35833584**Correct: depend on reactive values, not the Effect Event**35853586```tsx3587import { useEffect, useEffectEvent } from "react";35883589function ChatRoom({ roomId, onConnected }: { roomId: string; onConnected: () => void }) {3590 const handleConnected = useEffectEvent(onConnected);35913592 useEffect(() => {3593 const connection = createConnection(roomId);3594 connection.on("connected", handleConnected);3595 connection.connect();35963597 return () => connection.disconnect();3598 }, [roomId]);3599}3600```36013602Reference: [https://react.dev/reference/react/useEffectEvent#effect-event-in-deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)36033604### 8.2 Initialize App Once, Not Per Mount36053606**Impact: LOW-MEDIUM (avoids duplicate init in development)**36073608Do 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.36093610**Incorrect: runs twice in dev, re-runs on remount**36113612```tsx3613function Comp() {3614 useEffect(() => {3615 loadFromStorage();3616 checkAuthToken();3617 }, []);36183619 // ...3620}3621```36223623**Correct: once per app load**36243625```tsx3626let didInit = false;36273628function Comp() {3629 useEffect(() => {3630 if (didInit) return;3631 didInit = true;3632 loadFromStorage();3633 checkAuthToken();3634 }, []);36353636 // ...3637}3638```36393640Reference: [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)36413642### 8.3 Store Event Handlers in Refs36433644**Impact: LOW (stable subscriptions)**36453646Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.36473648**Incorrect: re-subscribes on every render**36493650```tsx3651function useWindowEvent(event: string, handler: (e) => void) {3652 useEffect(() => {3653 window.addEventListener(event, handler);3654 return () => window.removeEventListener(event, handler);3655 }, [event, handler]);3656}3657```36583659**Correct: stable subscription**36603661```tsx3662import { useEffectEvent } from "react";36633664function useWindowEvent(event: string, handler: (e) => void) {3665 const onEvent = useEffectEvent(handler);36663667 useEffect(() => {3668 window.addEventListener(event, onEvent);3669 return () => window.removeEventListener(event, onEvent);3670 }, [event]);3671}3672```36733674**Alternative: use `useEffectEvent` if you're on latest React:**36753676`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.36773678### 8.4 useEffectEvent for Stable Callback Refs36793680**Impact: LOW (prevents effect re-runs)**36813682Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.36833684**Incorrect: effect re-runs on every callback change**36853686```tsx3687function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3688 const [query, setQuery] = useState("");36893690 useEffect(() => {3691 const timeout = setTimeout(() => onSearch(query), 300);3692 return () => clearTimeout(timeout);3693 }, [query, onSearch]);3694}3695```36963697**Correct: using React's useEffectEvent**36983699```tsx3700import { useEffectEvent } from "react";37013702function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {3703 const [query, setQuery] = useState("");3704 const onSearchEvent = useEffectEvent(onSearch);37053706 useEffect(() => {3707 const timeout = setTimeout(() => onSearchEvent(query), 300);3708 return () => clearTimeout(timeout);3709 }, [query]);3710}3711```37123713---37143715## References371637171. [https://react.dev](https://react.dev)37182. [https://nextjs.org](https://nextjs.org)37193. [https://swr.vercel.app](https://swr.vercel.app)37204. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)37215. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)37226. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)37237. [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)3724
Also in itxSaaad/medlens-plus-app
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| itxSaaad/medlens-plus-app.cursor/rules/testing.mdc · 0 | Cursor rules | testtesting-strategyapimonorepo | 29/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.claude/skills/web-frontend/references/composition-patterns/AGENTS.md · 0 | AGENTS.md | styleapiuido-not | 57/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/agent-discipline.mdc · 0 | Cursor rules | teststyleagent-behaviour | 53/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/devops-ci.mdc · 0 | Cursor rules | securitydo-not | 23/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/engineering-standards.mdc · 0 | Cursor rules | test | 35/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/fastapi-backend.mdc · 0 | Cursor rules | testlint-formatstyle | 66/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/frontend-quality.mdc · 0 | Cursor rules | securityui | 33/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/github-delivery.mdc · 0 | Cursor rules | git | 16/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/graphify.mdc · 0 | Cursor rules | do-not | 45/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/langgraph-ai-workflows.mdc · 0 | Cursor rules | do-not | 32/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/medical-safety.mdc · 0 | Cursor rules | do-not | 23/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/nextjs-frontend.mdc · 0 | Cursor rules | teststylearch | 80/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/pr-quality-gate.mdc · 0 | Cursor rules | testgit | 43/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/rules/technical-seo.mdc · 0 | Cursor rules | no sections | 24/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/skills/web-frontend/references/composition-patterns/AGENTS.md · 0 | AGENTS.md | styleapiuido-not | 45/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.cursor/skills/web-frontend/references/react-best-practices/AGENTS.md · 0 | AGENTS.md | buildstyletypessecurity+6 | 65/100 | 3 days ago | |
| itxSaaad/medlens-plus-app.github/copilot-instructions.md · 0 | Copilot instructions | testgitdo-notagent-behaviour+1 | 70/100 | 3 days ago | |
| itxSaaad/medlens-plus-appAGENTS.md · 0 | AGENTS.md | gitdo-notagent-behaviour | 59/100 | 3 days ago | |
| itxSaaad/medlens-plus-appapps/mobile/CLAUDE.md · 0 | CLAUDE.md | stylearchmonorepoagent-behaviour | 86/100 | 3 days ago | |
| itxSaaad/medlens-plus-appCLAUDE.md · 0 | CLAUDE.md | testgitmonorepodo-not+1 | 76/100 | 3 days ago |
Diff against .cursor/rules/testing.mdc Diff against .claude/skills/web-frontend/references/composition-patterns/AGENTS.md Diff against .cursor/rules/agent-discipline.mdc Diff against .cursor/rules/devops-ci.mdc Diff against .cursor/rules/engineering-standards.mdc Diff against .cursor/rules/fastapi-backend.mdc Diff against .cursor/rules/frontend-quality.mdc Diff against .cursor/rules/github-delivery.mdc Diff against .cursor/rules/graphify.mdc Diff against .cursor/rules/langgraph-ai-workflows.mdc Diff against .cursor/rules/medical-safety.mdc Diff against .cursor/rules/nextjs-frontend.mdc Diff against .cursor/rules/pr-quality-gate.mdc Diff against .cursor/rules/technical-seo.mdc Diff against .cursor/skills/web-frontend/references/composition-patterns/AGENTS.md Diff against .cursor/skills/web-frontend/references/react-best-practices/AGENTS.md Diff against .github/copilot-instructions.md Diff against AGENTS.md Diff against apps/mobile/CLAUDE.md Diff against CLAUDE.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago | |
| SkeneTechnologies/skene-cookbookAGENTS.md · 51 | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 2 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| aaif-goose/gooseAGENTS.md · 52k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 67k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 2 days ago |
