AGENTS.md
plugins/agentic-awesome-skills-claude/skills/react-best-practices/AGENTS.mdAGENTS.md
Quality
61/100
Scores the file, not the repository.Length
7,381 words
57 headings · 105 code blocksRepository
44k
— · pushed 1 days agoLast changed
3 days ago
First indexed 3 days ago.1# React Best Practices23**Version 0.1.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 at Vercel. Humans10> may also find it useful, but guidance here is optimized for automation11> and consistency by AI-assisted workflows.1213---1415## Abstract1617Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.1819---2021## Table of Contents22231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**24 - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)25 - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)26 - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)27 - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)28 - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)292. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**30 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)31 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)32 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)33 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)34 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)353. [Server-Side Performance](#3-server-side-performance) — **HIGH**36 - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching)37 - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries)38 - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition)39 - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache)40 - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations)414. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**42 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)43 - 4.2 [Use SWR for Automatic Deduplication](#42-use-swr-for-automatic-deduplication)445. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**45 - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)46 - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components)47 - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies)48 - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state)49 - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates)50 - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization)51 - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates)526. [Rendering Performance](#6-rendering-performance) — **MEDIUM**53 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)54 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)55 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)56 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)57 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)58 - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)59 - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)607. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**61 - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes)62 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)63 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)64 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)65 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)66 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)67 - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)68 - 7.8 [Early Return from Functions](#78-early-return-from-functions)69 - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)70 - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)71 - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)72 - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)738. [Advanced Patterns](#8-advanced-patterns) — **LOW**74 - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)75 - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs)7677---7879## 1. Eliminating Waterfalls8081**Impact: CRITICAL**8283Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.8485### 1.1 Defer Await Until Needed8687**Impact: HIGH (avoids blocking unused code paths)**8889Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.9091**Incorrect: blocks both branches**9293```typescript94async function handleRequest(userId: string, skipProcessing: boolean) {95 const userData = await fetchUserData(userId)9697 if (skipProcessing) {98 // Returns immediately but still waited for userData99 return { skipped: true }100 }101102 // Only this branch uses userData103 return processUserData(userData)104}105```106107**Correct: only blocks when needed**108109```typescript110async function handleRequest(userId: string, skipProcessing: boolean) {111 if (skipProcessing) {112 // Returns immediately without waiting113 return { skipped: true }114 }115116 // Fetch only when needed117 const userData = await fetchUserData(userId)118 return processUserData(userData)119}120```121122**Another example: early return optimization**123124```typescript125// Incorrect: always fetches permissions126async function updateResource(resourceId: string, userId: string) {127 const permissions = await fetchPermissions(userId)128 const resource = await getResource(resourceId)129130 if (!resource) {131 return { error: 'Not found' }132 }133134 if (!permissions.canEdit) {135 return { error: 'Forbidden' }136 }137138 return await updateResourceData(resource, permissions)139}140141// Correct: fetches only when needed142async function updateResource(resourceId: string, userId: string) {143 const resource = await getResource(resourceId)144145 if (!resource) {146 return { error: 'Not found' }147 }148149 const permissions = await fetchPermissions(userId)150151 if (!permissions.canEdit) {152 return { error: 'Forbidden' }153 }154155 return await updateResourceData(resource, permissions)156}157```158159This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.160161### 1.2 Dependency-Based Parallelization162163**Impact: CRITICAL (2-10× improvement)**164165For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.166167**Incorrect: profile waits for config unnecessarily**168169```typescript170const [user, config] = await Promise.all([171 fetchUser(),172 fetchConfig()173])174const profile = await fetchProfile(user.id)175```176177**Correct: config and profile run in parallel**178179```typescript180import { all } from 'better-all'181182const { user, config, profile } = await all({183 async user() { return fetchUser() },184 async config() { return fetchConfig() },185 async profile() {186 return fetchProfile((await this.$.user).id)187 }188})189```190191Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)192193### 1.3 Prevent Waterfall Chains in API Routes194195**Impact: CRITICAL (2-10× improvement)**196197In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.198199**Incorrect: config waits for auth, data waits for both**200201```typescript202export async function GET(request: Request) {203 const session = await auth()204 const config = await fetchConfig()205 const data = await fetchData(session.user.id)206 return Response.json({ data, config })207}208```209210**Correct: auth and config start immediately**211212```typescript213export async function GET(request: Request) {214 const sessionPromise = auth()215 const configPromise = fetchConfig()216 const session = await sessionPromise217 const [config, data] = await Promise.all([218 configPromise,219 fetchData(session.user.id)220 ])221 return Response.json({ data, config })222}223```224225For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).226227### 1.4 Promise.all() for Independent Operations228229**Impact: CRITICAL (2-10× improvement)**230231When async operations have no interdependencies, execute them concurrently using `Promise.all()`.232233**Incorrect: sequential execution, 3 round trips**234235```typescript236const user = await fetchUser()237const posts = await fetchPosts()238const comments = await fetchComments()239```240241**Correct: parallel execution, 1 round trip**242243```typescript244const [user, posts, comments] = await Promise.all([245 fetchUser(),246 fetchPosts(),247 fetchComments()248])249```250251### 1.5 Strategic Suspense Boundaries252253**Impact: HIGH (faster initial paint)**254255Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.256257**Incorrect: wrapper blocked by data fetching**258259```tsx260async function Page() {261 const data = await fetchData() // Blocks entire page262263 return (264 <div>265 <div>Sidebar</div>266 <div>Header</div>267 <div>268 <DataDisplay data={data} />269 </div>270 <div>Footer</div>271 </div>272 )273}274```275276The entire layout waits for data even though only the middle section needs it.277278**Correct: wrapper shows immediately, data streams in**279280```tsx281function Page() {282 return (283 <div>284 <div>Sidebar</div>285 <div>Header</div>286 <div>287 <Suspense fallback={<Skeleton />}>288 <DataDisplay />289 </Suspense>290 </div>291 <div>Footer</div>292 </div>293 )294}295296async function DataDisplay() {297 const data = await fetchData() // Only blocks this component298 return <div>{data.content}</div>299}300```301302Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.303304**Alternative: share promise across components**305306```tsx307function Page() {308 // Start fetch immediately, but don't await309 const dataPromise = fetchData()310311 return (312 <div>313 <div>Sidebar</div>314 <div>Header</div>315 <Suspense fallback={<Skeleton />}>316 <DataDisplay dataPromise={dataPromise} />317 <DataSummary dataPromise={dataPromise} />318 </Suspense>319 <div>Footer</div>320 </div>321 )322}323324function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {325 const data = use(dataPromise) // Unwraps the promise326 return <div>{data.content}</div>327}328329function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {330 const data = use(dataPromise) // Reuses the same promise331 return <div>{data.summary}</div>332}333```334335Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.336337**When NOT to use this pattern:**338339- Critical data needed for layout decisions (affects positioning)340341- SEO-critical content above the fold342343- Small, fast queries where suspense overhead isn't worth it344345- When you want to avoid layout shift (loading → content jump)346347**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.348349---350351## 2. Bundle Size Optimization352353**Impact: CRITICAL**354355Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.356357### 2.1 Avoid Barrel File Imports358359**Impact: CRITICAL (200-800ms import cost, slow builds)**360361Import 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'`).362363Popular 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.364365**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.366367**Incorrect: imports entire library**368369```tsx370import { Check, X, Menu } from 'lucide-react'371// Loads 1,583 modules, takes ~2.8s extra in dev372// Runtime cost: 200-800ms on every cold start373374import { Button, TextField } from '@mui/material'375// Loads 2,225 modules, takes ~4.2s extra in dev376```377378**Correct: imports only what you need**379380```tsx381import Check from 'lucide-react/dist/esm/icons/check'382import X from 'lucide-react/dist/esm/icons/x'383import Menu from 'lucide-react/dist/esm/icons/menu'384// Loads only 3 modules (~2KB vs ~1MB)385386import Button from '@mui/material/Button'387import TextField from '@mui/material/TextField'388// Loads only what you use389```390391**Alternative: Next.js 13.5+**392393```js394// next.config.js - use optimizePackageImports395module.exports = {396 experimental: {397 optimizePackageImports: ['lucide-react', '@mui/material']398 }399}400401// Then you can keep the ergonomic barrel imports:402import { Check, X, Menu } from 'lucide-react'403// Automatically transformed to direct imports at build time404```405406Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.407408Libraries 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`.409410Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)411412### 2.2 Conditional Module Loading413414**Impact: HIGH (loads large data only when needed)**415416Load large data or modules only when a feature is activated.417418**Example: lazy-load animation frames**419420```tsx421function AnimationPlayer({ enabled }: { enabled: boolean }) {422 const [frames, setFrames] = useState<Frame[] | null>(null)423424 useEffect(() => {425 if (enabled && !frames && typeof window !== 'undefined') {426 import('./animation-frames.js')427 .then(mod => setFrames(mod.frames))428 .catch(() => setEnabled(false))429 }430 }, [enabled, frames])431432 if (!frames) return <Skeleton />433 return <Canvas frames={frames} />434}435```436437The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.438439### 2.3 Defer Non-Critical Third-Party Libraries440441**Impact: MEDIUM (loads after hydration)**442443Analytics, logging, and error tracking don't block user interaction. Load them after hydration.444445**Incorrect: blocks initial bundle**446447```tsx448import { Analytics } from '@vercel/analytics/react'449450export default function RootLayout({ children }) {451 return (452 <html>453 <body>454 {children}455 <Analytics />456 </body>457 </html>458 )459}460```461462**Correct: loads after hydration**463464```tsx465import dynamic from 'next/dynamic'466467const Analytics = dynamic(468 () => import('@vercel/analytics/react').then(m => m.Analytics),469 { ssr: false }470)471472export default function RootLayout({ children }) {473 return (474 <html>475 <body>476 {children}477 <Analytics />478 </body>479 </html>480 )481}482```483484### 2.4 Dynamic Imports for Heavy Components485486**Impact: CRITICAL (directly affects TTI and LCP)**487488Use `next/dynamic` to lazy-load large components not needed on initial render.489490**Incorrect: Monaco bundles with main chunk ~300KB**491492```tsx493import { MonacoEditor } from './monaco-editor'494495function CodePanel({ code }: { code: string }) {496 return <MonacoEditor value={code} />497}498```499500**Correct: Monaco loads on demand**501502```tsx503import dynamic from 'next/dynamic'504505const MonacoEditor = dynamic(506 () => import('./monaco-editor').then(m => m.MonacoEditor),507 { ssr: false }508)509510function CodePanel({ code }: { code: string }) {511 return <MonacoEditor value={code} />512}513```514515### 2.5 Preload Based on User Intent516517**Impact: MEDIUM (reduces perceived latency)**518519Preload heavy bundles before they're needed to reduce perceived latency.520521**Example: preload on hover/focus**522523```tsx524function EditorButton({ onClick }: { onClick: () => void }) {525 const preload = () => {526 if (typeof window !== 'undefined') {527 void import('./monaco-editor')528 }529 }530531 return (532 <button533 onMouseEnter={preload}534 onFocus={preload}535 onClick={onClick}536 >537 Open Editor538 </button>539 )540}541```542543**Example: preload when feature flag is enabled**544545```tsx546function FlagsProvider({ children, flags }: Props) {547 useEffect(() => {548 if (flags.editorEnabled && typeof window !== 'undefined') {549 void import('./monaco-editor').then(mod => mod.init())550 }551 }, [flags.editorEnabled])552553 return <FlagsContext.Provider value={flags}>554 {children}555 </FlagsContext.Provider>556}557```558559The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.560561---562563## 3. Server-Side Performance564565**Impact: HIGH**566567Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.568569### 3.1 Cross-Request LRU Caching570571**Impact: HIGH (caches across requests)**572573`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.574575**Implementation:**576577```typescript578import { LRUCache } from 'lru-cache'579580const cache = new LRUCache<string, any>({581 max: 1000,582 ttl: 5 * 60 * 1000 // 5 minutes583})584585export async function getUser(id: string) {586 const cached = cache.get(id)587 if (cached) return cached588589 const user = await db.user.findUnique({ where: { id } })590 cache.set(id, user)591 return user592}593594// Request 1: DB query, result cached595// Request 2: cache hit, no DB query596```597598Use when sequential user actions hit multiple endpoints needing the same data within seconds.599600**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.601602**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.603604Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)605606### 3.2 Minimize Serialization at RSC Boundaries607608**Impact: HIGH (reduces data transfer size)**609610The 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.611612**Incorrect: serializes all 50 fields**613614```tsx615async function Page() {616 const user = await fetchUser() // 50 fields617 return <Profile user={user} />618}619620'use client'621function Profile({ user }: { user: User }) {622 return <div>{user.name}</div> // uses 1 field623}624```625626**Correct: serializes only 1 field**627628```tsx629async function Page() {630 const user = await fetchUser()631 return <Profile name={user.name} />632}633634'use client'635function Profile({ name }: { name: string }) {636 return <div>{name}</div>637}638```639640### 3.3 Parallel Data Fetching with Component Composition641642**Impact: CRITICAL (eliminates server-side waterfalls)**643644React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.645646**Incorrect: Sidebar waits for Page's fetch to complete**647648```tsx649export default async function Page() {650 const header = await fetchHeader()651 return (652 <div>653 <div>{header}</div>654 <Sidebar />655 </div>656 )657}658659async function Sidebar() {660 const items = await fetchSidebarItems()661 return <nav>{items.map(renderItem)}</nav>662}663```664665**Correct: both fetch simultaneously**666667```tsx668async function Header() {669 const data = await fetchHeader()670 return <div>{data}</div>671}672673async function Sidebar() {674 const items = await fetchSidebarItems()675 return <nav>{items.map(renderItem)}</nav>676}677678export default function Page() {679 return (680 <div>681 <Header />682 <Sidebar />683 </div>684 )685}686```687688**Alternative with children prop:**689690```tsx691async function Layout({ children }: { children: ReactNode }) {692 const header = await fetchHeader()693 return (694 <div>695 <div>{header}</div>696 {children}697 </div>698 )699}700701async function Sidebar() {702 const items = await fetchSidebarItems()703 return <nav>{items.map(renderItem)}</nav>704}705706export default function Page() {707 return (708 <Layout>709 <Sidebar />710 </Layout>711 )712}713```714715### 3.4 Per-Request Deduplication with React.cache()716717**Impact: MEDIUM (deduplicates within request)**718719Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.720721**Usage:**722723```typescript724import { cache } from 'react'725726export const getCurrentUser = cache(async () => {727 const session = await auth()728 if (!session?.user?.id) return null729 return await db.user.findUnique({730 where: { id: session.user.id }731 })732})733```734735Within a single request, multiple calls to `getCurrentUser()` execute the query only once.736737### 3.5 Use after() for Non-Blocking Operations738739**Impact: MEDIUM (faster response times)**740741Use 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.742743**Incorrect: blocks response**744745```tsx746import { logUserAction } from '@/app/utils'747748export async function POST(request: Request) {749 // Perform mutation750 await updateDatabase(request)751752 // Logging blocks the response753 const userAgent = request.headers.get('user-agent') || 'unknown'754 await logUserAction({ userAgent })755756 return new Response(JSON.stringify({ status: 'success' }), {757 status: 200,758 headers: { 'Content-Type': 'application/json' }759 })760}761```762763**Correct: non-blocking**764765```tsx766import { after } from 'next/server'767import { headers, cookies } from 'next/headers'768import { logUserAction } from '@/app/utils'769770export async function POST(request: Request) {771 // Perform mutation772 await updateDatabase(request)773774 // Log after response is sent775 after(async () => {776 const userAgent = (await headers()).get('user-agent') || 'unknown'777 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'778779 logUserAction({ sessionCookie, userAgent })780 })781782 return new Response(JSON.stringify({ status: 'success' }), {783 status: 200,784 headers: { 'Content-Type': 'application/json' }785 })786}787```788789The response is sent immediately while logging happens in the background.790791**Common use cases:**792793- Analytics tracking794795- Audit logging796797- Sending notifications798799- Cache invalidation800801- Cleanup tasks802803**Important notes:**804805- `after()` runs even if the response fails or redirects806807- Works in Server Actions, Route Handlers, and Server Components808809Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)810811---812813## 4. Client-Side Data Fetching814815**Impact: MEDIUM-HIGH**816817Automatic deduplication and efficient data fetching patterns reduce redundant network requests.818819### 4.1 Deduplicate Global Event Listeners820821**Impact: LOW (single listener for N components)**822823Use `useSWRSubscription()` to share global event listeners across component instances.824825**Incorrect: N instances = N listeners**826827```tsx828function useKeyboardShortcut(key: string, callback: () => void) {829 useEffect(() => {830 const handler = (e: KeyboardEvent) => {831 if (e.metaKey && e.key === key) {832 callback()833 }834 }835 window.addEventListener('keydown', handler)836 return () => window.removeEventListener('keydown', handler)837 }, [key, callback])838}839```840841When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.842843**Correct: N instances = 1 listener**844845```tsx846import useSWRSubscription from 'swr/subscription'847848// Module-level Map to track callbacks per key849const keyCallbacks = new Map<string, Set<() => void>>()850851function useKeyboardShortcut(key: string, callback: () => void) {852 // Register this callback in the Map853 useEffect(() => {854 if (!keyCallbacks.has(key)) {855 keyCallbacks.set(key, new Set())856 }857 keyCallbacks.get(key)!.add(callback)858859 return () => {860 const set = keyCallbacks.get(key)861 if (set) {862 set.delete(callback)863 if (set.size === 0) {864 keyCallbacks.delete(key)865 }866 }867 }868 }, [key, callback])869870 useSWRSubscription('global-keydown', () => {871 const handler = (e: KeyboardEvent) => {872 if (e.metaKey && keyCallbacks.has(e.key)) {873 keyCallbacks.get(e.key)!.forEach(cb => cb())874 }875 }876 window.addEventListener('keydown', handler)877 return () => window.removeEventListener('keydown', handler)878 })879}880881function Profile() {882 // Multiple shortcuts will share the same listener883 useKeyboardShortcut('p', () => { /* ... */ })884 useKeyboardShortcut('k', () => { /* ... */ })885 // ...886}887```888889### 4.2 Use SWR for Automatic Deduplication890891**Impact: MEDIUM-HIGH (automatic deduplication)**892893SWR enables request deduplication, caching, and revalidation across component instances.894895**Incorrect: no deduplication, each instance fetches**896897```tsx898function UserList() {899 const [users, setUsers] = useState([])900 useEffect(() => {901 fetch('/api/users')902 .then(r => r.json())903 .then(setUsers)904 }, [])905}906```907908**Correct: multiple instances share one request**909910```tsx911import useSWR from 'swr'912913function UserList() {914 const { data: users } = useSWR('/api/users', fetcher)915}916```917918**For immutable data:**919920```tsx921import { useImmutableSWR } from '@/lib/swr'922923function StaticContent() {924 const { data } = useImmutableSWR('/api/config', fetcher)925}926```927928**For mutations:**929930```tsx931import { useSWRMutation } from 'swr/mutation'932933function UpdateButton() {934 const { trigger } = useSWRMutation('/api/user', updateUser)935 return <button onClick={() => trigger()}>Update</button>936}937```938939Reference: [https://swr.vercel.app](https://swr.vercel.app)940941---942943## 5. Re-render Optimization944945**Impact: MEDIUM**946947Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.948949### 5.1 Defer State Reads to Usage Point950951**Impact: MEDIUM (avoids unnecessary subscriptions)**952953Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.954955**Incorrect: subscribes to all searchParams changes**956957```tsx958function ShareButton({ chatId }: { chatId: string }) {959 const searchParams = useSearchParams()960961 const handleShare = () => {962 const ref = searchParams.get('ref')963 shareChat(chatId, { ref })964 }965966 return <button onClick={handleShare}>Share</button>967}968```969970**Correct: reads on demand, no subscription**971972```tsx973function ShareButton({ chatId }: { chatId: string }) {974 const handleShare = () => {975 const params = new URLSearchParams(window.location.search)976 const ref = params.get('ref')977 shareChat(chatId, { ref })978 }979980 return <button onClick={handleShare}>Share</button>981}982```983984### 5.2 Extract to Memoized Components985986**Impact: MEDIUM (enables early returns)**987988Extract expensive work into memoized components to enable early returns before computation.989990**Incorrect: computes avatar even when loading**991992```tsx993function Profile({ user, loading }: Props) {994 const avatar = useMemo(() => {995 const id = computeAvatarId(user)996 return <Avatar id={id} />997 }, [user])998999 if (loading) return <Skeleton />1000 return <div>{avatar}</div>1001}1002```10031004**Correct: skips computation when loading**10051006```tsx1007const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {1008 const id = useMemo(() => computeAvatarId(user), [user])1009 return <Avatar id={id} />1010})10111012function Profile({ user, loading }: Props) {1013 if (loading) return <Skeleton />1014 return (1015 <div>1016 <UserAvatar user={user} />1017 </div>1018 )1019}1020```10211022**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.10231024### 5.3 Narrow Effect Dependencies10251026**Impact: LOW (minimizes effect re-runs)**10271028Specify primitive dependencies instead of objects to minimize effect re-runs.10291030**Incorrect: re-runs on any user field change**10311032```tsx1033useEffect(() => {1034 console.log(user.id)1035}, [user])1036```10371038**Correct: re-runs only when id changes**10391040```tsx1041useEffect(() => {1042 console.log(user.id)1043}, [user.id])1044```10451046**For derived state, compute outside effect:**10471048```tsx1049// Incorrect: runs on width=767, 766, 765...1050useEffect(() => {1051 if (width < 768) {1052 enableMobileMode()1053 }1054}, [width])10551056// Correct: runs only on boolean transition1057const isMobile = width < 7681058useEffect(() => {1059 if (isMobile) {1060 enableMobileMode()1061 }1062}, [isMobile])1063```10641065### 5.4 Subscribe to Derived State10661067**Impact: MEDIUM (reduces re-render frequency)**10681069Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.10701071**Incorrect: re-renders on every pixel change**10721073```tsx1074function Sidebar() {1075 const width = useWindowWidth() // updates continuously1076 const isMobile = width < 7681077 return <nav className={isMobile ? 'mobile' : 'desktop'}>1078}1079```10801081**Correct: re-renders only when boolean changes**10821083```tsx1084function Sidebar() {1085 const isMobile = useMediaQuery('(max-width: 767px)')1086 return <nav className={isMobile ? 'mobile' : 'desktop'}>1087}1088```10891090### 5.5 Use Functional setState Updates10911092**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**10931094When 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.10951096**Incorrect: requires state as dependency**10971098```tsx1099function TodoList() {1100 const [items, setItems] = useState(initialItems)11011102 // Callback must depend on items, recreated on every items change1103 const addItems = useCallback((newItems: Item[]) => {1104 setItems([...items, ...newItems])1105 }, [items]) // ❌ items dependency causes recreations11061107 // Risk of stale closure if dependency is forgotten1108 const removeItem = useCallback((id: string) => {1109 setItems(items.filter(item => item.id !== id))1110 }, []) // ❌ Missing items dependency - will use stale items!11111112 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1113}1114```11151116The 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.11171118**Correct: stable callbacks, no stale closures**11191120```tsx1121function TodoList() {1122 const [items, setItems] = useState(initialItems)11231124 // Stable callback, never recreated1125 const addItems = useCallback((newItems: Item[]) => {1126 setItems(curr => [...curr, ...newItems])1127 }, []) // ✅ No dependencies needed11281129 // Always uses latest state, no stale closure risk1130 const removeItem = useCallback((id: string) => {1131 setItems(curr => curr.filter(item => item.id !== id))1132 }, []) // ✅ Safe and stable11331134 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />1135}1136```11371138**Benefits:**113911401. **Stable callback references** - Callbacks don't need to be recreated when state changes114111422. **No stale closures** - Always operates on the latest state value114311443. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks114511464. **Prevents bugs** - Eliminates the most common source of React closure bugs11471148**When to use functional updates:**11491150- Any setState that depends on the current state value11511152- Inside useCallback/useMemo when state is needed11531154- Event handlers that reference state11551156- Async operations that update state11571158**When direct updates are fine:**11591160- Setting state to a static value: `setCount(0)`11611162- Setting state from props/arguments only: `setName(newName)`11631164- State doesn't depend on previous value11651166**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.11671168### 5.6 Use Lazy State Initialization11691170**Impact: MEDIUM (wasted computation on every render)**11711172Pass 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.11731174**Incorrect: runs on every render**11751176```tsx1177function FilteredList({ items }: { items: Item[] }) {1178 // buildSearchIndex() runs on EVERY render, even after initialization1179 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))1180 const [query, setQuery] = useState('')11811182 // When query changes, buildSearchIndex runs again unnecessarily1183 return <SearchResults index={searchIndex} query={query} />1184}11851186function UserProfile() {1187 // JSON.parse runs on every render1188 const [settings, setSettings] = useState(1189 JSON.parse(localStorage.getItem('settings') || '{}')1190 )11911192 return <SettingsForm settings={settings} onChange={setSettings} />1193}1194```11951196**Correct: runs only once**11971198```tsx1199function FilteredList({ items }: { items: Item[] }) {1200 // buildSearchIndex() runs ONLY on initial render1201 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))1202 const [query, setQuery] = useState('')12031204 return <SearchResults index={searchIndex} query={query} />1205}12061207function UserProfile() {1208 // JSON.parse runs only on initial render1209 const [settings, setSettings] = useState(() => {1210 const stored = localStorage.getItem('settings')1211 return stored ? JSON.parse(stored) : {}1212 })12131214 return <SettingsForm settings={settings} onChange={setSettings} />1215}1216```12171218Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.12191220For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.12211222### 5.7 Use Transitions for Non-Urgent Updates12231224**Impact: MEDIUM (maintains UI responsiveness)**12251226Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.12271228**Incorrect: blocks UI on every scroll**12291230```tsx1231function ScrollTracker() {1232 const [scrollY, setScrollY] = useState(0)1233 useEffect(() => {1234 const handler = () => setScrollY(window.scrollY)1235 window.addEventListener('scroll', handler, { passive: true })1236 return () => window.removeEventListener('scroll', handler)1237 }, [])1238}1239```12401241**Correct: non-blocking updates**12421243```tsx1244import { startTransition } from 'react'12451246function ScrollTracker() {1247 const [scrollY, setScrollY] = useState(0)1248 useEffect(() => {1249 const handler = () => {1250 startTransition(() => setScrollY(window.scrollY))1251 }1252 window.addEventListener('scroll', handler, { passive: true })1253 return () => window.removeEventListener('scroll', handler)1254 }, [])1255}1256```12571258---12591260## 6. Rendering Performance12611262**Impact: MEDIUM**12631264Optimizing the rendering process reduces the work the browser needs to do.12651266### 6.1 Animate SVG Wrapper Instead of SVG Element12671268**Impact: LOW (enables hardware acceleration)**12691270Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.12711272**Incorrect: animating SVG directly - no hardware acceleration**12731274```tsx1275function LoadingSpinner() {1276 return (1277 <svg1278 className="animate-spin"1279 width="24"1280 height="24"1281 viewBox="0 0 24 24"1282 >1283 <circle cx="12" cy="12" r="10" stroke="currentColor" />1284 </svg>1285 )1286}1287```12881289**Correct: animating wrapper div - hardware accelerated**12901291```tsx1292function LoadingSpinner() {1293 return (1294 <div className="animate-spin">1295 <svg1296 width="24"1297 height="24"1298 viewBox="0 0 24 24"1299 >1300 <circle cx="12" cy="12" r="10" stroke="currentColor" />1301 </svg>1302 </div>1303 )1304}1305```13061307This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.13081309### 6.2 CSS content-visibility for Long Lists13101311**Impact: HIGH (faster initial render)**13121313Apply `content-visibility: auto` to defer off-screen rendering.13141315**CSS:**13161317```css1318.message-item {1319 content-visibility: auto;1320 contain-intrinsic-size: 0 80px;1321}1322```13231324**Example:**13251326```tsx1327function MessageList({ messages }: { messages: Message[] }) {1328 return (1329 <div className="overflow-y-auto h-screen">1330 {messages.map(msg => (1331 <div key={msg.id} className="message-item">1332 <Avatar user={msg.author} />1333 <div>{msg.content}</div>1334 </div>1335 ))}1336 </div>1337 )1338}1339```13401341For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).13421343### 6.3 Hoist Static JSX Elements13441345**Impact: LOW (avoids re-creation)**13461347Extract static JSX outside components to avoid re-creation.13481349**Incorrect: recreates element every render**13501351```tsx1352function LoadingSkeleton() {1353 return <div className="animate-pulse h-20 bg-gray-200" />1354}13551356function Container() {1357 return (1358 <div>1359 {loading && <LoadingSkeleton />}1360 </div>1361 )1362}1363```13641365**Correct: reuses same element**13661367```tsx1368const loadingSkeleton = (1369 <div className="animate-pulse h-20 bg-gray-200" />1370)13711372function Container() {1373 return (1374 <div>1375 {loading && loadingSkeleton}1376 </div>1377 )1378}1379```13801381This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.13821383**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.13841385### 6.4 Optimize SVG Precision13861387**Impact: LOW (reduces file size)**13881389Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.13901391**Incorrect: excessive precision**13921393```svg1394<path d="M 10.293847 20.847362 L 30.938472 40.192837" />1395```13961397**Correct: 1 decimal place**13981399```svg1400<path d="M 10.3 20.8 L 30.9 40.2" />1401```14021403**Automate with SVGO:**14041405```bash1406npx svgo --precision=1 --multipass icon.svg1407```14081409### 6.5 Prevent Hydration Mismatch Without Flickering14101411**Impact: MEDIUM (avoids visual flicker and hydration errors)**14121413When 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.14141415**Incorrect: breaks SSR**14161417```tsx1418function ThemeWrapper({ children }: { children: ReactNode }) {1419 // localStorage is not available on server - throws error1420 const theme = localStorage.getItem('theme') || 'light'14211422 return (1423 <div className={theme}>1424 {children}1425 </div>1426 )1427}1428```14291430Server-side rendering will fail because `localStorage` is undefined.14311432**Incorrect: visual flickering**14331434```tsx1435function ThemeWrapper({ children }: { children: ReactNode }) {1436 const [theme, setTheme] = useState('light')14371438 useEffect(() => {1439 // Runs after hydration - causes visible flash1440 const stored = localStorage.getItem('theme')1441 if (stored) {1442 setTheme(stored)1443 }1444 }, [])14451446 return (1447 <div className={theme}>1448 {children}1449 </div>1450 )1451}1452```14531454Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.14551456**Correct: no flicker, no hydration mismatch**14571458```tsx1459function ThemeWrapper({ children }: { children: ReactNode }) {1460 return (1461 <>1462 <div id="theme-wrapper">1463 {children}1464 </div>1465 <script1466 dangerouslySetInnerHTML={{1467 __html: `1468 (function() {1469 try {1470 var theme = localStorage.getItem('theme') || 'light';1471 var el = document.getElementById('theme-wrapper');1472 if (el) el.className = theme;1473 } catch (e) {}1474 })();1475 `,1476 }}1477 />1478 </>1479 )1480}1481```14821483The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.14841485This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.14861487### 6.6 Use Activity Component for Show/Hide14881489**Impact: MEDIUM (preserves state/DOM)**14901491Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.14921493**Usage:**14941495```tsx1496import { Activity } from 'react'14971498function Dropdown({ isOpen }: Props) {1499 return (1500 <Activity mode={isOpen ? 'visible' : 'hidden'}>1501 <ExpensiveMenu />1502 </Activity>1503 )1504}1505```15061507Avoids expensive re-renders and state loss.15081509### 6.7 Use Explicit Conditional Rendering15101511**Impact: LOW (prevents rendering 0 or NaN)**15121513Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.15141515**Incorrect: renders "0" when count is 0**15161517```tsx1518function Badge({ count }: { count: number }) {1519 return (1520 <div>1521 {count && <span className="badge">{count}</span>}1522 </div>1523 )1524}15251526// When count = 0, renders: <div>0</div>1527// When count = 5, renders: <div><span class="badge">5</span></div>1528```15291530**Correct: renders nothing when count is 0**15311532```tsx1533function Badge({ count }: { count: number }) {1534 return (1535 <div>1536 {count > 0 ? <span className="badge">{count}</span> : null}1537 </div>1538 )1539}15401541// When count = 0, renders: <div></div>1542// When count = 5, renders: <div><span class="badge">5</span></div>1543```15441545---15461547## 7. JavaScript Performance15481549**Impact: LOW-MEDIUM**15501551Micro-optimizations for hot paths can add up to meaningful improvements.15521553### 7.1 Batch DOM CSS Changes15541555**Impact: MEDIUM (reduces reflows/repaints)**15561557Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.15581559**Incorrect: multiple reflows**15601561```typescript1562function updateElementStyles(element: HTMLElement) {1563 // Each line triggers a reflow1564 element.style.width = '100px'1565 element.style.height = '200px'1566 element.style.backgroundColor = 'blue'1567 element.style.border = '1px solid black'1568}1569```15701571**Correct: add class - single reflow**15721573```typescript1574// CSS file1575.highlighted-box {1576 width: 100px;1577 height: 200px;1578 background-color: blue;1579 border: 1px solid black;1580}15811582// JavaScript1583function updateElementStyles(element: HTMLElement) {1584 element.classList.add('highlighted-box')1585}1586```15871588**Correct: change cssText - single reflow**15891590```typescript1591function updateElementStyles(element: HTMLElement) {1592 element.style.cssText = `1593 width: 100px;1594 height: 200px;1595 background-color: blue;1596 border: 1px solid black;1597 `1598}1599```16001601**React example:**16021603```tsx1604// Incorrect: changing styles one by one1605function Box({ isHighlighted }: { isHighlighted: boolean }) {1606 const ref = useRef<HTMLDivElement>(null)16071608 useEffect(() => {1609 if (ref.current && isHighlighted) {1610 ref.current.style.width = '100px'1611 ref.current.style.height = '200px'1612 ref.current.style.backgroundColor = 'blue'1613 }1614 }, [isHighlighted])16151616 return <div ref={ref}>Content</div>1617}16181619// Correct: toggle class1620function Box({ isHighlighted }: { isHighlighted: boolean }) {1621 return (1622 <div className={isHighlighted ? 'highlighted-box' : ''}>1623 Content1624 </div>1625 )1626}1627```16281629Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.16301631### 7.2 Build Index Maps for Repeated Lookups16321633**Impact: LOW-MEDIUM (1M ops to 2K ops)**16341635Multiple `.find()` calls by the same key should use a Map.16361637**Incorrect (O(n) per lookup):**16381639```typescript1640function processOrders(orders: Order[], users: User[]) {1641 return orders.map(order => ({1642 ...order,1643 user: users.find(u => u.id === order.userId)1644 }))1645}1646```16471648**Correct (O(1) per lookup):**16491650```typescript1651function processOrders(orders: Order[], users: User[]) {1652 const userById = new Map(users.map(u => [u.id, u]))16531654 return orders.map(order => ({1655 ...order,1656 user: userById.get(order.userId)1657 }))1658}1659```16601661Build map once (O(n)), then all lookups are O(1).16621663For 1000 orders × 1000 users: 1M ops → 2K ops.16641665### 7.3 Cache Property Access in Loops16661667**Impact: LOW-MEDIUM (reduces lookups)**16681669Cache object property lookups in hot paths.16701671**Incorrect: 3 lookups × N iterations**16721673```typescript1674for (let i = 0; i < arr.length; i++) {1675 process(obj.config.settings.value)1676}1677```16781679**Correct: 1 lookup total**16801681```typescript1682const value = obj.config.settings.value1683const len = arr.length1684for (let i = 0; i < len; i++) {1685 process(value)1686}1687```16881689### 7.4 Cache Repeated Function Calls16901691**Impact: MEDIUM (avoid redundant computation)**16921693Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.16941695**Incorrect: redundant computation**16961697```typescript1698function ProjectList({ projects }: { projects: Project[] }) {1699 return (1700 <div>1701 {projects.map(project => {1702 // slugify() called 100+ times for same project names1703 const slug = slugify(project.name)17041705 return <ProjectCard key={project.id} slug={slug} />1706 })}1707 </div>1708 )1709}1710```17111712**Correct: cached results**17131714```typescript1715// Module-level cache1716const slugifyCache = new Map<string, string>()17171718function cachedSlugify(text: string): string {1719 if (slugifyCache.has(text)) {1720 return slugifyCache.get(text)!1721 }1722 const result = slugify(text)1723 slugifyCache.set(text, result)1724 return result1725}17261727function ProjectList({ projects }: { projects: Project[] }) {1728 return (1729 <div>1730 {projects.map(project => {1731 // Computed only once per unique project name1732 const slug = cachedSlugify(project.name)17331734 return <ProjectCard key={project.id} slug={slug} />1735 })}1736 </div>1737 )1738}1739```17401741**Simpler pattern for single-value functions:**17421743```typescript1744let isLoggedInCache: boolean | null = null17451746function isLoggedIn(): boolean {1747 if (isLoggedInCache !== null) {1748 return isLoggedInCache1749 }17501751 isLoggedInCache = document.cookie.includes('auth=')1752 return isLoggedInCache1753}17541755// Clear cache when auth changes1756function onAuthChange() {1757 isLoggedInCache = null1758}1759```17601761Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.17621763Reference: [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)17641765### 7.5 Cache Storage API Calls17661767**Impact: LOW-MEDIUM (reduces expensive I/O)**17681769`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.17701771**Incorrect: reads storage on every call**17721773```typescript1774function getTheme() {1775 return localStorage.getItem('theme') ?? 'light'1776}1777// Called 10 times = 10 storage reads1778```17791780**Correct: Map cache**17811782```typescript1783const storageCache = new Map<string, string | null>()17841785function getLocalStorage(key: string) {1786 if (!storageCache.has(key)) {1787 storageCache.set(key, localStorage.getItem(key))1788 }1789 return storageCache.get(key)1790}17911792function setLocalStorage(key: string, value: string) {1793 localStorage.setItem(key, value)1794 storageCache.set(key, value) // keep cache in sync1795}1796```17971798Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.17991800**Cookie caching:**18011802```typescript1803let cookieCache: Record<string, string> | null = null18041805function getCookie(name: string) {1806 if (!cookieCache) {1807 cookieCache = Object.fromEntries(1808 document.cookie.split('; ').map(c => c.split('='))1809 )1810 }1811 return cookieCache[name]1812}1813```18141815**Important: invalidate on external changes**18161817```typescript1818window.addEventListener('storage', (e) => {1819 if (e.key) storageCache.delete(e.key)1820})18211822document.addEventListener('visibilitychange', () => {1823 if (document.visibilityState === 'visible') {1824 storageCache.clear()1825 }1826})1827```18281829If storage can change externally (another tab, server-set cookies), invalidate cache:18301831### 7.6 Combine Multiple Array Iterations18321833**Impact: LOW-MEDIUM (reduces iterations)**18341835Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.18361837**Incorrect: 3 iterations**18381839```typescript1840const admins = users.filter(u => u.isAdmin)1841const testers = users.filter(u => u.isTester)1842const inactive = users.filter(u => !u.isActive)1843```18441845**Correct: 1 iteration**18461847```typescript1848const admins: User[] = []1849const testers: User[] = []1850const inactive: User[] = []18511852for (const user of users) {1853 if (user.isAdmin) admins.push(user)1854 if (user.isTester) testers.push(user)1855 if (!user.isActive) inactive.push(user)1856}1857```18581859### 7.7 Early Length Check for Array Comparisons18601861**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**18621863When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.18641865In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).18661867**Incorrect: always runs expensive comparison**18681869```typescript1870function hasChanges(current: string[], original: string[]) {1871 // Always sorts and joins, even when lengths differ1872 return current.sort().join() !== original.sort().join()1873}1874```18751876Two 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.18771878**Correct (O(1) length check first):**18791880```typescript1881function hasChanges(current: string[], original: string[]) {1882 // Early return if lengths differ1883 if (current.length !== original.length) {1884 return true1885 }1886 // Only sort/join when lengths match1887 const currentSorted = current.toSorted()1888 const originalSorted = original.toSorted()1889 for (let i = 0; i < currentSorted.length; i++) {1890 if (currentSorted[i] !== originalSorted[i]) {1891 return true1892 }1893 }1894 return false1895}1896```18971898This new approach is more efficient because:18991900- It avoids the overhead of sorting and joining the arrays when lengths differ19011902- It avoids consuming memory for the joined strings (especially important for large arrays)19031904- It avoids mutating the original arrays19051906- It returns early when a difference is found19071908### 7.8 Early Return from Functions19091910**Impact: LOW-MEDIUM (avoids unnecessary computation)**19111912Return early when result is determined to skip unnecessary processing.19131914**Incorrect: processes all items even after finding answer**19151916```typescript1917function validateUsers(users: User[]) {1918 let hasError = false1919 let errorMessage = ''19201921 for (const user of users) {1922 if (!user.email) {1923 hasError = true1924 errorMessage = 'Email required'1925 }1926 if (!user.name) {1927 hasError = true1928 errorMessage = 'Name required'1929 }1930 // Continues checking all users even after error found1931 }19321933 return hasError ? { valid: false, error: errorMessage } : { valid: true }1934}1935```19361937**Correct: returns immediately on first error**19381939```typescript1940function validateUsers(users: User[]) {1941 for (const user of users) {1942 if (!user.email) {1943 return { valid: false, error: 'Email required' }1944 }1945 if (!user.name) {1946 return { valid: false, error: 'Name required' }1947 }1948 }19491950 return { valid: true }1951}1952```19531954### 7.9 Hoist RegExp Creation19551956**Impact: LOW-MEDIUM (avoids recreation)**19571958Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.19591960**Incorrect: new RegExp every render**19611962```tsx1963function Highlighter({ text, query }: Props) {1964 const regex = new RegExp(`(${query})`, 'gi')1965 const parts = text.split(regex)1966 return <>{parts.map((part, i) => ...)}</>1967}1968```19691970**Correct: memoize or hoist**19711972```tsx1973const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/19741975function Highlighter({ text, query }: Props) {1976 const regex = useMemo(1977 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),1978 [query]1979 )1980 const parts = text.split(regex)1981 return <>{parts.map((part, i) => ...)}</>1982}1983```19841985**Warning: global regex has mutable state**19861987```typescript1988const regex = /foo/g1989regex.test('foo') // true, lastIndex = 31990regex.test('foo') // false, lastIndex = 01991```19921993Global regex (`/g`) has mutable `lastIndex` state:19941995### 7.10 Use Loop for Min/Max Instead of Sort19961997**Impact: LOW (O(n) instead of O(n log n))**19981999Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.20002001**Incorrect (O(n log n) - sort to find latest):**20022003```typescript2004interface Project {2005 id: string2006 name: string2007 updatedAt: number2008}20092010function getLatestProject(projects: Project[]) {2011 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)2012 return sorted[0]2013}2014```20152016Sorts the entire array just to find the maximum value.20172018**Incorrect (O(n log n) - sort for oldest and newest):**20192020```typescript2021function getOldestAndNewest(projects: Project[]) {2022 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)2023 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }2024}2025```20262027Still sorts unnecessarily when only min/max are needed.20282029**Correct (O(n) - single loop):**20302031```typescript2032function getLatestProject(projects: Project[]) {2033 if (projects.length === 0) return null20342035 let latest = projects[0]20362037 for (let i = 1; i < projects.length; i++) {2038 if (projects[i].updatedAt > latest.updatedAt) {2039 latest = projects[i]2040 }2041 }20422043 return latest2044}20452046function getOldestAndNewest(projects: Project[]) {2047 if (projects.length === 0) return { oldest: null, newest: null }20482049 let oldest = projects[0]2050 let newest = projects[0]20512052 for (let i = 1; i < projects.length; i++) {2053 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]2054 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]2055 }20562057 return { oldest, newest }2058}2059```20602061Single pass through the array, no copying, no sorting.20622063**Alternative: Math.min/Math.max for small arrays**20642065```typescript2066const numbers = [5, 2, 8, 1, 9]2067const min = Math.min(...numbers)2068const max = Math.max(...numbers)2069```20702071This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.20722073### 7.11 Use Set/Map for O(1) Lookups20742075**Impact: LOW-MEDIUM (O(n) to O(1))**20762077Convert arrays to Set/Map for repeated membership checks.20782079**Incorrect (O(n) per check):**20802081```typescript2082const allowedIds = ['a', 'b', 'c', ...]2083items.filter(item => allowedIds.includes(item.id))2084```20852086**Correct (O(1) per check):**20872088```typescript2089const allowedIds = new Set(['a', 'b', 'c', ...])2090items.filter(item => allowedIds.has(item.id))2091```20922093### 7.12 Use toSorted() Instead of sort() for Immutability20942095**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**20962097`.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.20982099**Incorrect: mutates original array**21002101```typescript2102function UserList({ users }: { users: User[] }) {2103 // Mutates the users prop array!2104 const sorted = useMemo(2105 () => users.sort((a, b) => a.name.localeCompare(b.name)),2106 [users]2107 )2108 return <div>{sorted.map(renderUser)}</div>2109}2110```21112112**Correct: creates new array**21132114```typescript2115function UserList({ users }: { users: User[] }) {2116 // Creates new sorted array, original unchanged2117 const sorted = useMemo(2118 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),2119 [users]2120 )2121 return <div>{sorted.map(renderUser)}</div>2122}2123```21242125**Why this matters in React:**212621271. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only212821292. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior21302131**Browser support: fallback for older browsers**21322133```typescript2134// Fallback for older browsers2135const sorted = [...items].sort((a, b) => a.value - b.value)2136```21372138`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:21392140**Other immutable array methods:**21412142- `.toSorted()` - immutable sort21432144- `.toReversed()` - immutable reverse21452146- `.toSpliced()` - immutable splice21472148- `.with()` - immutable element replacement21492150---21512152## 8. Advanced Patterns21532154**Impact: LOW**21552156Advanced patterns for specific cases that require careful implementation.21572158### 8.1 Store Event Handlers in Refs21592160**Impact: LOW (stable subscriptions)**21612162Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.21632164**Incorrect: re-subscribes on every render**21652166```tsx2167function useWindowEvent(event: string, handler: () => void) {2168 useEffect(() => {2169 window.addEventListener(event, handler)2170 return () => window.removeEventListener(event, handler)2171 }, [event, handler])2172}2173```21742175**Correct: stable subscription**21762177```tsx2178import { useEffectEvent } from 'react'21792180function useWindowEvent(event: string, handler: () => void) {2181 const onEvent = useEffectEvent(handler)21822183 useEffect(() => {2184 window.addEventListener(event, onEvent)2185 return () => window.removeEventListener(event, onEvent)2186 }, [event])2187}2188```21892190**Alternative: use `useEffectEvent` if you're on latest React:**21912192`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.21932194### 8.2 useLatest for Stable Callback Refs21952196**Impact: LOW (prevents effect re-runs)**21972198Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.21992200**Implementation:**22012202```typescript2203function useLatest<T>(value: T) {2204 const ref = useRef(value)2205 useEffect(() => {2206 ref.current = value2207 }, [value])2208 return ref2209}2210```22112212**Incorrect: effect re-runs on every callback change**22132214```tsx2215function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2216 const [query, setQuery] = useState('')22172218 useEffect(() => {2219 const timeout = setTimeout(() => onSearch(query), 300)2220 return () => clearTimeout(timeout)2221 }, [query, onSearch])2222}2223```22242225**Correct: stable effect, fresh callback**22262227```tsx2228function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {2229 const [query, setQuery] = useState('')2230 const onSearchRef = useLatest(onSearch)22312232 useEffect(() => {2233 const timeout = setTimeout(() => onSearchRef.current(query), 300)2234 return () => clearTimeout(timeout)2235 }, [query])2236}2237```22382239---22402241## References224222431. [https://react.dev](https://react.dev)22442. [https://nextjs.org](https://nextjs.org)22453. [https://swr.vercel.app](https://swr.vercel.app)22464. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)22475. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)22486. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)22497. [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)2250
Also in sickn33/agentic-awesome-skills
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 |
|---|---|---|---|---|---|
| sickn33/agentic-awesome-skillsAGENTS.md · 44k | AGENTS.md | buildteststylearch+3 | 90/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-golang/AGENTS.md · 44k | AGENTS.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-golang/CLAUDE.md · 44k | CLAUDE.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-python/AGENTS.md · 44k | AGENTS.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-python/CLAUDE.md · 44k | CLAUDE.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-typescript/AGENTS.md · 44k | AGENTS.md | archtypes | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/dbos-typescript/CLAUDE.md · 44k | CLAUDE.md | archtypes | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills-claude/skills/loki-mode/CLAUDE.md · 44k | CLAUDE.md | testlint-formatstylearch+5 | 77/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-golang/AGENTS.md · 44k | AGENTS.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-golang/CLAUDE.md · 44k | CLAUDE.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-python/AGENTS.md · 44k | AGENTS.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-python/CLAUDE.md · 44k | CLAUDE.md | arch | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-typescript/AGENTS.md · 44k | AGENTS.md | archtypes | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/dbos-typescript/CLAUDE.md · 44k | CLAUDE.md | archtypes | 54/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/postgres-best-practices/AGENTS.md · 44k | AGENTS.md | styletypessecuritydatabase+3 | 45/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-awesome-skills/skills/react-best-practices/AGENTS.md · 44k | AGENTS.md | buildlint-formatstyledependencies+4 | 61/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-bundle-aas-data-analytics/skills/postgres-best-practices/AGENTS.md · 44k | AGENTS.md | styletypessecuritydatabase+3 | 45/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-bundle-aas-data-engineering-platform/skills/postgres-best-practices/AGENTS.md · 44k | AGENTS.md | styletypessecuritydatabase+3 | 45/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-bundle-aas-web-app-builder/skills/react-best-practices/AGENTS.md · 44k | AGENTS.md | buildlint-formatstyledependencies+4 | 61/100 | 3 days ago | |
| sickn33/agentic-awesome-skillsplugins/agentic-bundle-data-analytics/skills/postgres-best-practices/AGENTS.md · 44k | AGENTS.md | styletypessecuritydatabase+3 | 45/100 | 3 days ago |
Diff against AGENTS.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-golang/AGENTS.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-golang/CLAUDE.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-python/AGENTS.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-python/CLAUDE.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-typescript/AGENTS.md Diff against plugins/agentic-awesome-skills-claude/skills/dbos-typescript/CLAUDE.md Diff against plugins/agentic-awesome-skills-claude/skills/loki-mode/CLAUDE.md Diff against plugins/agentic-awesome-skills/skills/dbos-golang/AGENTS.md Diff against plugins/agentic-awesome-skills/skills/dbos-golang/CLAUDE.md Diff against plugins/agentic-awesome-skills/skills/dbos-python/AGENTS.md Diff against plugins/agentic-awesome-skills/skills/dbos-python/CLAUDE.md Diff against plugins/agentic-awesome-skills/skills/dbos-typescript/AGENTS.md Diff against plugins/agentic-awesome-skills/skills/dbos-typescript/CLAUDE.md Diff against plugins/agentic-awesome-skills/skills/postgres-best-practices/AGENTS.md Diff against plugins/agentic-awesome-skills/skills/react-best-practices/AGENTS.md Diff against plugins/agentic-bundle-aas-data-analytics/skills/postgres-best-practices/AGENTS.md Diff against plugins/agentic-bundle-aas-data-engineering-platform/skills/postgres-best-practices/AGENTS.md Diff against plugins/agentic-bundle-aas-web-app-builder/skills/react-best-practices/AGENTS.md Diff against plugins/agentic-bundle-data-analytics/skills/postgres-best-practices/AGENTS.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| OnlyTerp/prompt-cache-skillsAGENTS.md · 112 | AGENTS.md | setupbuildtestlint-format+5 | 100/100 | 3 days ago | |
| vllm-project/vllmAGENTS.md · 88k | AGENTS.md | setuptestlint-formatstyle+5 | 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 | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago |
