diff --git a/.gitignore b/.gitignore index fc3dc6c..a5523f2 100644 --- a/.gitignore +++ b/.gitignore @@ -427,6 +427,8 @@ local/ *.backup *.old *.orig +backup-before-env-removal-*/ +backup-*/ # System files .directory diff --git a/.windsurf/skills/find-skills/SKILL.md b/.windsurf/skills/find-skills/SKILL.md deleted file mode 100644 index c797184..0000000 --- a/.windsurf/skills/find-skills/SKILL.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -name: find-skills -description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. ---- - -# Find Skills - -This skill helps you discover and install skills from the open agent skills ecosystem. - -## When to Use This Skill - -Use this skill when the user: - -- Asks "how do I do X" where X might be a common task with an existing skill -- Says "find a skill for X" or "is there a skill for X" -- Asks "can you do X" where X is a specialized capability -- Expresses interest in extending agent capabilities -- Wants to search for tools, templates, or workflows -- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) - -## What is the Skills CLI? - -The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. - -**Key commands:** - -- `npx skills find [query]` - Search for skills interactively or by keyword -- `npx skills add ` - Install a skill from GitHub or other sources -- `npx skills check` - Check for skill updates -- `npx skills update` - Update all installed skills - -**Browse skills at:** https://skills.sh/ - -## How to Help Users Find Skills - -### Step 1: Understand What They Need - -When a user asks for help with something, identify: - -1. The domain (e.g., React, testing, design, deployment) -2. The specific task (e.g., writing tests, creating animations, reviewing PRs) -3. Whether this is a common enough task that a skill likely exists - -### Step 2: Search for Skills - -Run the find command with a relevant query: - -```bash -npx skills find [query] -``` - -For example: - -- User asks "how do I make my React app faster?" → `npx skills find react performance` -- User asks "can you help me with PR reviews?" → `npx skills find pr review` -- User asks "I need to create a changelog" → `npx skills find changelog` - -The command will return results like: - -``` -Install with npx skills add - -vercel-labs/agent-skills@vercel-react-best-practices -└ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices -``` - -### Step 3: Present Options to the User - -When you find relevant skills, present them to the user with: - -1. The skill name and what it does -2. The install command they can run -3. A link to learn more at skills.sh - -Example response: - -``` -I found a skill that might help! The "vercel-react-best-practices" skill provides -React and Next.js performance optimization guidelines from Vercel Engineering. - -To install it: -npx skills add vercel-labs/agent-skills@vercel-react-best-practices - -Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices -``` - -### Step 4: Offer to Install - -If the user wants to proceed, you can install the skill for them: - -```bash -npx skills add -g -y -``` - -The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. - -## Common Skill Categories - -When searching, consider these common categories: - -| Category | Example Queries | -| --------------- | ---------------------------------------- | -| Web Development | react, nextjs, typescript, css, tailwind | -| Testing | testing, jest, playwright, e2e | -| DevOps | deploy, docker, kubernetes, ci-cd | -| Documentation | docs, readme, changelog, api-docs | -| Code Quality | review, lint, refactor, best-practices | -| Design | ui, ux, design-system, accessibility | -| Productivity | workflow, automation, git | - -## Tips for Effective Searches - -1. **Use specific keywords**: "react testing" is better than just "testing" -2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" -3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` - -## When No Skills Are Found - -If no relevant skills exist: - -1. Acknowledge that no existing skill was found -2. Offer to help with the task directly using your general capabilities -3. Suggest the user could create their own skill with `npx skills init` - -Example: - -``` -I searched for skills related to "xyz" but didn't find any matches. -I can still help you with this task directly! Would you like me to proceed? - -If this is something you do often, you could create your own skill: -npx skills init my-xyz-skill -``` diff --git a/.windsurf/skills/ui-ux-pro-max/SKILL.md b/.windsurf/skills/ui-ux-pro-max/SKILL.md deleted file mode 100644 index e58d618..0000000 --- a/.windsurf/skills/ui-ux-pro-max/SKILL.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -name: ui-ux-pro-max -description: "UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 9 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient. Integrations: shadcn/ui MCP for component search and examples." ---- - -# UI/UX Pro Max - Design Intelligence - -Comprehensive design guide for web and mobile applications. Contains 50+ styles, 97 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 9 technology stacks. Searchable database with priority-based recommendations. - -## When to Apply - -Reference these guidelines when: -- Designing new UI components or pages -- Choosing color palettes and typography -- Reviewing code for UX issues -- Building landing pages or dashboards -- Implementing accessibility requirements - -## Rule Categories by Priority - -| Priority | Category | Impact | Domain | -|----------|----------|--------|--------| -| 1 | Accessibility | CRITICAL | `ux` | -| 2 | Touch & Interaction | CRITICAL | `ux` | -| 3 | Performance | HIGH | `ux` | -| 4 | Layout & Responsive | HIGH | `ux` | -| 5 | Typography & Color | MEDIUM | `typography`, `color` | -| 6 | Animation | MEDIUM | `ux` | -| 7 | Style Selection | MEDIUM | `style`, `product` | -| 8 | Charts & Data | LOW | `chart` | - -## Quick Reference - -### 1. Accessibility (CRITICAL) - -- `color-contrast` - Minimum 4.5:1 ratio for normal text -- `focus-states` - Visible focus rings on interactive elements -- `alt-text` - Descriptive alt text for meaningful images -- `aria-labels` - aria-label for icon-only buttons -- `keyboard-nav` - Tab order matches visual order -- `form-labels` - Use label with for attribute - -### 2. Touch & Interaction (CRITICAL) - -- `touch-target-size` - Minimum 44x44px touch targets -- `hover-vs-tap` - Use click/tap for primary interactions -- `loading-buttons` - Disable button during async operations -- `error-feedback` - Clear error messages near problem -- `cursor-pointer` - Add cursor-pointer to clickable elements - -### 3. Performance (HIGH) - -- `image-optimization` - Use WebP, srcset, lazy loading -- `reduced-motion` - Check prefers-reduced-motion -- `content-jumping` - Reserve space for async content - -### 4. Layout & Responsive (HIGH) - -- `viewport-meta` - width=device-width initial-scale=1 -- `readable-font-size` - Minimum 16px body text on mobile -- `horizontal-scroll` - Ensure content fits viewport width -- `z-index-management` - Define z-index scale (10, 20, 30, 50) - -### 5. Typography & Color (MEDIUM) - -- `line-height` - Use 1.5-1.75 for body text -- `line-length` - Limit to 65-75 characters per line -- `font-pairing` - Match heading/body font personalities - -### 6. Animation (MEDIUM) - -- `duration-timing` - Use 150-300ms for micro-interactions -- `transform-performance` - Use transform/opacity, not width/height -- `loading-states` - Skeleton screens or spinners - -### 7. Style Selection (MEDIUM) - -- `style-match` - Match style to product type -- `consistency` - Use same style across all pages -- `no-emoji-icons` - Use SVG icons, not emojis - -### 8. Charts & Data (LOW) - -- `chart-type` - Match chart type to data type -- `color-guidance` - Use accessible color palettes -- `data-table` - Provide table alternative for accessibility - -## How to Use - -Search specific domains using the CLI tool below. - ---- - -## Prerequisites - -Check if Python is installed: - -```bash -python3 --version || python --version -``` - -If Python is not installed, install it based on user's OS: - -**macOS:** -```bash -brew install python3 -``` - -**Ubuntu/Debian:** -```bash -sudo apt update && sudo apt install python3 -``` - -**Windows:** -```powershell -winget install Python.Python.3.12 -``` - ---- - -## How to Use This Skill - -When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow: - -### Step 1: Analyze User Requirements - -Extract key information from user request: -- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc. -- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc. -- **Industry**: healthcare, fintech, gaming, education, etc. -- **Stack**: React, Vue, Next.js, or default to `html-tailwind` - -### Step 2: Generate Design System (REQUIRED) - -**Always start with `--design-system`** to get comprehensive recommendations with reasoning: - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py " " --design-system [-p "Project Name"] -``` - -This command: -1. Searches 5 domains in parallel (product, style, color, landing, typography) -2. Applies reasoning rules from `ui-reasoning.csv` to select best matches -3. Returns complete design system: pattern, style, colors, typography, effects -4. Includes anti-patterns to avoid - -**Example:** -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa" -``` - -### Step 2b: Persist Design System (Master + Overrides Pattern) - -To save the design system for **hierarchical retrieval across sessions**, add `--persist`: - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" -``` - -This creates: -- `design-system/MASTER.md` — Global Source of Truth with all design rules -- `design-system/pages/` — Folder for page-specific overrides - -**With page-specific override:** -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" --page "dashboard" -``` - -This also creates: -- `design-system/pages/dashboard.md` — Page-specific deviations from Master - -**How hierarchical retrieval works:** -1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md` -2. If the page file exists, its rules **override** the Master file -3. If not, use `design-system/MASTER.md` exclusively - -**Context-aware retrieval prompt:** -``` -I am building the [Page Name] page. Please read design-system/MASTER.md. -Also check if design-system/pages/[page-name].md exists. -If the page file exists, prioritize its rules. -If not, use the Master rules exclusively. -Now, generate the code... -``` - -### Step 3: Supplement with Detailed Searches (as needed) - -After getting the design system, use domain searches to get additional details: - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "" --domain [-n ] -``` - -**When to use detailed searches:** - -| Need | Domain | Example | -|------|--------|---------| -| More style options | `style` | `--domain style "glassmorphism dark"` | -| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | -| UX best practices | `ux` | `--domain ux "animation accessibility"` | -| Alternative fonts | `typography` | `--domain typography "elegant luxury"` | -| Landing structure | `landing` | `--domain landing "hero social-proof"` | - -### Step 4: Stack Guidelines (Default: html-tailwind) - -Get implementation-specific best practices. If user doesn't specify a stack, **default to `html-tailwind`**. - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "" --stack html-tailwind -``` - -Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`, `shadcn`, `jetpack-compose` - ---- - -## Search Reference - -### Available Domains - -| Domain | Use For | Example Keywords | -|--------|---------|------------------| -| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | -| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | -| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | -| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | -| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | -| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | -| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | -| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | -| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize | -| `prompt` | AI prompts, CSS keywords | (style name) | - -### Available Stacks - -| Stack | Focus | -|-------|-------| -| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) | -| `react` | State, hooks, performance, patterns | -| `nextjs` | SSR, routing, images, API routes | -| `vue` | Composition API, Pinia, Vue Router | -| `svelte` | Runes, stores, SvelteKit | -| `swiftui` | Views, State, Navigation, Animation | -| `react-native` | Components, Navigation, Lists | -| `flutter` | Widgets, State, Layout, Theming | -| `shadcn` | shadcn/ui components, theming, forms, patterns | -| `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition | - ---- - -## Example Workflow - -**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp" - -### Step 1: Analyze Requirements -- Product type: Beauty/Spa service -- Style keywords: elegant, professional, soft -- Industry: Beauty/Wellness -- Stack: html-tailwind (default) - -### Step 2: Generate Design System (REQUIRED) - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service elegant" --design-system -p "Serenity Spa" -``` - -**Output:** Complete design system with pattern, style, colors, typography, effects, and anti-patterns. - -### Step 3: Supplement with Detailed Searches (as needed) - -```bash -# Get UX guidelines for animation and accessibility -python3 skills/ui-ux-pro-max/scripts/search.py "animation accessibility" --domain ux - -# Get alternative typography options if needed -python3 skills/ui-ux-pro-max/scripts/search.py "elegant luxury serif" --domain typography -``` - -### Step 4: Stack Guidelines - -```bash -python3 skills/ui-ux-pro-max/scripts/search.py "layout responsive form" --stack html-tailwind -``` - -**Then:** Synthesize design system + detailed searches and implement the design. - ---- - -## Output Formats - -The `--design-system` flag supports two output formats: - -```bash -# ASCII box (default) - best for terminal display -python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system - -# Markdown - best for documentation -python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown -``` - ---- - -## Tips for Better Results - -1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app" -2. **Search multiple times** - Different keywords reveal different insights -3. **Combine domains** - Style + Typography + Color = Complete design system -4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues -5. **Use stack flag** - Get implementation-specific best practices -6. **Iterate** - If first search doesn't match, try different keywords - ---- - -## Common Rules for Professional UI - -These are frequently overlooked issues that make UI look unprofessional: - -### Icons & Visual Elements - -| Rule | Do | Don't | -|------|----|----- | -| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons | -| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout | -| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths | -| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly | - -### Interaction & Cursor - -| Rule | Do | Don't | -|------|----|----- | -| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements | -| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive | -| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) | - -### Light/Dark Mode Contrast - -| Rule | Do | Don't | -|------|----|----- | -| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) | -| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text | -| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter | -| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) | - -### Layout & Spacing - -| Rule | Do | Don't | -|------|----|----- | -| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` | -| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements | -| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths | - ---- - -## Pre-Delivery Checklist - -Before delivering UI code, verify these items: - -### Visual Quality -- [ ] No emojis used as icons (use SVG instead) -- [ ] All icons from consistent icon set (Heroicons/Lucide) -- [ ] Brand logos are correct (verified from Simple Icons) -- [ ] Hover states don't cause layout shift -- [ ] Use theme colors directly (bg-primary) not var() wrapper - -### Interaction -- [ ] All clickable elements have `cursor-pointer` -- [ ] Hover states provide clear visual feedback -- [ ] Transitions are smooth (150-300ms) -- [ ] Focus states visible for keyboard navigation - -### Light/Dark Mode -- [ ] Light mode text has sufficient contrast (4.5:1 minimum) -- [ ] Glass/transparent elements visible in light mode -- [ ] Borders visible in both modes -- [ ] Test both modes before delivery - -### Layout -- [ ] Floating elements have proper spacing from edges -- [ ] No content hidden behind fixed navbars -- [ ] Responsive at 375px, 768px, 1024px, 1440px -- [ ] No horizontal scroll on mobile - -### Accessibility -- [ ] All images have alt text -- [ ] Form inputs have labels -- [ ] Color is not the only indicator -- [ ] `prefers-reduced-motion` respected diff --git a/.windsurf/skills/ui-ux-pro-max/data b/.windsurf/skills/ui-ux-pro-max/data deleted file mode 100644 index e5b9469..0000000 --- a/.windsurf/skills/ui-ux-pro-max/data +++ /dev/null @@ -1 +0,0 @@ -../../../src/ui-ux-pro-max/data \ No newline at end of file diff --git a/.windsurf/skills/ui-ux-pro-max/scripts b/.windsurf/skills/ui-ux-pro-max/scripts deleted file mode 100644 index ccb93f7..0000000 --- a/.windsurf/skills/ui-ux-pro-max/scripts +++ /dev/null @@ -1 +0,0 @@ -../../../src/ui-ux-pro-max/scripts \ No newline at end of file diff --git a/.windsurf/skills/vercel-react-best-practices/AGENTS.md b/.windsurf/skills/vercel-react-best-practices/AGENTS.md deleted file mode 100644 index db951ab..0000000 --- a/.windsurf/skills/vercel-react-best-practices/AGENTS.md +++ /dev/null @@ -1,2934 +0,0 @@ -# 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. 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 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes) - - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props) - - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching) - - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries) - - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition) - - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache) - - 3.7 [Use after() for Non-Blocking Operations](#37-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 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering) - - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point) - - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo) - - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant) - - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components) - - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies) - - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers) - - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state) - - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates) - - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization) - - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates) - - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values) -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 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches) - - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide) - - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering) - - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states) -7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** - - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing) - - 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 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount) - - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs) - - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-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) - } -}) -``` - -**Alternative without extra dependencies:** - -```typescript -const userPromise = fetchUser() -const profilePromise = userPromise.then(user => fetchProfile(user.id)) - -const [user, config, profile] = await Promise.all([ - userPromise, - fetchConfig(), - profilePromise -]) -``` - -We can also create all the promises first, and do `Promise.all()` at the end. - -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 Authenticate Server Actions Like API Routes - -**Impact: CRITICAL (prevents unauthorized access to server mutations)** - -Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly. - -Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation." - -**Incorrect: no authentication check** - -```typescript -'use server' - -export async function deleteUser(userId: string) { - // Anyone can call this! No auth check - await db.user.delete({ where: { id: userId } }) - return { success: true } -} -``` - -**Correct: authentication inside the action** - -```typescript -'use server' - -import { verifySession } from '@/lib/auth' -import { unauthorized } from '@/lib/errors' - -export async function deleteUser(userId: string) { - // Always check auth inside the action - const session = await verifySession() - - if (!session) { - throw unauthorized('Must be logged in') - } - - // Check authorization too - if (session.user.role !== 'admin' && session.user.id !== userId) { - throw unauthorized('Cannot delete other users') - } - - await db.user.delete({ where: { id: userId } }) - return { success: true } -} -``` - -**With input validation:** - -```typescript -'use server' - -import { verifySession } from '@/lib/auth' -import { z } from 'zod' - -const updateProfileSchema = z.object({ - userId: z.string().uuid(), - name: z.string().min(1).max(100), - email: z.string().email() -}) - -export async function updateProfile(data: unknown) { - // Validate input first - const validated = updateProfileSchema.parse(data) - - // Then authenticate - const session = await verifySession() - if (!session) { - throw new Error('Unauthorized') - } - - // Then authorize - if (session.user.id !== validated.userId) { - throw new Error('Can only update own profile') - } - - // Finally perform the mutation - await db.user.update({ - where: { id: validated.userId }, - data: { - name: validated.name, - email: validated.email - } - }) - - return { success: true } -} -``` - -Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication) - -### 3.2 Avoid Duplicate Serialization in RSC Props - -**Impact: LOW (reduces network payload by avoiding duplicate serialization)** - -RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server. - -**Incorrect: duplicates array** - -```tsx -// RSC: sends 6 strings (2 arrays × 3 items) - -``` - -**Correct: sends 3 strings** - -```tsx -// RSC: send once - - -// Client: transform there -'use client' -const sorted = useMemo(() => [...usernames].sort(), [usernames]) -``` - -**Nested deduplication behavior:** - -```tsx -// string[] - duplicates everything -usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings - -// object[] - duplicates array structure only -users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4) -``` - -Deduplication works recursively. Impact varies by data type: - -- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated - -- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference - -**Operations breaking deduplication: create new references** - -- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]` - -- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())` - -**More examples:** - -```tsx -// ❌ Bad - u.active)} /> - - -// ✅ Good - - -// Do filtering/destructuring in client -``` - -**Exception:** Pass derived data when transformation is expensive or client doesn't need original. - -### 3.3 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.4 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.5 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.6 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.7 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 Calculate Derived State During Rendering - -**Impact: MEDIUM (avoids redundant renders and state drift)** - -If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead. - -**Incorrect: redundant state and effect** - -```tsx -function Form() { - const [firstName, setFirstName] = useState('First') - const [lastName, setLastName] = useState('Last') - const [fullName, setFullName] = useState('') - - useEffect(() => { - setFullName(firstName + ' ' + lastName) - }, [firstName, lastName]) - - return

