diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad3f086 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# IDE and editor settings +.vscode/ +.idea/ + +# Kiro settings (user-specific) +.kiro/ + +# OS files +.DS_Store +Thumbs.db + +# Node modules (if any) +node_modules/ diff --git a/README.md b/README.md index 7f3ee3b..91fdbd1 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,28 @@ Documentation is available at https://kiro.dev/docs/powers/ --- +## Vercel Powers + +Powers ported from [Vercel Agent Skills](https://github.com/vercel-labs/agent-skills). + +### vercel/react-best-practices +**React Best Practices** - React and Next.js performance optimization guidelines from Vercel Engineering. Contains 45+ rules across 8 categories, prioritized by impact to guide code review, refactoring, and optimization. + +**MCP Servers:** None (Knowledge Base Power) + +**Categories:** Eliminating Waterfalls (CRITICAL), Bundle Size Optimization (CRITICAL), Server-Side Performance (HIGH), Client-Side Data Fetching (MEDIUM-HIGH), Re-render Optimization (MEDIUM), Rendering Performance (MEDIUM), JavaScript Performance (LOW-MEDIUM), Advanced Patterns (LOW) + +--- + +### vercel/web-design-guidelines +**Web Design Guidelines** - Review UI code for compliance with web interface best practices. Audits code for 100+ rules covering accessibility, performance, UX, forms, animations, typography, images, navigation, dark mode, touch interactions, and internationalization. + +**MCP Servers:** None (Knowledge Base Power) + +**Categories:** Accessibility, Focus States, Forms, Animation, Typography, Images, Performance, Navigation & State, Dark Mode & Theming, Touch & Interaction, Locale & i18n + +--- + ## Security diff --git a/vercel/react-best-practices/POWER.md b/vercel/react-best-practices/POWER.md new file mode 100644 index 0000000..41d296e --- /dev/null +++ b/vercel/react-best-practices/POWER.md @@ -0,0 +1,191 @@ +--- +name: "react-best-practices" +displayName: "React Best Practices" +description: "React and Next.js performance optimization guidelines from Vercel Engineering. Contains 45+ rules across 8 categories, prioritized by impact to guide code review, refactoring, and optimization." +keywords: ["react", "nextjs", "performance", "optimization", "vercel", "best-practices"] +author: "Vercel Engineering" +--- + +# React Best Practices + +Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel Engineering. Contains 45+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). + +## Overview + +This power provides detailed performance optimization guidelines for React and Next.js development. Each rule includes: +- Clear explanation of why it matters +- Incorrect code example with explanation +- Correct code example with explanation +- Impact metrics and priority levels +- Real-world context and references + +## When to Use + +Reference these guidelines when: +- Writing new React components or Next.js pages +- Implementing data fetching (client or server-side) +- Reviewing code for performance issues +- Refactoring existing React/Next.js code +- Optimizing bundle size or load times +- Conducting code reviews + +## Rule Categories by Priority + +| Priority | Category | Impact | Rules | +|----------|----------|--------|-------| +| 1 | Eliminating Waterfalls | CRITICAL | 5 rules | +| 2 | Bundle Size Optimization | CRITICAL | 5 rules | +| 3 | Server-Side Performance | HIGH | 5 rules | +| 4 | Client-Side Data Fetching | MEDIUM-HIGH | 2 rules | +| 5 | Re-render Optimization | MEDIUM | 7 rules | +| 6 | Rendering Performance | MEDIUM | 7 rules | +| 7 | JavaScript Performance | LOW-MEDIUM | 12 rules | +| 8 | Advanced Patterns | LOW | 2 rules | + +## Quick Reference + +### 1. Eliminating Waterfalls (CRITICAL) + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains (2-10× improvement). + +- **async-defer-await** - Move await into branches where actually used +- **async-parallel** - Use Promise.all() for independent operations +- **async-dependencies** - Use better-all for partial dependencies +- **async-api-routes** - Start promises early, await late in API routes +- **async-suspense-boundaries** - Use Suspense to stream content + +### 2. Bundle Size Optimization (CRITICAL) + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint (200-800ms savings). + +- **bundle-barrel-imports** - Import directly, avoid barrel files +- **bundle-dynamic-imports** - Use next/dynamic for heavy components +- **bundle-defer-third-party** - Load analytics/logging after hydration +- **bundle-conditional** - Load modules only when feature is activated +- **bundle-preload** - Preload on hover/focus for perceived speed + +### 3. Server-Side Performance (HIGH) + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +- **server-cache-react** - Use React.cache() for per-request deduplication +- **server-cache-lru** - Use LRU cache for cross-request caching +- **server-serialization** - Minimize data passed to client components +- **server-parallel-fetching** - Restructure components to parallelize fetches +- **server-after-nonblocking** - Use after() for non-blocking operations + +### 4. Client-Side Data Fetching (MEDIUM-HIGH) + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +- **client-swr-dedup** - Use SWR for automatic request deduplication +- **client-event-listeners** - Deduplicate global event listeners + +### 5. Re-render Optimization (MEDIUM) + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +- **rerender-defer-reads** - Don't subscribe to state only used in callbacks +- **rerender-memo** - Extract expensive work into memoized components +- **rerender-dependencies** - Use primitive dependencies in effects +- **rerender-derived-state** - Subscribe to derived booleans, not raw values +- **rerender-functional-setstate** - Use functional setState for stable callbacks +- **rerender-lazy-state-init** - Pass function to useState for expensive values +- **rerender-transitions** - Use startTransition for non-urgent updates + +### 6. Rendering Performance (MEDIUM) + +Optimizing the rendering process reduces the work the browser needs to do. + +- **rendering-animate-svg-wrapper** - Animate div wrapper, not SVG element +- **rendering-content-visibility** - Use content-visibility for long lists +- **rendering-hoist-jsx** - Extract static JSX outside components +- **rendering-svg-precision** - Reduce SVG coordinate precision +- **rendering-hydration-no-flicker** - Use inline script for client-only data +- **rendering-activity** - Use Activity component for show/hide +- **rendering-conditional-render** - Use ternary, not && for conditionals + +### 7. JavaScript Performance (LOW-MEDIUM) + +Micro-optimizations for hot paths can add up to meaningful improvements. + +- **js-batch-dom-css** - Group CSS changes via classes or cssText +- **js-index-maps** - Build Map for repeated lookups +- **js-cache-property-access** - Cache object properties in loops +- **js-cache-function-results** - Cache function results in module-level Map +- **js-cache-storage** - Cache localStorage/sessionStorage reads +- **js-combine-iterations** - Combine multiple filter/map into one loop +- **js-length-check-first** - Check array length before expensive comparison +- **js-early-exit** - Return early from functions +- **js-hoist-regexp** - Hoist RegExp creation outside loops +- **js-min-max-loop** - Use loop for min/max instead of sort +- **js-set-map-lookups** - Use Set/Map for O(1) lookups +- **js-tosorted-immutable** - Use toSorted() for immutability + +### 8. Advanced Patterns (LOW) + +Advanced patterns for specific cases that require careful implementation. + +- **advanced-event-handler-refs** - Store event handlers in refs +- **advanced-use-latest** - useLatest for stable callback refs + +## Available Steering Files + +This power includes detailed documentation for all 45+ rules: + +- **all-rules.md** - Complete index of all rules organized by category with descriptions and links to individual rule files + +### Individual Rule Files + +Each rule has its own detailed file in the `rules/` directory with: +- Detailed explanation of the optimization +- Incorrect code examples with explanations +- Correct code examples with explanations +- Impact metrics and real-world context + +Access individual rules by category: +- **Async/Waterfalls**: `rules/async-*.md` (5 rules) +- **Bundle Size**: `rules/bundle-*.md` (5 rules) +- **Server Performance**: `rules/server-*.md` (5 rules) +- **Client Data Fetching**: `rules/client-*.md` (2 rules) +- **Re-render Optimization**: `rules/rerender-*.md` (7 rules) +- **Rendering Performance**: `rules/rendering-*.md` (7 rules) +- **JavaScript Performance**: `rules/js-*.md` (12 rules) +- **Advanced Patterns**: `rules/advanced-*.md` (2 rules) + +## Best Practices + +1. **Prioritize by Impact** - Focus on CRITICAL and HIGH impact rules first +2. **Measure Before Optimizing** - Use profiling tools to identify actual bottlenecks +3. **Apply Incrementally** - Don't try to apply all rules at once +4. **Test Performance** - Verify improvements with real metrics +5. **Consider Trade-offs** - Some optimizations add complexity +6. **Stay Updated** - React and Next.js evolve; patterns may change + +## Common Patterns + +### Code Review Checklist + +When reviewing React/Next.js code, check for: + +1. **Waterfalls** - Are there sequential awaits that could be parallel? +2. **Barrel Imports** - Are components importing from barrel files? +3. **Bundle Size** - Are heavy libraries loaded upfront? +4. **Server Caching** - Is data being fetched multiple times? +5. **Re-renders** - Are components re-rendering unnecessarily? + +### Refactoring Workflow + +1. **Profile** - Use React DevTools Profiler and Chrome DevTools +2. **Identify** - Find the highest-impact issues +3. **Apply Rules** - Reference specific rules from this power +4. **Measure** - Verify the improvement +5. **Iterate** - Move to the next highest-impact issue + +## References + +- [React Documentation](https://react.dev) +- [Next.js Documentation](https://nextjs.org) +- [SWR Documentation](https://swr.vercel.app) +- [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) +- [How we made the Vercel dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast) diff --git a/vercel/react-best-practices/steering/all-rules.md b/vercel/react-best-practices/steering/all-rules.md new file mode 100644 index 0000000..5078183 --- /dev/null +++ b/vercel/react-best-practices/steering/all-rules.md @@ -0,0 +1,174 @@ +# All React Best Practices Rules + +This file contains all 45+ performance optimization rules organized by category. Each rule includes detailed explanations, code examples, and impact metrics. + +For individual rule files, see the `rules/` subdirectory. + +## Table of Contents + +1. [Eliminating Waterfalls (CRITICAL)](#1-eliminating-waterfalls-critical) +2. [Bundle Size Optimization (CRITICAL)](#2-bundle-size-optimization-critical) +3. [Server-Side Performance (HIGH)](#3-server-side-performance-high) +4. [Client-Side Data Fetching (MEDIUM-HIGH)](#4-client-side-data-fetching-medium-high) +5. [Re-render Optimization (MEDIUM)](#5-re-render-optimization-medium) +6. [Rendering Performance (MEDIUM)](#6-rendering-performance-medium) +7. [JavaScript Performance (LOW-MEDIUM)](#7-javascript-performance-low-medium) +8. [Advanced Patterns (LOW)](#8-advanced-patterns-low) + +--- + +## 1. Eliminating Waterfalls (CRITICAL) + +**Impact:** CRITICAL +**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### Rules in this category: + +- async-defer-await +- async-parallel +- async-dependencies +- async-api-routes +- async-suspense-boundaries + +**For detailed examples of each rule, see the individual files in `rules/async-*.md`** + +--- + +## 2. Bundle Size Optimization (CRITICAL) + +**Impact:** CRITICAL +**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### Rules in this category: + +- bundle-barrel-imports +- bundle-dynamic-imports +- bundle-defer-third-party +- bundle-conditional +- bundle-preload + +**For detailed examples of each rule, see the individual files in `rules/bundle-*.md`** + +--- + +## 3. Server-Side Performance (HIGH) + +**Impact:** HIGH +**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### Rules in this category: + +- server-cache-react +- server-cache-lru +- server-serialization +- server-parallel-fetching +- server-after-nonblocking + +**For detailed examples of each rule, see the individual files in `rules/server-*.md`** + +--- + +## 4. Client-Side Data Fetching (MEDIUM-HIGH) + +**Impact:** MEDIUM-HIGH +**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### Rules in this category: + +- client-swr-dedup +- client-event-listeners + +**For detailed examples of each rule, see the individual files in `rules/client-*.md`** + +--- + +## 5. Re-render Optimization (MEDIUM) + +**Impact:** MEDIUM +**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### Rules in this category: + +- rerender-defer-reads +- rerender-memo +- rerender-dependencies +- rerender-derived-state +- rerender-functional-setstate +- rerender-lazy-state-init +- rerender-transitions + +**For detailed examples of each rule, see the individual files in `rules/rerender-*.md`** + +--- + +## 6. Rendering Performance (MEDIUM) + +**Impact:** MEDIUM +**Description:** Optimizing the rendering process reduces the work the browser needs to do. + +### Rules in this category: + +- rendering-animate-svg-wrapper +- rendering-content-visibility +- rendering-hoist-jsx +- rendering-svg-precision +- rendering-hydration-no-flicker +- rendering-activity +- rendering-conditional-render + +**For detailed examples of each rule, see the individual files in `rules/rendering-*.md`** + +--- + +## 7. JavaScript Performance (LOW-MEDIUM) + +**Impact:** LOW-MEDIUM +**Description:** Micro-optimizations for hot paths can add up to meaningful improvements. + +### Rules in this category: + +- js-batch-dom-css +- js-index-maps +- js-cache-property-access +- js-cache-function-results +- js-cache-storage +- js-combine-iterations +- js-length-check-first +- js-early-exit +- js-hoist-regexp +- js-min-max-loop +- js-set-map-lookups +- js-tosorted-immutable + +**For detailed examples of each rule, see the individual files in `rules/js-*.md`** + +--- + +## 8. Advanced Patterns (LOW) + +**Impact:** LOW +**Description:** Advanced patterns for specific cases that require careful implementation. + +### Rules in this category: + +- advanced-event-handler-refs +- advanced-use-latest + +**For detailed examples of each rule, see the individual files in `rules/advanced-*.md`** + +--- + +## How to Use These Rules + +1. **Start with CRITICAL impact rules** - Focus on eliminating waterfalls and reducing bundle size first +2. **Profile before optimizing** - Use React DevTools and Chrome DevTools to identify actual bottlenecks +3. **Apply rules incrementally** - Don't try to apply everything at once +4. **Measure improvements** - Verify that optimizations actually help +5. **Consider trade-offs** - Some optimizations add code complexity + +## References + +- React Documentation: https://react.dev +- Next.js Documentation: https://nextjs.org +- SWR Documentation: https://swr.vercel.app +- Vercel Blog: https://vercel.com/blog diff --git a/vercel/react-best-practices/steering/rules/_sections.md b/vercel/react-best-practices/steering/rules/_sections.md new file mode 100644 index 0000000..4d20c14 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/_sections.md @@ -0,0 +1,46 @@ +# Sections + +This file defines all sections, their ordering, impact levels, and descriptions. +The section ID (in parentheses) is the filename prefix used to group rules. + +--- + +## 1. Eliminating Waterfalls (async) + +**Impact:** CRITICAL +**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +## 2. Bundle Size Optimization (bundle) + +**Impact:** CRITICAL +**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +## 3. Server-Side Performance (server) + +**Impact:** HIGH +**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +## 4. Client-Side Data Fetching (client) + +**Impact:** MEDIUM-HIGH +**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +## 5. Re-render Optimization (rerender) + +**Impact:** MEDIUM +**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +## 6. Rendering Performance (rendering) + +**Impact:** MEDIUM +**Description:** Optimizing the rendering process reduces the work the browser needs to do. + +## 7. JavaScript Performance (js) + +**Impact:** LOW-MEDIUM +**Description:** Micro-optimizations for hot paths can add up to meaningful improvements. + +## 8. Advanced Patterns (advanced) + +**Impact:** LOW +**Description:** Advanced patterns for specific cases that require careful implementation. diff --git a/vercel/react-best-practices/steering/rules/_template.md b/vercel/react-best-practices/steering/rules/_template.md new file mode 100644 index 0000000..1e9e707 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/_template.md @@ -0,0 +1,28 @@ +--- +title: Rule Title Here +impact: MEDIUM +impactDescription: Optional description of impact (e.g., "20-50% improvement") +tags: tag1, tag2 +--- + +## Rule Title Here + +**Impact: MEDIUM (optional impact description)** + +Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications. + +**Incorrect (description of what's wrong):** + +```typescript +// Bad code example here +const bad = example() +``` + +**Correct (description of what's right):** + +```typescript +// Good code example here +const good = example() +``` + +Reference: [Link to documentation or resource](https://example.com) diff --git a/vercel/react-best-practices/steering/rules/advanced-event-handler-refs.md b/vercel/react-best-practices/steering/rules/advanced-event-handler-refs.md new file mode 100644 index 0000000..3a45152 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/advanced-event-handler-refs.md @@ -0,0 +1,55 @@ +--- +title: Store Event Handlers in Refs +impact: LOW +impactDescription: stable subscriptions +tags: advanced, hooks, refs, event-handlers, optimization +--- + +## Store Event Handlers in Refs + +Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes. + +**Incorrect (re-subscribes on every render):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + useEffect(() => { + window.addEventListener(event, handler) + return () => window.removeEventListener(event, handler) + }, [event, handler]) +} +``` + +**Correct (stable subscription):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + const handlerRef = useRef(handler) + useEffect(() => { + handlerRef.current = handler + }, [handler]) + + useEffect(() => { + const listener = () => handlerRef.current() + window.addEventListener(event, listener) + return () => window.removeEventListener(event, listener) + }, [event]) +} +``` + +**Alternative: use `useEffectEvent` if you're on latest React:** + +```tsx +import { useEffectEvent } from 'react' + +function useWindowEvent(event: string, handler: () => void) { + const onEvent = useEffectEvent(handler) + + useEffect(() => { + window.addEventListener(event, onEvent) + return () => window.removeEventListener(event, onEvent) + }, [event]) +} +``` + +`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler. diff --git a/vercel/react-best-practices/steering/rules/advanced-use-latest.md b/vercel/react-best-practices/steering/rules/advanced-use-latest.md new file mode 100644 index 0000000..3facdf2 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/advanced-use-latest.md @@ -0,0 +1,49 @@ +--- +title: useLatest for Stable Callback Refs +impact: LOW +impactDescription: prevents effect re-runs +tags: advanced, hooks, useLatest, refs, optimization +--- + +## useLatest for Stable Callback Refs + +Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. + +**Implementation:** + +```typescript +function useLatest(value: T) { + const ref = useRef(value) + useEffect(() => { + ref.current = value + }, [value]) + return ref +} +``` + +**Incorrect (effect re-runs on every callback change):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + + useEffect(() => { + const timeout = setTimeout(() => onSearch(query), 300) + return () => clearTimeout(timeout) + }, [query, onSearch]) +} +``` + +**Correct (stable effect, fresh callback):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + const onSearchRef = useLatest(onSearch) + + useEffect(() => { + const timeout = setTimeout(() => onSearchRef.current(query), 300) + return () => clearTimeout(timeout) + }, [query]) +} +``` diff --git a/vercel/react-best-practices/steering/rules/async-api-routes.md b/vercel/react-best-practices/steering/rules/async-api-routes.md new file mode 100644 index 0000000..6feda1e --- /dev/null +++ b/vercel/react-best-practices/steering/rules/async-api-routes.md @@ -0,0 +1,38 @@ +--- +title: Prevent Waterfall Chains in API Routes +impact: CRITICAL +impactDescription: 2-10× improvement +tags: api-routes, server-actions, waterfalls, parallelization +--- + +## 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). diff --git a/vercel/react-best-practices/steering/rules/async-defer-await.md b/vercel/react-best-practices/steering/rules/async-defer-await.md new file mode 100644 index 0000000..ea7082a --- /dev/null +++ b/vercel/react-best-practices/steering/rules/async-defer-await.md @@ -0,0 +1,80 @@ +--- +title: Defer Await Until Needed +impact: HIGH +impactDescription: avoids blocking unused code paths +tags: async, await, conditional, optimization +--- + +## 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. diff --git a/vercel/react-best-practices/steering/rules/async-dependencies.md b/vercel/react-best-practices/steering/rules/async-dependencies.md new file mode 100644 index 0000000..fb90d86 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/async-dependencies.md @@ -0,0 +1,36 @@ +--- +title: Dependency-Based Parallelization +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, dependencies, better-all +--- + +## 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) diff --git a/vercel/react-best-practices/steering/rules/async-parallel.md b/vercel/react-best-practices/steering/rules/async-parallel.md new file mode 100644 index 0000000..64133f6 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/async-parallel.md @@ -0,0 +1,28 @@ +--- +title: Promise.all() for Independent Operations +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, promises, waterfalls +--- + +## 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() +]) +``` diff --git a/vercel/react-best-practices/steering/rules/async-suspense-boundaries.md b/vercel/react-best-practices/steering/rules/async-suspense-boundaries.md new file mode 100644 index 0000000..1fbc05b --- /dev/null +++ b/vercel/react-best-practices/steering/rules/async-suspense-boundaries.md @@ -0,0 +1,99 @@ +--- +title: Strategic Suspense Boundaries +impact: HIGH +impactDescription: faster initial paint +tags: async, suspense, streaming, layout-shift +--- + +## 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. + +**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. diff --git a/vercel/react-best-practices/steering/rules/bundle-barrel-imports.md b/vercel/react-best-practices/steering/rules/bundle-barrel-imports.md new file mode 100644 index 0000000..ee48f32 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/bundle-barrel-imports.md @@ -0,0 +1,59 @@ +--- +title: Avoid Barrel File Imports +impact: CRITICAL +impactDescription: 200-800ms import cost, slow builds +tags: bundle, imports, tree-shaking, barrel-files, performance +--- + +## 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. + +**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: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) diff --git a/vercel/react-best-practices/steering/rules/bundle-conditional.md b/vercel/react-best-practices/steering/rules/bundle-conditional.md new file mode 100644 index 0000000..40bd6f9 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/bundle-conditional.md @@ -0,0 +1,31 @@ +--- +title: Conditional Module Loading +impact: HIGH +impactDescription: loads large data only when needed +tags: bundle, conditional-loading, lazy-loading +--- + +## 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. diff --git a/vercel/react-best-practices/steering/rules/bundle-defer-third-party.md b/vercel/react-best-practices/steering/rules/bundle-defer-third-party.md new file mode 100644 index 0000000..db041d1 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/bundle-defer-third-party.md @@ -0,0 +1,49 @@ +--- +title: Defer Non-Critical Third-Party Libraries +impact: MEDIUM +impactDescription: loads after hydration +tags: bundle, third-party, analytics, defer +--- + +## 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} + + + + ) +} +``` diff --git a/vercel/react-best-practices/steering/rules/bundle-dynamic-imports.md b/vercel/react-best-practices/steering/rules/bundle-dynamic-imports.md new file mode 100644 index 0000000..60b6269 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/bundle-dynamic-imports.md @@ -0,0 +1,35 @@ +--- +title: Dynamic Imports for Heavy Components +impact: CRITICAL +impactDescription: directly affects TTI and LCP +tags: bundle, dynamic-import, code-splitting, next-dynamic +--- + +## 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 +} +``` diff --git a/vercel/react-best-practices/steering/rules/bundle-preload.md b/vercel/react-best-practices/steering/rules/bundle-preload.md new file mode 100644 index 0000000..7000504 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/bundle-preload.md @@ -0,0 +1,50 @@ +--- +title: Preload Based on User Intent +impact: MEDIUM +impactDescription: reduces perceived latency +tags: bundle, preload, user-intent, hover +--- + +## 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. diff --git a/vercel/react-best-practices/steering/rules/client-event-listeners.md b/vercel/react-best-practices/steering/rules/client-event-listeners.md new file mode 100644 index 0000000..aad4ae9 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/client-event-listeners.md @@ -0,0 +1,74 @@ +--- +title: Deduplicate Global Event Listeners +impact: LOW +impactDescription: single listener for N components +tags: client, swr, event-listeners, subscription +--- + +## 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', () => { /* ... */ }) + // ... +} +``` diff --git a/vercel/react-best-practices/steering/rules/client-swr-dedup.md b/vercel/react-best-practices/steering/rules/client-swr-dedup.md new file mode 100644 index 0000000..2a430f2 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/client-swr-dedup.md @@ -0,0 +1,56 @@ +--- +title: Use SWR for Automatic Deduplication +impact: MEDIUM-HIGH +impactDescription: automatic deduplication +tags: client, swr, deduplication, data-fetching +--- + +## 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) diff --git a/vercel/react-best-practices/steering/rules/js-batch-dom-css.md b/vercel/react-best-practices/steering/rules/js-batch-dom-css.md new file mode 100644 index 0000000..92a3b63 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-batch-dom-css.md @@ -0,0 +1,82 @@ +--- +title: Batch DOM CSS Changes +impact: MEDIUM +impactDescription: reduces reflows/repaints +tags: javascript, dom, css, performance, reflow +--- + +## Batch DOM CSS Changes + +Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows. + +**Incorrect (multiple reflows):** + +```typescript +function updateElementStyles(element: HTMLElement) { + // Each line triggers a reflow + element.style.width = '100px' + element.style.height = '200px' + element.style.backgroundColor = 'blue' + element.style.border = '1px solid black' +} +``` + +**Correct (add class - single reflow):** + +```typescript +// CSS file +.highlighted-box { + width: 100px; + height: 200px; + background-color: blue; + border: 1px solid black; +} + +// JavaScript +function updateElementStyles(element: HTMLElement) { + element.classList.add('highlighted-box') +} +``` + +**Correct (change cssText - single reflow):** + +```typescript +function updateElementStyles(element: HTMLElement) { + element.style.cssText = ` + width: 100px; + height: 200px; + background-color: blue; + border: 1px solid black; + ` +} +``` + +**React example:** + +```tsx +// Incorrect: changing styles one by one +function Box({ isHighlighted }: { isHighlighted: boolean }) { + const ref = useRef(null) + + useEffect(() => { + if (ref.current && isHighlighted) { + ref.current.style.width = '100px' + ref.current.style.height = '200px' + ref.current.style.backgroundColor = 'blue' + } + }, [isHighlighted]) + + return
Content
+} + +// Correct: toggle class +function Box({ isHighlighted }: { isHighlighted: boolean }) { + return ( +
+ Content +
+ ) +} +``` + +Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns. diff --git a/vercel/react-best-practices/steering/rules/js-cache-function-results.md b/vercel/react-best-practices/steering/rules/js-cache-function-results.md new file mode 100644 index 0000000..180f8ac --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-cache-function-results.md @@ -0,0 +1,80 @@ +--- +title: Cache Repeated Function Calls +impact: MEDIUM +impactDescription: avoid redundant computation +tags: javascript, cache, memoization, performance +--- + +## Cache Repeated Function Calls + +Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render. + +**Incorrect (redundant computation):** + +```typescript +function ProjectList({ projects }: { projects: Project[] }) { + return ( +
+ {projects.map(project => { + // slugify() called 100+ times for same project names + const slug = slugify(project.name) + + return + })} +
+ ) +} +``` + +**Correct (cached results):** + +```typescript +// Module-level cache +const slugifyCache = new Map() + +function cachedSlugify(text: string): string { + if (slugifyCache.has(text)) { + return slugifyCache.get(text)! + } + const result = slugify(text) + slugifyCache.set(text, result) + return result +} + +function ProjectList({ projects }: { projects: Project[] }) { + return ( +
+ {projects.map(project => { + // Computed only once per unique project name + const slug = cachedSlugify(project.name) + + return + })} +
+ ) +} +``` + +**Simpler pattern for single-value functions:** + +```typescript +let isLoggedInCache: boolean | null = null + +function isLoggedIn(): boolean { + if (isLoggedInCache !== null) { + return isLoggedInCache + } + + isLoggedInCache = document.cookie.includes('auth=') + return isLoggedInCache +} + +// Clear cache when auth changes +function onAuthChange() { + isLoggedInCache = null +} +``` + +Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. + +Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast) diff --git a/vercel/react-best-practices/steering/rules/js-cache-property-access.md b/vercel/react-best-practices/steering/rules/js-cache-property-access.md new file mode 100644 index 0000000..39eec90 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-cache-property-access.md @@ -0,0 +1,28 @@ +--- +title: Cache Property Access in Loops +impact: LOW-MEDIUM +impactDescription: reduces lookups +tags: javascript, loops, optimization, caching +--- + +## Cache Property Access in Loops + +Cache object property lookups in hot paths. + +**Incorrect (3 lookups × N iterations):** + +```typescript +for (let i = 0; i < arr.length; i++) { + process(obj.config.settings.value) +} +``` + +**Correct (1 lookup total):** + +```typescript +const value = obj.config.settings.value +const len = arr.length +for (let i = 0; i < len; i++) { + process(value) +} +``` diff --git a/vercel/react-best-practices/steering/rules/js-cache-storage.md b/vercel/react-best-practices/steering/rules/js-cache-storage.md new file mode 100644 index 0000000..aa4a30c --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-cache-storage.md @@ -0,0 +1,70 @@ +--- +title: Cache Storage API Calls +impact: LOW-MEDIUM +impactDescription: reduces expensive I/O +tags: javascript, localStorage, storage, caching, performance +--- + +## Cache Storage API Calls + +`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory. + +**Incorrect (reads storage on every call):** + +```typescript +function getTheme() { + return localStorage.getItem('theme') ?? 'light' +} +// Called 10 times = 10 storage reads +``` + +**Correct (Map cache):** + +```typescript +const storageCache = new Map() + +function getLocalStorage(key: string) { + if (!storageCache.has(key)) { + storageCache.set(key, localStorage.getItem(key)) + } + return storageCache.get(key) +} + +function setLocalStorage(key: string, value: string) { + localStorage.setItem(key, value) + storageCache.set(key, value) // keep cache in sync +} +``` + +Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. + +**Cookie caching:** + +```typescript +let cookieCache: Record | null = null + +function getCookie(name: string) { + if (!cookieCache) { + cookieCache = Object.fromEntries( + document.cookie.split('; ').map(c => c.split('=')) + ) + } + return cookieCache[name] +} +``` + +**Important (invalidate on external changes):** + +If storage can change externally (another tab, server-set cookies), invalidate cache: + +```typescript +window.addEventListener('storage', (e) => { + if (e.key) storageCache.delete(e.key) +}) + +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + storageCache.clear() + } +}) +``` diff --git a/vercel/react-best-practices/steering/rules/js-combine-iterations.md b/vercel/react-best-practices/steering/rules/js-combine-iterations.md new file mode 100644 index 0000000..044d017 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-combine-iterations.md @@ -0,0 +1,32 @@ +--- +title: Combine Multiple Array Iterations +impact: LOW-MEDIUM +impactDescription: reduces iterations +tags: javascript, arrays, loops, performance +--- + +## Combine Multiple Array Iterations + +Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop. + +**Incorrect (3 iterations):** + +```typescript +const admins = users.filter(u => u.isAdmin) +const testers = users.filter(u => u.isTester) +const inactive = users.filter(u => !u.isActive) +``` + +**Correct (1 iteration):** + +```typescript +const admins: User[] = [] +const testers: User[] = [] +const inactive: User[] = [] + +for (const user of users) { + if (user.isAdmin) admins.push(user) + if (user.isTester) testers.push(user) + if (!user.isActive) inactive.push(user) +} +``` diff --git a/vercel/react-best-practices/steering/rules/js-early-exit.md b/vercel/react-best-practices/steering/rules/js-early-exit.md new file mode 100644 index 0000000..f46cb89 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-early-exit.md @@ -0,0 +1,50 @@ +--- +title: Early Return from Functions +impact: LOW-MEDIUM +impactDescription: avoids unnecessary computation +tags: javascript, functions, optimization, early-return +--- + +## Early Return from Functions + +Return early when result is determined to skip unnecessary processing. + +**Incorrect (processes all items even after finding answer):** + +```typescript +function validateUsers(users: User[]) { + let hasError = false + let errorMessage = '' + + for (const user of users) { + if (!user.email) { + hasError = true + errorMessage = 'Email required' + } + if (!user.name) { + hasError = true + errorMessage = 'Name required' + } + // Continues checking all users even after error found + } + + return hasError ? { valid: false, error: errorMessage } : { valid: true } +} +``` + +**Correct (returns immediately on first error):** + +```typescript +function validateUsers(users: User[]) { + for (const user of users) { + if (!user.email) { + return { valid: false, error: 'Email required' } + } + if (!user.name) { + return { valid: false, error: 'Name required' } + } + } + + return { valid: true } +} +``` diff --git a/vercel/react-best-practices/steering/rules/js-hoist-regexp.md b/vercel/react-best-practices/steering/rules/js-hoist-regexp.md new file mode 100644 index 0000000..dae3fef --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-hoist-regexp.md @@ -0,0 +1,45 @@ +--- +title: Hoist RegExp Creation +impact: LOW-MEDIUM +impactDescription: avoids recreation +tags: javascript, regexp, optimization, memoization +--- + +## Hoist RegExp Creation + +Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`. + +**Incorrect (new RegExp every render):** + +```tsx +function Highlighter({ text, query }: Props) { + const regex = new RegExp(`(${query})`, 'gi') + const parts = text.split(regex) + return <>{parts.map((part, i) => ...)} +} +``` + +**Correct (memoize or hoist):** + +```tsx +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +function Highlighter({ text, query }: Props) { + const regex = useMemo( + () => new RegExp(`(${escapeRegex(query)})`, 'gi'), + [query] + ) + const parts = text.split(regex) + return <>{parts.map((part, i) => ...)} +} +``` + +**Warning (global regex has mutable state):** + +Global regex (`/g`) has mutable `lastIndex` state: + +```typescript +const regex = /foo/g +regex.test('foo') // true, lastIndex = 3 +regex.test('foo') // false, lastIndex = 0 +``` diff --git a/vercel/react-best-practices/steering/rules/js-index-maps.md b/vercel/react-best-practices/steering/rules/js-index-maps.md new file mode 100644 index 0000000..9d357a0 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-index-maps.md @@ -0,0 +1,37 @@ +--- +title: Build Index Maps for Repeated Lookups +impact: LOW-MEDIUM +impactDescription: 1M ops to 2K ops +tags: javascript, map, indexing, optimization, performance +--- + +## Build Index Maps for Repeated Lookups + +Multiple `.find()` calls by the same key should use a Map. + +**Incorrect (O(n) per lookup):** + +```typescript +function processOrders(orders: Order[], users: User[]) { + return orders.map(order => ({ + ...order, + user: users.find(u => u.id === order.userId) + })) +} +``` + +**Correct (O(1) per lookup):** + +```typescript +function processOrders(orders: Order[], users: User[]) { + const userById = new Map(users.map(u => [u.id, u])) + + return orders.map(order => ({ + ...order, + user: userById.get(order.userId) + })) +} +``` + +Build map once (O(n)), then all lookups are O(1). +For 1000 orders × 1000 users: 1M ops → 2K ops. diff --git a/vercel/react-best-practices/steering/rules/js-length-check-first.md b/vercel/react-best-practices/steering/rules/js-length-check-first.md new file mode 100644 index 0000000..6c38625 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-length-check-first.md @@ -0,0 +1,49 @@ +--- +title: Early Length Check for Array Comparisons +impact: MEDIUM-HIGH +impactDescription: avoids expensive operations when lengths differ +tags: javascript, arrays, performance, optimization, comparison +--- + +## Early Length Check for Array Comparisons + +When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal. + +In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops). + +**Incorrect (always runs expensive comparison):** + +```typescript +function hasChanges(current: string[], original: string[]) { + // Always sorts and joins, even when lengths differ + return current.sort().join() !== original.sort().join() +} +``` + +Two 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. + +**Correct (O(1) length check first):** + +```typescript +function hasChanges(current: string[], original: string[]) { + // Early return if lengths differ + if (current.length !== original.length) { + return true + } + // Only sort/join when lengths match + const currentSorted = current.toSorted() + const originalSorted = original.toSorted() + for (let i = 0; i < currentSorted.length; i++) { + if (currentSorted[i] !== originalSorted[i]) { + return true + } + } + return false +} +``` + +This new approach is more efficient because: +- It avoids the overhead of sorting and joining the arrays when lengths differ +- It avoids consuming memory for the joined strings (especially important for large arrays) +- It avoids mutating the original arrays +- It returns early when a difference is found diff --git a/vercel/react-best-practices/steering/rules/js-min-max-loop.md b/vercel/react-best-practices/steering/rules/js-min-max-loop.md new file mode 100644 index 0000000..02caec5 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-min-max-loop.md @@ -0,0 +1,82 @@ +--- +title: Use Loop for Min/Max Instead of Sort +impact: LOW +impactDescription: O(n) instead of O(n log n) +tags: javascript, arrays, performance, sorting, algorithms +--- + +## Use Loop for Min/Max Instead of Sort + +Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower. + +**Incorrect (O(n log n) - sort to find latest):** + +```typescript +interface Project { + id: string + name: string + updatedAt: number +} + +function getLatestProject(projects: Project[]) { + const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt) + return sorted[0] +} +``` + +Sorts the entire array just to find the maximum value. + +**Incorrect (O(n log n) - sort for oldest and newest):** + +```typescript +function getOldestAndNewest(projects: Project[]) { + const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt) + return { oldest: sorted[0], newest: sorted[sorted.length - 1] } +} +``` + +Still sorts unnecessarily when only min/max are needed. + +**Correct (O(n) - single loop):** + +```typescript +function getLatestProject(projects: Project[]) { + if (projects.length === 0) return null + + let latest = projects[0] + + for (let i = 1; i < projects.length; i++) { + if (projects[i].updatedAt > latest.updatedAt) { + latest = projects[i] + } + } + + return latest +} + +function getOldestAndNewest(projects: Project[]) { + if (projects.length === 0) return { oldest: null, newest: null } + + let oldest = projects[0] + let newest = projects[0] + + for (let i = 1; i < projects.length; i++) { + if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i] + if (projects[i].updatedAt > newest.updatedAt) newest = projects[i] + } + + return { oldest, newest } +} +``` + +Single pass through the array, no copying, no sorting. + +**Alternative (Math.min/Math.max for small arrays):** + +```typescript +const numbers = [5, 2, 8, 1, 9] +const min = Math.min(...numbers) +const max = Math.max(...numbers) +``` + +This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability. diff --git a/vercel/react-best-practices/steering/rules/js-set-map-lookups.md b/vercel/react-best-practices/steering/rules/js-set-map-lookups.md new file mode 100644 index 0000000..680a489 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-set-map-lookups.md @@ -0,0 +1,24 @@ +--- +title: Use Set/Map for O(1) Lookups +impact: LOW-MEDIUM +impactDescription: O(n) to O(1) +tags: javascript, set, map, data-structures, performance +--- + +## Use Set/Map for O(1) Lookups + +Convert arrays to Set/Map for repeated membership checks. + +**Incorrect (O(n) per check):** + +```typescript +const allowedIds = ['a', 'b', 'c', ...] +items.filter(item => allowedIds.includes(item.id)) +``` + +**Correct (O(1) per check):** + +```typescript +const allowedIds = new Set(['a', 'b', 'c', ...]) +items.filter(item => allowedIds.has(item.id)) +``` diff --git a/vercel/react-best-practices/steering/rules/js-tosorted-immutable.md b/vercel/react-best-practices/steering/rules/js-tosorted-immutable.md new file mode 100644 index 0000000..eae8b3f --- /dev/null +++ b/vercel/react-best-practices/steering/rules/js-tosorted-immutable.md @@ -0,0 +1,57 @@ +--- +title: Use toSorted() Instead of sort() for Immutability +impact: MEDIUM-HIGH +impactDescription: prevents mutation bugs in React state +tags: javascript, arrays, immutability, react, state, mutation +--- + +## Use toSorted() Instead of sort() for Immutability + +`.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. + +**Incorrect (mutates original array):** + +```typescript +function UserList({ users }: { users: User[] }) { + // Mutates the users prop array! + const sorted = useMemo( + () => users.sort((a, b) => a.name.localeCompare(b.name)), + [users] + ) + return
{sorted.map(renderUser)}
+} +``` + +**Correct (creates new array):** + +```typescript +function UserList({ users }: { users: User[] }) { + // Creates new sorted array, original unchanged + const sorted = useMemo( + () => users.toSorted((a, b) => a.name.localeCompare(b.name)), + [users] + ) + return
{sorted.map(renderUser)}
+} +``` + +**Why this matters in React:** + +1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only +2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior + +**Browser support (fallback for older browsers):** + +`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator: + +```typescript +// Fallback for older browsers +const sorted = [...items].sort((a, b) => a.value - b.value) +``` + +**Other immutable array methods:** + +- `.toSorted()` - immutable sort +- `.toReversed()` - immutable reverse +- `.toSpliced()` - immutable splice +- `.with()` - immutable element replacement diff --git a/vercel/react-best-practices/steering/rules/rendering-activity.md b/vercel/react-best-practices/steering/rules/rendering-activity.md new file mode 100644 index 0000000..c957a49 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-activity.md @@ -0,0 +1,26 @@ +--- +title: Use Activity Component for Show/Hide +impact: MEDIUM +impactDescription: preserves state/DOM +tags: rendering, activity, visibility, state-preservation +--- + +## Use Activity Component for Show/Hide + +Use React's `` to preserve state/DOM for expensive components that frequently toggle visibility. + +**Usage:** + +```tsx +import { Activity } from 'react' + +function Dropdown({ isOpen }: Props) { + return ( + + + + ) +} +``` + +Avoids expensive re-renders and state loss. diff --git a/vercel/react-best-practices/steering/rules/rendering-animate-svg-wrapper.md b/vercel/react-best-practices/steering/rules/rendering-animate-svg-wrapper.md new file mode 100644 index 0000000..646744c --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-animate-svg-wrapper.md @@ -0,0 +1,47 @@ +--- +title: Animate SVG Wrapper Instead of SVG Element +impact: LOW +impactDescription: enables hardware acceleration +tags: rendering, svg, css, animation, performance +--- + +## Animate SVG Wrapper Instead of SVG Element + +Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `
` and animate the wrapper instead. + +**Incorrect (animating SVG directly - no hardware acceleration):** + +```tsx +function LoadingSpinner() { + return ( + + + + ) +} +``` + +**Correct (animating wrapper div - hardware accelerated):** + +```tsx +function LoadingSpinner() { + return ( +
+ + + +
+ ) +} +``` + +This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations. diff --git a/vercel/react-best-practices/steering/rules/rendering-conditional-render.md b/vercel/react-best-practices/steering/rules/rendering-conditional-render.md new file mode 100644 index 0000000..7e866f5 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-conditional-render.md @@ -0,0 +1,40 @@ +--- +title: Use Explicit Conditional Rendering +impact: LOW +impactDescription: prevents rendering 0 or NaN +tags: rendering, conditional, jsx, falsy-values +--- + +## Use Explicit Conditional Rendering + +Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render. + +**Incorrect (renders "0" when count is 0):** + +```tsx +function Badge({ count }: { count: number }) { + return ( +
+ {count && {count}} +
+ ) +} + +// When count = 0, renders:
0
+// When count = 5, renders:
5
+``` + +**Correct (renders nothing when count is 0):** + +```tsx +function Badge({ count }: { count: number }) { + return ( +
+ {count > 0 ? {count} : null} +
+ ) +} + +// When count = 0, renders:
+// When count = 5, renders:
5
+``` diff --git a/vercel/react-best-practices/steering/rules/rendering-content-visibility.md b/vercel/react-best-practices/steering/rules/rendering-content-visibility.md new file mode 100644 index 0000000..aa66563 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-content-visibility.md @@ -0,0 +1,38 @@ +--- +title: CSS content-visibility for Long Lists +impact: HIGH +impactDescription: faster initial render +tags: rendering, css, content-visibility, long-lists +--- + +## CSS content-visibility for Long Lists + +Apply `content-visibility: auto` to defer off-screen rendering. + +**CSS:** + +```css +.message-item { + content-visibility: auto; + contain-intrinsic-size: 0 80px; +} +``` + +**Example:** + +```tsx +function MessageList({ messages }: { messages: Message[] }) { + return ( +
+ {messages.map(msg => ( +
+ +
{msg.content}
+
+ ))} +
+ ) +} +``` + +For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render). diff --git a/vercel/react-best-practices/steering/rules/rendering-hoist-jsx.md b/vercel/react-best-practices/steering/rules/rendering-hoist-jsx.md new file mode 100644 index 0000000..32d2f3f --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-hoist-jsx.md @@ -0,0 +1,46 @@ +--- +title: Hoist Static JSX Elements +impact: LOW +impactDescription: avoids re-creation +tags: rendering, jsx, static, optimization +--- + +## Hoist Static JSX Elements + +Extract static JSX outside components to avoid re-creation. + +**Incorrect (recreates element every render):** + +```tsx +function LoadingSkeleton() { + return
+} + +function Container() { + return ( +
+ {loading && } +
+ ) +} +``` + +**Correct (reuses same element):** + +```tsx +const loadingSkeleton = ( +
+) + +function Container() { + return ( +
+ {loading && loadingSkeleton} +
+ ) +} +``` + +This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render. + +**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. diff --git a/vercel/react-best-practices/steering/rules/rendering-hydration-no-flicker.md b/vercel/react-best-practices/steering/rules/rendering-hydration-no-flicker.md new file mode 100644 index 0000000..5cf0e79 --- /dev/null +++ b/vercel/react-best-practices/steering/rules/rendering-hydration-no-flicker.md @@ -0,0 +1,82 @@ +--- +title: Prevent Hydration Mismatch Without Flickering +impact: MEDIUM +impactDescription: avoids visual flicker and hydration errors +tags: rendering, ssr, hydration, localStorage, flicker +--- + +## Prevent Hydration Mismatch Without Flickering + +When 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. + +**Incorrect (breaks SSR):** + +```tsx +function ThemeWrapper({ children }: { children: ReactNode }) { + // localStorage is not available on server - throws error + const theme = localStorage.getItem('theme') || 'light' + + return ( +
+ {children} +
+ ) +} +``` + +Server-side rendering will fail because `localStorage` is undefined. + +**Incorrect (visual flickering):** + +```tsx +function ThemeWrapper({ children }: { children: ReactNode }) { + const [theme, setTheme] = useState('light') + + useEffect(() => { + // Runs after hydration - causes visible flash + const stored = localStorage.getItem('theme') + if (stored) { + setTheme(stored) + } + }, []) + + return ( +
+ {children} +
+ ) +} +``` + +Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content. + +**Correct (no flicker, no hydration mismatch):** + +```tsx +function ThemeWrapper({ children }: { children: ReactNode }) { + return ( + <> +
+ {children} +
+