# React Best Practices **Version 1.0.0** Vercel Engineering January 2026 > **Note:** > This document is mainly for agents and LLMs to follow when maintaining, > generating, or refactoring React and Next.js codebases at Vercel. Humans > may also find it useful, but guidance here is optimized for automation > and consistency by AI-assisted workflows. --- ## Abstract Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. --- ## Table of Contents 1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) 2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) - 2.2 [Conditional Module Loading](#22-conditional-module-loading) - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) 3. [Server-Side Performance](#3-server-side-performance) — **HIGH** - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching) - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries) - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition) - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache) - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations) 4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) 5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components) - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies) - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state) - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates) - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization) - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates) 6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) 7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes) - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) - 7.8 [Early Return from Functions](#78-early-return-from-functions) - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) 8. [Advanced Patterns](#8-advanced-patterns) — **LOW** - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs) --- ## 1. Eliminating Waterfalls **Impact: CRITICAL** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. ### 1.1 Defer Await Until Needed **Impact: HIGH (avoids blocking unused code paths)** Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. **Incorrect: blocks both branches** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { const userData = await fetchUserData(userId); if (skipProcessing) { // Returns immediately but still waited for userData return { skipped: true }; } // Only this branch uses userData return processUserData(userData); } ``` **Correct: only blocks when needed** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { if (skipProcessing) { // Returns immediately without waiting return { skipped: true }; } // Fetch only when needed const userData = await fetchUserData(userId); return processUserData(userData); } ``` **Another example: early return optimization** ```typescript // Incorrect: always fetches permissions async function updateResource(resourceId: string, userId: string) { const permissions = await fetchPermissions(userId); const resource = await getResource(resourceId); if (!resource) { return { error: "Not found" }; } if (!permissions.canEdit) { return { error: "Forbidden" }; } return await updateResourceData(resource, permissions); } // Correct: fetches only when needed async function updateResource(resourceId: string, userId: string) { const resource = await getResource(resourceId); if (!resource) { return { error: "Not found" }; } const permissions = await fetchPermissions(userId); if (!permissions.canEdit) { return { error: "Forbidden" }; } return await updateResourceData(resource, permissions); } ``` This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. ### 1.2 Dependency-Based Parallelization **Impact: CRITICAL (2-10× improvement)** For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. **Incorrect: profile waits for config unnecessarily** ```typescript const [user, config] = await Promise.all([fetchUser(), fetchConfig()]); const profile = await fetchProfile(user.id); ``` **Correct: config and profile run in parallel** ```typescript import { all } from "better-all"; const { user, config, profile } = await all({ async user() { return fetchUser(); }, async config() { return fetchConfig(); }, async profile() { return fetchProfile((await this.$.user).id); }, }); ``` Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) ### 1.3 Prevent Waterfall Chains in API Routes **Impact: CRITICAL (2-10× improvement)** In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. **Incorrect: config waits for auth, data waits for both** ```typescript export async function GET(request: Request) { const session = await auth(); const config = await fetchConfig(); const data = await fetchData(session.user.id); return Response.json({ data, config }); } ``` **Correct: auth and config start immediately** ```typescript export async function GET(request: Request) { const sessionPromise = auth(); const configPromise = fetchConfig(); const session = await sessionPromise; const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]); return Response.json({ data, config }); } ``` For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). ### 1.4 Promise.all() for Independent Operations **Impact: CRITICAL (2-10× improvement)** When async operations have no interdependencies, execute them concurrently using `Promise.all()`. **Incorrect: sequential execution, 3 round trips** ```typescript const user = await fetchUser(); const posts = await fetchPosts(); const comments = await fetchComments(); ``` **Correct: parallel execution, 1 round trip** ```typescript const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]); ``` ### 1.5 Strategic Suspense Boundaries **Impact: HIGH (faster initial paint)** Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. **Incorrect: wrapper blocked by data fetching** ```tsx async function Page() { const data = await fetchData(); // Blocks entire page return (
Sidebar
Header
Footer
); } ``` The entire layout waits for data even though only the middle section needs it. **Correct: wrapper shows immediately, data streams in** ```tsx function Page() { return (
Sidebar
Header
}>
Footer
); } async function DataDisplay() { const data = await fetchData(); // Only blocks this component return
{data.content}
; } ``` Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. **Alternative: share promise across components** ```tsx function Page() { // Start fetch immediately, but don't await const dataPromise = fetchData(); return (
Sidebar
Header
}>
Footer
); } function DataDisplay({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise); // Unwraps the promise return
{data.content}
; } function DataSummary({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise); // Reuses the same promise return
{data.summary}
; } ``` Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. **When NOT to use this pattern:** - Critical data needed for layout decisions (affects positioning) - SEO-critical content above the fold - Small, fast queries where suspense overhead isn't worth it - When you want to avoid layout shift (loading → content jump) **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. --- ## 2. Bundle Size Optimization **Impact: CRITICAL** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. ### 2.1 Avoid Barrel File Imports **Impact: CRITICAL (200-800ms import cost, slow builds)** Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. **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. **Incorrect: imports entire library** ```tsx import { Check, X, Menu } from "lucide-react"; // Loads 1,583 modules, takes ~2.8s extra in dev // Runtime cost: 200-800ms on every cold start import { Button, TextField } from "@mui/material"; // Loads 2,225 modules, takes ~4.2s extra in dev ``` **Correct: imports only what you need** ```tsx import Check from "lucide-react/dist/esm/icons/check"; import X from "lucide-react/dist/esm/icons/x"; import Menu from "lucide-react/dist/esm/icons/menu"; // Loads only 3 modules (~2KB vs ~1MB) import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; // Loads only what you use ``` **Alternative: Next.js 13.5+** ```js // next.config.js - use optimizePackageImports module.exports = { experimental: { optimizePackageImports: ["lucide-react", "@mui/material"], }, }; // Then you can keep the ergonomic barrel imports: import { Check, X, Menu } from "lucide-react"; // Automatically transformed to direct imports at build time ``` Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) ### 2.2 Conditional Module Loading **Impact: HIGH (loads large data only when needed)** Load large data or modules only when a feature is activated. **Example: lazy-load animation frames** ```tsx function AnimationPlayer({ enabled, setEnabled, }: { enabled: boolean; setEnabled: React.Dispatch>; }) { const [frames, setFrames] = useState(null); useEffect(() => { if (enabled && !frames && typeof window !== "undefined") { import("./animation-frames.js") .then((mod) => setFrames(mod.frames)) .catch(() => setEnabled(false)); } }, [enabled, frames, setEnabled]); if (!frames) return ; return ; } ``` The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. ### 2.3 Defer Non-Critical Third-Party Libraries **Impact: MEDIUM (loads after hydration)** Analytics, logging, and error tracking don't block user interaction. Load them after hydration. **Incorrect: blocks initial bundle** ```tsx import { Analytics } from "@vercel/analytics/react"; export default function RootLayout({ children }) { return ( {children} ); } ``` **Correct: loads after hydration** ```tsx import dynamic from "next/dynamic"; const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), { ssr: false, }); export default function RootLayout({ children }) { return ( {children} ); } ``` ### 2.4 Dynamic Imports for Heavy Components **Impact: CRITICAL (directly affects TTI and LCP)** Use `next/dynamic` to lazy-load large components not needed on initial render. **Incorrect: Monaco bundles with main chunk ~300KB** ```tsx import { MonacoEditor } from "./monaco-editor"; function CodePanel({ code }: { code: string }) { return ; } ``` **Correct: Monaco loads on demand** ```tsx import dynamic from "next/dynamic"; const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), { ssr: false, }); function CodePanel({ code }: { code: string }) { return ; } ``` ### 2.5 Preload Based on User Intent **Impact: MEDIUM (reduces perceived latency)** Preload heavy bundles before they're needed to reduce perceived latency. **Example: preload on hover/focus** ```tsx function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== "undefined") { void import("./monaco-editor"); } }; return ( ); } ``` **Example: preload when feature flag is enabled** ```tsx function FlagsProvider({ children, flags }: Props) { useEffect(() => { if (flags.editorEnabled && typeof window !== "undefined") { void import("./monaco-editor").then((mod) => mod.init()); } }, [flags.editorEnabled]); return {children}; } ``` The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. --- ## 3. Server-Side Performance **Impact: HIGH** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. ### 3.1 Cross-Request LRU Caching **Impact: HIGH (caches across requests)** `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. **Implementation:** ```typescript import { LRUCache } from "lru-cache"; const cache = new LRUCache({ max: 1000, ttl: 5 * 60 * 1000, // 5 minutes }); export async function getUser(id: string) { const cached = cache.get(id); if (cached) return cached; const user = await db.user.findUnique({ where: { id } }); cache.set(id, user); return user; } // Request 1: DB query, result cached // Request 2: cache hit, no DB query ``` Use when sequential user actions hit multiple endpoints needing the same data within seconds. **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. **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) ### 3.2 Minimize Serialization at RSC Boundaries **Impact: HIGH (reduces data transfer size)** The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. **Incorrect: serializes all 50 fields** ```tsx async function Page() { const user = await fetchUser(); // 50 fields return ; } ("use client"); function Profile({ user }: { user: User }) { return
{user.name}
; // uses 1 field } ``` **Correct: serializes only 1 field** ```tsx async function Page() { const user = await fetchUser(); return ; } ("use client"); function Profile({ name }: { name: string }) { return
{name}
; } ``` ### 3.3 Parallel Data Fetching with Component Composition **Impact: CRITICAL (eliminates server-side waterfalls)** React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. **Incorrect: Sidebar waits for Page's fetch to complete** ```tsx export default async function Page() { const header = await fetchHeader(); return (
{header}
); } async function Sidebar() { const items = await fetchSidebarItems(); return ; } ``` **Correct: both fetch simultaneously** ```tsx async function Header() { const data = await fetchHeader(); return
{data}
; } async function Sidebar() { const items = await fetchSidebarItems(); return ; } export default function Page() { return (
); } ``` **Alternative with children prop:** ```tsx async function Header() { const data = await fetchHeader(); return
{data}
; } async function Sidebar() { const items = await fetchSidebarItems(); return ; } function Layout({ children }: { children: ReactNode }) { return (
{children}
); } export default function Page() { return ( ); } ``` ### 3.4 Per-Request Deduplication with React.cache() **Impact: MEDIUM (deduplicates within request)** Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. **Usage:** ```typescript import { cache } from "react"; export const getCurrentUser = cache(async () => { const session = await auth(); if (!session?.user?.id) return null; return await db.user.findUnique({ where: { id: session.user.id }, }); }); ``` Within a single request, multiple calls to `getCurrentUser()` execute the query only once. **Avoid inline objects as arguments:** `React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. **Incorrect: always cache miss** ```typescript const getUser = cache(async (params: { uid: number }) => { return await db.user.findUnique({ where: { id: params.uid } }); }); // Each call creates new object, never hits cache getUser({ uid: 1 }); getUser({ uid: 1 }); // Cache miss, runs query again ``` **Correct: cache hit** ```typescript const params = { uid: 1 }; getUser(params); // Query runs getUser(params); // Cache hit (same reference) ``` If you must pass objects, pass the same reference: **Next.js-Specific Note:** In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks: - Database queries (Prisma, Drizzle, etc.) - Heavy computations - Authentication checks - File system operations - Any non-fetch async work Use `React.cache()` to deduplicate these operations across your component tree. Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) ### 3.5 Use after() for Non-Blocking Operations **Impact: MEDIUM (faster response times)** Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. **Incorrect: blocks response** ```tsx import { logUserAction } from "@/app/utils"; export async function POST(request: Request) { // Perform mutation await updateDatabase(request); // Logging blocks the response const userAgent = request.headers.get("user-agent") || "unknown"; await logUserAction({ userAgent }); return new Response(JSON.stringify({ status: "success" }), { status: 200, headers: { "Content-Type": "application/json" }, }); } ``` **Correct: non-blocking** ```tsx import { after } from "next/server"; import { headers, cookies } from "next/headers"; import { logUserAction } from "@/app/utils"; export async function POST(request: Request) { // Perform mutation await updateDatabase(request); // Log after response is sent after(async () => { const userAgent = (await headers()).get("user-agent") || "unknown"; const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"; logUserAction({ sessionCookie, userAgent }); }); return new Response(JSON.stringify({ status: "success" }), { status: 200, headers: { "Content-Type": "application/json" }, }); } ``` The response is sent immediately while logging happens in the background. **Common use cases:** - Analytics tracking - Audit logging - Sending notifications - Cache invalidation - Cleanup tasks **Important notes:** - `after()` runs even if the response fails or redirects - Works in Server Actions, Route Handlers, and Server Components Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) --- ## 4. Client-Side Data Fetching **Impact: MEDIUM-HIGH** Automatic deduplication and efficient data fetching patterns reduce redundant network requests. ### 4.1 Deduplicate Global Event Listeners **Impact: LOW (single listener for N components)** Use `useSWRSubscription()` to share global event listeners across component instances. **Incorrect: N instances = N listeners** ```tsx function useKeyboardShortcut(key: string, callback: () => void) { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.metaKey && e.key === key) { callback(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [key, callback]); } ``` When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. **Correct: N instances = 1 listener** ```tsx import useSWRSubscription from "swr/subscription"; // Module-level Map to track callbacks per key const keyCallbacks = new Map void>>(); function useKeyboardShortcut(key: string, callback: () => void) { // Register this callback in the Map useEffect(() => { if (!keyCallbacks.has(key)) { keyCallbacks.set(key, new Set()); } keyCallbacks.get(key)!.add(callback); return () => { const set = keyCallbacks.get(key); if (set) { set.delete(callback); if (set.size === 0) { keyCallbacks.delete(key); } } }; }, [key, callback]); useSWRSubscription("global-keydown", () => { const handler = (e: KeyboardEvent) => { if (e.metaKey && keyCallbacks.has(e.key)) { keyCallbacks.get(e.key)!.forEach((cb) => cb()); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }); } function Profile() { // Multiple shortcuts will share the same listener useKeyboardShortcut("p", () => { /* ... */ }); useKeyboardShortcut("k", () => { /* ... */ }); // ... } ``` ### 4.2 Use Passive Event Listeners for Scrolling Performance **Impact: MEDIUM (eliminates scroll delay caused by event listeners)** Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay. **Incorrect:** ```typescript useEffect(() => { const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); const handleWheel = (e: WheelEvent) => console.log(e.deltaY); document.addEventListener("touchstart", handleTouch); document.addEventListener("wheel", handleWheel); return () => { document.removeEventListener("touchstart", handleTouch); document.removeEventListener("wheel", handleWheel); }; }, []); ``` **Correct:** ```typescript useEffect(() => { const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); const handleWheel = (e: WheelEvent) => console.log(e.deltaY); document.addEventListener("touchstart", handleTouch, { passive: true }); document.addEventListener("wheel", handleWheel, { passive: true }); return () => { document.removeEventListener("touchstart", handleTouch); document.removeEventListener("wheel", handleWheel); }; }, []); ``` **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. **Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. ### 4.3 Use SWR for Automatic Deduplication **Impact: MEDIUM-HIGH (automatic deduplication)** SWR enables request deduplication, caching, and revalidation across component instances. **Incorrect: no deduplication, each instance fetches** ```tsx function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch("/api/users") .then((r) => r.json()) .then(setUsers); }, []); } ``` **Correct: multiple instances share one request** ```tsx import useSWR from "swr"; function UserList() { const { data: users } = useSWR("/api/users", fetcher); } ``` **For immutable data:** ```tsx import { useImmutableSWR } from "@/lib/swr"; function StaticContent() { const { data } = useImmutableSWR("/api/config", fetcher); } ``` **For mutations:** ```tsx import { useSWRMutation } from "swr/mutation"; function UpdateButton() { const { trigger } = useSWRMutation("/api/user", updateUser); return ; } ``` Reference: [https://swr.vercel.app](https://swr.vercel.app) ### 4.4 Version and Minimize localStorage Data **Impact: MEDIUM (prevents schema conflicts, reduces storage size)** Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. **Incorrect:** ```typescript // No version, stores everything, no error handling localStorage.setItem("userConfig", JSON.stringify(fullUserObject)); const data = localStorage.getItem("userConfig"); ``` **Correct:** ```typescript const VERSION = "v2"; function saveConfig(config: { theme: string; language: string }) { try { localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)); } catch { // Throws in incognito/private browsing, quota exceeded, or disabled } } function loadConfig() { try { const data = localStorage.getItem(`userConfig:${VERSION}`); return data ? JSON.parse(data) : null; } catch { return null; } } // Migration from v1 to v2 function migrate() { try { const v1 = localStorage.getItem("userConfig:v1"); if (v1) { const old = JSON.parse(v1); saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang }); localStorage.removeItem("userConfig:v1"); } } catch {} } ``` **Store minimal fields from server responses:** ```typescript // User object has 20+ fields, only store what UI needs function cachePrefs(user: FullUser) { try { localStorage.setItem( "prefs:v1", JSON.stringify({ theme: user.preferences.theme, notifications: user.preferences.notifications, }) ); } catch {} } ``` **Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. **Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. --- ## 5. Re-render Optimization **Impact: MEDIUM** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. ### 5.1 Defer State Reads to Usage Point **Impact: MEDIUM (avoids unnecessary subscriptions)** Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. **Incorrect: subscribes to all searchParams changes** ```tsx function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams(); const handleShare = () => { const ref = searchParams.get("ref"); shareChat(chatId, { ref }); }; return ; } ``` **Correct: reads on demand, no subscription** ```tsx function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search); const ref = params.get("ref"); shareChat(chatId, { ref }); }; return ; } ``` ### 5.2 Extract to Memoized Components **Impact: MEDIUM (enables early returns)** Extract expensive work into memoized components to enable early returns before computation. **Incorrect: computes avatar even when loading** ```tsx function Profile({ user, loading }: Props) { const avatar = useMemo(() => { const id = computeAvatarId(user); return ; }, [user]); if (loading) return ; return
{avatar}
; } ``` **Correct: skips computation when loading** ```tsx const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { const id = useMemo(() => computeAvatarId(user), [user]); return ; }); function Profile({ user, loading }: Props) { if (loading) return ; return (
); } ``` **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. ### 5.3 Narrow Effect Dependencies **Impact: LOW (minimizes effect re-runs)** Specify primitive dependencies instead of objects to minimize effect re-runs. **Incorrect: re-runs on any user field change** ```tsx useEffect(() => { console.log(user.id); }, [user]); ``` **Correct: re-runs only when id changes** ```tsx useEffect(() => { console.log(user.id); }, [user.id]); ``` **For derived state, compute outside effect:** ```tsx // Incorrect: runs on width=767, 766, 765... useEffect(() => { if (width < 768) { enableMobileMode(); } }, [width]); // Correct: runs only on boolean transition const isMobile = width < 768; useEffect(() => { if (isMobile) { enableMobileMode(); } }, [isMobile]); ``` ### 5.4 Subscribe to Derived State **Impact: MEDIUM (reduces re-render frequency)** Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. **Incorrect: re-renders on every pixel change** ```tsx function Sidebar() { const width = useWindowWidth(); // updates continuously const isMobile = width < 768; return