{fullName}

-} -``` - -**Correct: derive during render** - -```tsx -function Form() { - const [firstName, setFirstName] = useState('First') - const [lastName, setLastName] = useState('Last') - const fullName = firstName + ' ' + lastName - - return

{fullName}

-} -``` - -Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect) - -### 5.2 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.3 Do not wrap a simple expression with a primitive result type in useMemo - -**Impact: LOW-MEDIUM (wasted computation on every render)** - -When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`. - -Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself. - -**Incorrect:** - -```tsx -function Header({ user, notifications }: Props) { - const isLoading = useMemo(() => { - return user.isLoading || notifications.isLoading - }, [user.isLoading, notifications.isLoading]) - - if (isLoading) return - // return some markup -} -``` - -**Correct:** - -```tsx -function Header({ user, notifications }: Props) { - const isLoading = user.isLoading || notifications.isLoading - - if (isLoading) return - // return some markup -} -``` - -### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant - -**Impact: MEDIUM (restores memoization by using a constant for default value)** - -When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`. - -To address this issue, extract the default value into a constant. - -**Incorrect: `onClick` has different values on every rerender** - -```tsx -const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) { - // ... -}) - -// Used without optional onClick - -``` - -**Correct: stable default value** - -```tsx -const NOOP = () => {}; - -const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) { - // ... -}) - -// Used without optional onClick - -``` - -### 5.5 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.6 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.7 Put Interaction Logic in Event Handlers - -**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)** - -If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action. - -**Incorrect: event modeled as state + effect** - -```tsx -function Form() { - const [submitted, setSubmitted] = useState(false) - const theme = useContext(ThemeContext) - - useEffect(() => { - if (submitted) { - post('/api/register') - showToast('Registered', theme) - } - }, [submitted, theme]) - - return -} -``` - -**Correct: do it in the handler** - -```tsx -function Form() { - const theme = useContext(ThemeContext) - - function handleSubmit() { - post('/api/register') - showToast('Registered', theme) - } - - return -} -``` - -Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler) - -### 5.8 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