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