diff --git a/.claude/skills/react-best-practices/SKILL.md b/.claude/skills/react-best-practices/SKILL.md new file mode 100644 index 00000000000..afb9aa36089 --- /dev/null +++ b/.claude/skills/react-best-practices/SKILL.md @@ -0,0 +1,218 @@ +--- +name: react-best-practices +description: Comprehensive React and Next.js performance optimization guide with 40+ rules for eliminating waterfalls, optimizing bundles, and improving rendering. Use when optimizing React apps, reviewing performance, or refactoring components. +version: 1.0.0 +author: Vercel Engineering +license: MIT +tags: [React, Next.js, Performance, Optimization, Best Practices, Bundle Size, Rendering, Server Components] +dependencies: [] +--- + +# React Best Practices - Performance Optimization + +Comprehensive performance optimization guide for React and Next.js applications with 40+ rules organized by impact level. Designed to help developers eliminate performance bottlenecks and follow best practices. + +## When to use this skill + +**Use React Best Practices when:** +- Optimizing React or Next.js application performance +- Reviewing code for performance improvements +- Refactoring existing components for better performance +- Implementing new features with performance in mind +- Debugging slow rendering or loading issues +- Reducing bundle size +- Eliminating request waterfalls + +**Key areas covered:** +- **Eliminating Waterfalls** (CRITICAL): Prevent sequential async operations +- **Bundle Size Optimization** (CRITICAL): Reduce initial JavaScript payload +- **Server-Side Performance** (HIGH): Optimize RSC and data fetching +- **Client-Side Data Fetching** (MEDIUM-HIGH): Implement efficient caching +- **Re-render Optimization** (MEDIUM): Minimize unnecessary re-renders +- **Rendering Performance** (MEDIUM): Optimize browser rendering +- **JavaScript Performance** (LOW-MEDIUM): Micro-optimizations for hot paths +- **Advanced Patterns** (LOW): Specialized techniques for edge cases + +## Quick reference + +### Critical priorities + +1. **Defer await until needed** - Move awaits into branches where they're used +2. **Use Promise.all()** - Parallelize independent async operations +3. **Avoid barrel imports** - Import directly from source files +4. **Dynamic imports** - Lazy-load heavy components +5. **Strategic Suspense** - Stream content while showing layout + +### Common patterns + +**Parallel data fetching:** +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +**Direct imports:** +```tsx +// ❌ Loads entire library +import { Check } from 'lucide-react' + +// ✅ Loads only what you need +import Check from 'lucide-react/dist/esm/icons/check' +``` + +**Dynamic components:** +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor'), + { ssr: false } +) +``` + +## Using the guidelines + +The complete performance guidelines are available in the references folder: + +- **react-performance-guidelines.md**: Complete guide with all 40+ rules, code examples, and impact analysis + +Each rule includes: +- Incorrect/correct code comparisons +- Specific impact metrics +- When to apply the optimization +- Real-world examples + +## Categories overview + +### 1. Eliminating Waterfalls (CRITICAL) +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. +- Defer await until needed +- Dependency-based parallelization +- Prevent waterfall chains in API routes +- Promise.all() for independent operations +- Strategic Suspense boundaries + +### 2. Bundle Size Optimization (CRITICAL) +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. +- Avoid barrel file imports +- Conditional module loading +- Defer non-critical third-party libraries +- Dynamic imports for heavy components +- Preload based on user intent + +### 3. Server-Side Performance (HIGH) +Optimize server-side rendering and data fetching. +- Cross-request LRU caching +- Minimize serialization at RSC boundaries +- Parallel data fetching with component composition +- Per-request deduplication with React.cache() + +### 4. Client-Side Data Fetching (MEDIUM-HIGH) +Automatic deduplication and efficient data fetching patterns. +- Deduplicate global event listeners +- Use SWR for automatic deduplication + +### 5. Re-render Optimization (MEDIUM) +Reduce unnecessary re-renders to minimize wasted computation. +- Defer state reads to usage point +- Extract to memoized components +- Narrow effect dependencies +- Subscribe to derived state +- Use lazy state initialization +- Use transitions for non-urgent updates + +### 6. Rendering Performance (MEDIUM) +Optimize the browser rendering process. +- Animate SVG wrapper instead of SVG element +- CSS content-visibility for long lists +- Hoist static JSX elements +- Optimize SVG precision +- Prevent hydration mismatch without flickering +- Use Activity component for show/hide +- Use explicit conditional rendering + +### 7. JavaScript Performance (LOW-MEDIUM) +Micro-optimizations for hot paths. +- Batch DOM CSS changes +- Build index maps for repeated lookups +- Cache property access in loops +- Cache repeated function calls +- Cache storage API calls +- Combine multiple array iterations +- Early length check for array comparisons +- Early return from functions +- Hoist RegExp creation +- Use loop for min/max instead of sort +- Use Set/Map for O(1) lookups +- Use toSorted() instead of sort() + +### 8. Advanced Patterns (LOW) +Specialized techniques for edge cases. +- Store event handlers in refs +- useLatest for stable callback refs + +## Implementation approach + +When optimizing a React application: + +1. **Profile first**: Use React DevTools Profiler and browser performance tools to identify bottlenecks +2. **Focus on critical paths**: Start with eliminating waterfalls and reducing bundle size +3. **Measure impact**: Verify improvements with metrics (LCP, TTI, FID) +4. **Apply incrementally**: Don't over-optimize prematurely +5. **Test thoroughly**: Ensure optimizations don't break functionality + +## Key metrics to track + +- **Time to Interactive (TTI)**: When page becomes fully interactive +- **Largest Contentful Paint (LCP)**: When main content is visible +- **First Input Delay (FID)**: Responsiveness to user interactions +- **Cumulative Layout Shift (CLS)**: Visual stability +- **Bundle size**: Initial JavaScript payload +- **Server response time**: TTFB for server-rendered content + +## Common pitfalls to avoid + +❌ **Don't:** +- Use barrel imports from large libraries +- Block parallel operations with sequential awaits +- Re-render entire trees when only part needs updating +- Load analytics/tracking in the critical path +- Mutate arrays with .sort() instead of .toSorted() +- Create RegExp or heavy objects inside render + +✅ **Do:** +- Import directly from source files +- Use Promise.all() for independent operations +- Memoize expensive components +- Lazy-load non-critical code +- Use immutable array methods +- Hoist static objects outside components + +## Project-specific style preferences + +The reference guidelines use `function` keyword syntax for examples, but this project prefers: + +1. **Arrow functions for components** - Use `const Component = memo(() => ...)` not `memo(function Component() ...)` +2. **useMemo over IIFE in TSX** - Use `useMemo(() => ..., [deps])` not `(() => { ... })()` +3. **Implicit returns when possible** - Use `useMemo(() => value, [deps])` not `useMemo(() => { return value }, [deps])` +4. **Avoid nested ternaries** - Use useMemo with early returns or IIFE with if/else blocks for complex conditionals + +## Resources + +- [React Documentation](https://react.dev) +- [Next.js Documentation](https://nextjs.org) +- [SWR Documentation](https://swr.vercel.app) +- [Vercel Bundle Optimization](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) +- [Vercel Dashboard Performance](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast) +- [better-all Library](https://github.com/shuding/better-all) +- [node-lru-cache](https://github.com/isaacs/node-lru-cache) + +## Version history + +**v0.1.0** (January 2026) +- Initial release from Vercel Engineering +- 40+ performance rules across 8 categories +- Comprehensive code examples and impact analysis diff --git a/.claude/skills/react-best-practices/references/react-performance-guidelines.md b/.claude/skills/react-best-practices/references/react-performance-guidelines.md new file mode 100644 index 00000000000..e6b29a8ea0c --- /dev/null +++ b/.claude/skills/react-best-practices/references/react-performance-guidelines.md @@ -0,0 +1,1865 @@ +# React Best Practices - Complete Document + +**Version 0.1.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. + +--- + +## 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) + - 1.2 [Dependency-Based Parallelization](#12) + - 1.3 [Prevent Waterfall Chains in API Routes](#13) + - 1.4 [Promise.all() for Independent Operations](#14) + - 1.5 [Strategic Suspense Boundaries](#15) +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21) + - 2.2 [Conditional Module Loading](#22) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23) + - 2.4 [Dynamic Imports for Heavy Components](#24) + - 2.5 [Preload Based on User Intent](#25) +3. [Server-Side Performance](#3-server-side-performance) — **HIGH** + - 3.1 [Cross-Request LRU Caching](#31) + - 3.2 [Minimize Serialization at RSC Boundaries](#32) + - 3.3 [Parallel Data Fetching with Component Composition](#33) + - 3.4 [Per-Request Deduplication with React.cache()](#34) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41) + - 4.2 [Use SWR for Automatic Deduplication](#42) +5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51) + - 5.2 [Extract to Memoized Components](#52) + - 5.3 [Narrow Effect Dependencies](#53) + - 5.4 [Subscribe to Derived State](#54) + - 5.5 [Use Lazy State Initialization](#55) + - 5.6 [Use Transitions for Non-Urgent Updates](#56) +6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61) + - 6.2 [CSS content-visibility for Long Lists](#62) + - 6.3 [Hoist Static JSX Elements](#63) + - 6.4 [Optimize SVG Precision](#64) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65) + - 6.6 [Use Activity Component for Show/Hide](#66) + - 6.7 [Use Explicit Conditional Rendering](#67) +7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** + - 7.1 [Batch DOM CSS Changes](#71) + - 7.2 [Build Index Maps for Repeated Lookups](#72) + - 7.3 [Cache Property Access in Loops](#73) + - 7.4 [Cache Repeated Function Calls](#74) + - 7.5 [Cache Storage API Calls](#75) + - 7.6 [Combine Multiple Array Iterations](#76) + - 7.7 [Early Length Check for Array Comparisons](#77) + - 7.8 [Early Return from Functions](#78) + - 7.9 [Hoist RegExp Creation](#79) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710) + - 7.11 [Use Set/Map for O(1) Lookups](#711) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712) +8. [Advanced Patterns](#8-advanced-patterns) — **LOW** + - 8.1 [Store Event Handlers in Refs](#81) + - 8.2 [useLatest for Stable Callback Refs](#82) + +--- + +## 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 + +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 + +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 + +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 + +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 + +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. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +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. + +**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 + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ enabled }: { enabled: boolean }) { + 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]) + + 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 + +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 + +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 + +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 + +`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. In serverless, 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 + +The React Server/Client boundary serializes all object properties. 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 + +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 Layout({ children }: { children: ReactNode }) { + const header = await fetchHeader() + return ( +
+
{header}
+ {children} +
+ ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( + + + + ) +} +``` + +### 3.4 Per-Request Deduplication with React.cache() + +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. + +--- + +## 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 + +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 SWR for 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) + +--- + +## 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 + +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 + +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 ( +
+ +
+ ) +} +``` + +### 5.3 Narrow Effect Dependencies + +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 + +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