Compare commits

..

1 Commits

Author SHA1 Message Date
ea37904c75 refactor: unify structured output with pydantic model
Signed-off-by: Stream <Stream_2@qq.com>
2026-01-21 20:01:52 +08:00
1796 changed files with 24594 additions and 118762 deletions

1
.agent/skills Symbolic link
View File

@ -0,0 +1 @@
../.claude/skills

View File

@ -1 +0,0 @@
../../.agents/skills/component-refactoring

View File

@ -1 +0,0 @@
../../.agents/skills/frontend-code-review

View File

@ -1 +0,0 @@
../../.agents/skills/frontend-testing

View File

@ -1 +0,0 @@
../../.agents/skills/orpc-contract-first

View File

@ -1 +0,0 @@
../../.agents/skills/skill-creator

View File

@ -1 +0,0 @@
../../.agents/skills/vercel-react-best-practices

View File

@ -1 +0,0 @@
../../.agents/skills/web-design-guidelines

View File

@ -1,42 +0,0 @@
---
title: Initialize App Once, Not Per Mount
impact: LOW-MEDIUM
impactDescription: avoids duplicate init in development
tags: initialization, useEffect, app-startup, side-effects
---
## Initialize App Once, Not Per Mount
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect (runs twice in dev, re-runs on remount):**
```tsx
function Comp() {
useEffect(() => {
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
**Correct (once per app load):**
```tsx
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)

View File

@ -1,107 +0,0 @@
---
title: Avoid Layout Thrashing
impact: MEDIUM
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
tags: javascript, dom, css, performance, reflow, layout-thrashing
---
## Avoid Layout Thrashing
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
**This is OK (browser batches style changes):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
}
```
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function layoutThrashing(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
}
```
**Correct (batch reads, then writes):**
```typescript
function avoidThrashing(element: HTMLElement) {
// Read phase - all layout queries first
const rect1 = element.getBoundingClientRect()
const offsetWidth = element.offsetWidth
const offsetHeight = element.offsetHeight
// Write phase - all style changes after
element.style.width = '100px'
element.style.height = '200px'
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
const { width, height } = element.getBoundingClientRect()
}
```
**React example:**
```tsx
// Incorrect: interleaving style changes with layout queries
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
const width = ref.current.offsetWidth // Forces layout
ref.current.style.height = '200px'
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
}
```
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.

View File

@ -1,30 +0,0 @@
---
title: Suppress Expected Hydration Mismatches
impact: LOW-MEDIUM
impactDescription: avoids noisy hydration warnings for known differences
tags: rendering, hydration, ssr, nextjs
---
## Suppress Expected Hydration Mismatches
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Dont overuse it.
**Incorrect (known mismatch warnings):**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>
}
```
**Correct (suppress expected mismatch only):**
```tsx
function Timestamp() {
return (
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
)
}
```

View File

@ -1,75 +0,0 @@
---
title: Use useTransition Over Manual Loading States
impact: LOW
impactDescription: reduces re-renders and improves code clarity
tags: rendering, transitions, useTransition, loading, state
---
## Use useTransition Over Manual Loading States
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
**Incorrect (manual loading state):**
```tsx
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const handleSearch = async (value: string) => {
setIsLoading(true)
setQuery(value)
const data = await fetchResults(value)
setResults(data)
setIsLoading(false)
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Correct (useTransition with built-in pending state):**
```tsx
import { useTransition, useState } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value)
setResults(data)
})
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Benefits:**
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
- **Error resilience**: Pending state correctly resets even if the transition throws
- **Better responsiveness**: Keeps the UI responsive during updates
- **Interrupt handling**: New transitions automatically cancel pending ones
Reference: [useTransition](https://react.dev/reference/react/useTransition)

View File

@ -1,40 +0,0 @@
---
title: Calculate Derived State During Rendering
impact: MEDIUM
impactDescription: avoids redundant renders and state drift
tags: rerender, derived-state, useEffect, state
---
## Calculate Derived State During Rendering
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 <p>{fullName}</p>
}
```
**Correct (derive during render):**
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const fullName = firstName + ' ' + lastName
return <p>{fullName}</p>
}
```
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)

View File

@ -1,38 +0,0 @@
---
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
impact: MEDIUM
impactDescription: restores memoization by using a constant for default value
tags: rerender, memo, optimization
---
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
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
<UserAvatar />
```
**Correct (stable default value):**
```tsx
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```

View File

@ -1,45 +0,0 @@
---
title: Put Interaction Logic in Event Handlers
impact: MEDIUM
impactDescription: avoids effect re-runs and duplicate side effects
tags: rerender, useEffect, events, side-effects, dependencies
---
## Put Interaction Logic in Event Handlers
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 <button onClick={() => setSubmitted(true)}>Submit</button>
}
```
**Correct (do it in the handler):**
```tsx
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}
```
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)

View File

@ -1,35 +0,0 @@
---
title: Do not wrap a simple expression with a primitive result type in useMemo
impact: LOW-MEDIUM
impactDescription: wasted computation on every render
tags: rerender, useMemo, optimization
---
## Do not wrap a simple expression with a primitive result type in useMemo
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 <Skeleton />
// return some markup
}
```
**Correct:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading
if (isLoading) return <Skeleton />
// return some markup
}
```

View File

@ -1,73 +0,0 @@
---
title: Use useRef for Transient Values
impact: MEDIUM
impactDescription: avoids unnecessary re-renders on frequent updates
tags: rerender, useref, state, performance
---
## Use useRef for Transient Values
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
**Incorrect (renders every update):**
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0)
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX)
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
style={{
position: 'fixed',
top: 0,
left: lastX,
width: 8,
height: 8,
background: 'black',
}}
/>
)
}
```
**Correct (no re-render for tracking):**
```tsx
function Tracker() {
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX
const node = dotRef.current
if (node) {
node.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
ref={dotRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: 8,
height: 8,
background: 'black',
transform: 'translateX(0px)',
}}
/>
)
}
```

View File

@ -1,96 +0,0 @@
---
title: Authenticate Server Actions Like API Routes
impact: CRITICAL
impactDescription: prevents unauthorized access to server mutations
tags: server, server-actions, authentication, security, authorization
---
## 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)

View File

@ -1,65 +0,0 @@
---
title: Avoid Duplicate Serialization in RSC Props
impact: LOW
impactDescription: reduces network payload by avoiding duplicate serialization
tags: server, rsc, serialization, props, client-components
---
## 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)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
```
**Correct (sends 3 strings):**
```tsx
// RSC: send once
<ClientList usernames={usernames} />
// Client: transform there
'use client'
const sorted = useMemo(() => [...usernames].sort(), [usernames])
```
**Nested deduplication behavior:**
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
```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)
```
**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
<C users={users} active={users.filter(u => u.active)} />
<C product={product} productName={product.name} />
// ✅ Good
<C users={users} />
<C product={product} />
// Do filtering/destructuring in client
```
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.

View File

@ -1,39 +0,0 @@
---
name: web-design-guidelines
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
metadata:
author: vercel
version: "1.0.0"
argument-hint: <file-or-pattern>
---
# Web Interface Guidelines
Review files for compliance with Web Interface Guidelines.
## How It Works
1. Fetch the latest guidelines from the source URL below
2. Read the specified files (or prompt user for files/pattern)
3. Check against all rules in the fetched guidelines
4. Output findings in the terse `file:line` format
## Guidelines Source
Fetch fresh guidelines before each review:
```
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
```
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines
4. Output findings using the format specified in the guidelines
If no files specified, ask the user which files to review.

View File

@ -1 +0,0 @@
../../.agents/skills/component-refactoring

View File

@ -1 +0,0 @@
../../.agents/skills/frontend-code-review

View File

@ -1 +0,0 @@
../../.agents/skills/frontend-testing

View File

@ -1 +0,0 @@
../../.agents/skills/orpc-contract-first

View File

@ -1 +0,0 @@
../../.agents/skills/skill-creator

View File

@ -4,6 +4,7 @@ Quick validation script for skills - minimal version
""" """
import sys import sys
import os
import re import re
import yaml import yaml
from pathlib import Path from pathlib import Path

View File

@ -1 +0,0 @@
../../.agents/skills/vercel-react-best-practices

View File

@ -33,43 +33,34 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) - 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) - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
3. [Server-Side Performance](#3-server-side-performance) — **HIGH** 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.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching)
- 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props) - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries)
- 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching) - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition)
- 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries) - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache)
- 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition) - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations)
- 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. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
- 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) - 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.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.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) - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** 5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
- 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering) - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)
- 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point) - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components)
- 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.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies)
- 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.4 [Subscribe to Derived State](#54-subscribe-to-derived-state)
- 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components) - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates)
- 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies) - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization)
- 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers) - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates)
- 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. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
- 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) - 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.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.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
- 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
- 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) - 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.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)
- 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide) - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)
- 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. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
- 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing) - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes)
- 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) - 7.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.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
- 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
@ -82,9 +73,8 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) - 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) - 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. [Advanced Patterns](#8-advanced-patterns) — **LOW**
- 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount) - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)
- 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs) - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs)
- 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
--- ---
@ -200,21 +190,6 @@ const { user, config, profile } = await all({
}) })
``` ```
**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) Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
### 1.3 Prevent Waterfall Chains in API Routes ### 1.3 Prevent Waterfall Chains in API Routes
@ -593,158 +568,7 @@ The `typeof window !== 'undefined'` check prevents bundling preloaded modules fo
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
### 3.1 Authenticate Server Actions Like API Routes ### 3.1 Cross-Request LRU Caching
**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)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
```
**Correct: sends 3 strings**
```tsx
// RSC: send once
<ClientList usernames={usernames} />
// 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
<C users={users} active={users.filter(u => u.active)} />
<C product={product} productName={product.name} />
// ✅ Good
<C users={users} />
<C product={product} />
// 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)** **Impact: HIGH (caches across requests)**
@ -781,7 +605,7 @@ Use when sequential user actions hit multiple endpoints needing the same data wi
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
### 3.4 Minimize Serialization at RSC Boundaries ### 3.2 Minimize Serialization at RSC Boundaries
**Impact: HIGH (reduces data transfer size)** **Impact: HIGH (reduces data transfer size)**
@ -815,7 +639,7 @@ function Profile({ name }: { name: string }) {
} }
``` ```
### 3.5 Parallel Data Fetching with Component Composition ### 3.3 Parallel Data Fetching with Component Composition
**Impact: CRITICAL (eliminates server-side waterfalls)** **Impact: CRITICAL (eliminates server-side waterfalls)**
@ -894,7 +718,7 @@ export default function Page() {
} }
``` ```
### 3.6 Per-Request Deduplication with React.cache() ### 3.4 Per-Request Deduplication with React.cache()
**Impact: MEDIUM (deduplicates within request)** **Impact: MEDIUM (deduplicates within request)**
@ -960,7 +784,7 @@ Use `React.cache()` to deduplicate these operations across your component tree.
Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
### 3.7 Use after() for Non-Blocking Operations ### 3.5 Use after() for Non-Blocking Operations
**Impact: MEDIUM (faster response times)** **Impact: MEDIUM (faster response times)**
@ -1283,43 +1107,7 @@ function cachePrefs(user: FullUser) {
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
### 5.1 Calculate Derived State During Rendering ### 5.1 Defer State Reads to Usage Point
**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 <p>{fullName}</p>
}
```
**Correct: derive during render**
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const fullName = firstName + ' ' + lastName
return <p>{fullName}</p>
}
```
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)** **Impact: MEDIUM (avoids unnecessary subscriptions)**
@ -1354,71 +1142,7 @@ function ShareButton({ chatId }: { chatId: string }) {
} }
``` ```
### 5.3 Do not wrap a simple expression with a primitive result type in useMemo ### 5.2 Extract to Memoized Components
**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 <Skeleton />
// return some markup
}
```
**Correct:**
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading
if (isLoading) return <Skeleton />
// 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
<UserAvatar />
```
**Correct: stable default value**
```tsx
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
})
// Used without optional onClick
<UserAvatar />
```
### 5.5 Extract to Memoized Components
**Impact: MEDIUM (enables early returns)** **Impact: MEDIUM (enables early returns)**
@ -1458,7 +1182,7 @@ function Profile({ user, loading }: Props) {
**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. **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 ### 5.3 Narrow Effect Dependencies
**Impact: LOW (minimizes effect re-runs)** **Impact: LOW (minimizes effect re-runs)**
@ -1499,48 +1223,7 @@ useEffect(() => {
}, [isMobile]) }, [isMobile])
``` ```
### 5.7 Put Interaction Logic in Event Handlers ### 5.4 Subscribe to Derived State
**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 <button onClick={() => setSubmitted(true)}>Submit</button>
}
```
**Correct: do it in the handler**
```tsx
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}
```
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)** **Impact: MEDIUM (reduces re-render frequency)**
@ -1565,7 +1248,7 @@ function Sidebar() {
} }
``` ```
### 5.9 Use Functional setState Updates ### 5.5 Use Functional setState Updates
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)** **Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
@ -1643,7 +1326,7 @@ function TodoList() {
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs. **Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
### 5.10 Use Lazy State Initialization ### 5.6 Use Lazy State Initialization
**Impact: MEDIUM (wasted computation on every render)** **Impact: MEDIUM (wasted computation on every render)**
@ -1697,7 +1380,7 @@ Use lazy initialization when computing initial values from localStorage/sessionS
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary. For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
### 5.11 Use Transitions for Non-Urgent Updates ### 5.7 Use Transitions for Non-Urgent Updates
**Impact: MEDIUM (maintains UI responsiveness)** **Impact: MEDIUM (maintains UI responsiveness)**
@ -1733,75 +1416,6 @@ function ScrollTracker() {
} }
``` ```
### 5.12 Use useRef for Transient Values
**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
**Incorrect: renders every update**
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0)
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX)
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
style={{
position: 'fixed',
top: 0,
left: lastX,
width: 8,
height: 8,
background: 'black',
}}
/>
)
}
```
**Correct: no re-render for tracking**
```tsx
function Tracker() {
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX
const node = dotRef.current
if (node) {
node.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
return (
<div
ref={dotRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: 8,
height: 8,
background: 'black',
transform: 'translateX(0px)',
}}
/>
)
}
```
--- ---
## 6. Rendering Performance ## 6. Rendering Performance
@ -2031,33 +1645,7 @@ The inline script executes synchronously before showing the element, ensuring th
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values. This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
### 6.6 Suppress Expected Hydration Mismatches ### 6.6 Use Activity Component for Show/Hide
**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Dont overuse it.
**Incorrect: known mismatch warnings**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>
}
```
**Correct: suppress expected mismatch only**
```tsx
function Timestamp() {
return (
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
)
}
```
### 6.7 Use Activity Component for Show/Hide
**Impact: MEDIUM (preserves state/DOM)** **Impact: MEDIUM (preserves state/DOM)**
@ -2079,7 +1667,7 @@ function Dropdown({ isOpen }: Props) {
Avoids expensive re-renders and state loss. Avoids expensive re-renders and state loss.
### 6.8 Use Explicit Conditional Rendering ### 6.7 Use Explicit Conditional Rendering
**Impact: LOW (prevents rendering 0 or NaN)** **Impact: LOW (prevents rendering 0 or NaN)**
@ -2115,80 +1703,6 @@ function Badge({ count }: { count: number }) {
// When count = 5, renders: <div><span class="badge">5</span></div> // When count = 5, renders: <div><span class="badge">5</span></div>
``` ```
### 6.9 Use useTransition Over Manual Loading States
**Impact: LOW (reduces re-renders and improves code clarity)**
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
**Incorrect: manual loading state**
```tsx
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const handleSearch = async (value: string) => {
setIsLoading(true)
setQuery(value)
const data = await fetchResults(value)
setResults(data)
setIsLoading(false)
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Correct: useTransition with built-in pending state**
```tsx
import { useTransition, useState } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value)
setResults(data)
})
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
}
```
**Benefits:**
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
- **Error resilience**: Pending state correctly resets even if the transition throws
- **Better responsiveness**: Keeps the UI responsive during updates
- **Interrupt handling**: New transitions automatically cancel pending ones
Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
--- ---
## 7. JavaScript Performance ## 7. JavaScript Performance
@ -2197,17 +1711,17 @@ Reference: [https://react.dev/reference/react/useTransition](https://react.dev/r
Micro-optimizations for hot paths can add up to meaningful improvements. Micro-optimizations for hot paths can add up to meaningful improvements.
### 7.1 Avoid Layout Thrashing ### 7.1 Batch DOM CSS Changes
**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)** **Impact: MEDIUM (reduces reflows/repaints)**
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow. Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
**This is OK: browser batches style changes** **Incorrect: multiple reflows**
```typescript ```typescript
function updateElementStyles(element: HTMLElement) { function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation // Each line triggers a reflow
element.style.width = '100px' element.style.width = '100px'
element.style.height = '200px' element.style.height = '200px'
element.style.backgroundColor = 'blue' element.style.backgroundColor = 'blue'
@ -2215,56 +1729,48 @@ function updateElementStyles(element: HTMLElement) {
} }
``` ```
**Incorrect: interleaved reads and writes force reflows** **Correct: add class - single reflow**
```typescript ```typescript
function layoutThrashing(element: HTMLElement) { // CSS file
element.style.width = '100px' .highlighted-box {
const width = element.offsetWidth // Forces reflow width: 100px;
element.style.height = '200px' height: 200px;
const height = element.offsetHeight // Forces another reflow background-color: blue;
border: 1px solid black;
} }
```
**Correct: batch writes, then read once** // JavaScript
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
}
```
**Correct: batch reads, then writes**
```typescript
function updateElementStyles(element: HTMLElement) { function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box') element.classList.add('highlighted-box')
const { width, height } = element.getBoundingClientRect()
} }
``` ```
**Better: use CSS classes** **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:** **React example:**
```tsx ```tsx
// Incorrect: interleaving style changes with layout queries // Incorrect: changing styles one by one
function Box({ isHighlighted }: { isHighlighted: boolean }) { function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
if (ref.current && isHighlighted) { if (ref.current && isHighlighted) {
ref.current.style.width = '100px' ref.current.style.width = '100px'
const width = ref.current.offsetWidth // Forces layout
ref.current.style.height = '200px' ref.current.style.height = '200px'
ref.current.style.backgroundColor = 'blue'
} }
}, [isHighlighted]) }, [isHighlighted])
@ -2281,9 +1787,7 @@ function Box({ isHighlighted }: { isHighlighted: boolean }) {
} }
``` ```
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain. Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
### 7.2 Build Index Maps for Repeated Lookups ### 7.2 Build Index Maps for Repeated Lookups
@ -2540,7 +2044,7 @@ function hasChanges(current: string[], original: string[]) {
if (current.length !== original.length) { if (current.length !== original.length) {
return true return true
} }
// Only sort when lengths match // Only sort/join when lengths match
const currentSorted = current.toSorted() const currentSorted = current.toSorted()
const originalSorted = original.toSorted() const originalSorted = original.toSorted()
for (let i = 0; i < currentSorted.length; i++) { for (let i = 0; i < currentSorted.length; i++) {
@ -2725,7 +2229,7 @@ const min = Math.min(...numbers)
const max = Math.max(...numbers) const max = Math.max(...numbers)
``` ```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability. This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
### 7.11 Use Set/Map for O(1) Lookups ### 7.11 Use Set/Map for O(1) Lookups
@ -2812,45 +2316,7 @@ const sorted = [...items].sort((a, b) => a.value - b.value)
Advanced patterns for specific cases that require careful implementation. Advanced patterns for specific cases that require careful implementation.
### 8.1 Initialize App Once, Not Per Mount ### 8.1 Store Event Handlers in Refs
**Impact: LOW-MEDIUM (avoids duplicate init in development)**
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
**Incorrect: runs twice in dev, re-runs on remount**
```tsx
function Comp() {
useEffect(() => {
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
**Correct: once per app load**
```tsx
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
// ...
}
```
Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
### 8.2 Store Event Handlers in Refs
**Impact: LOW (stable subscriptions)** **Impact: LOW (stable subscriptions)**
@ -2859,7 +2325,7 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
**Incorrect: re-subscribes on every render** **Incorrect: re-subscribes on every render**
```tsx ```tsx
function useWindowEvent(event: string, handler: (e) => void) { function useWindowEvent(event: string, handler: () => void) {
useEffect(() => { useEffect(() => {
window.addEventListener(event, handler) window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler) return () => window.removeEventListener(event, handler)
@ -2872,7 +2338,7 @@ function useWindowEvent(event: string, handler: (e) => void) {
```tsx ```tsx
import { useEffectEvent } from 'react' import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) { function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler) const onEvent = useEffectEvent(handler)
useEffect(() => { useEffect(() => {
@ -2886,12 +2352,24 @@ function useWindowEvent(event: string, handler: (e) => void) {
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler. `useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
### 8.3 useEffectEvent for Stable Callback Refs ### 8.2 useLatest for Stable Callback Refs
**Impact: LOW (prevents effect re-runs)** **Impact: LOW (prevents effect re-runs)**
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Implementation:**
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref
}
```
**Incorrect: effect re-runs on every callback change** **Incorrect: effect re-runs on every callback change**
```tsx ```tsx
@ -2905,17 +2383,15 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
} }
``` ```
**Correct: using React's useEffectEvent** **Correct: stable effect, fresh callback**
```tsx ```tsx
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch) const onSearchRef = useLatest(onSearch)
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300) const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout) return () => clearTimeout(timeout)
}, [query]) }, [query])
} }

View File

@ -9,7 +9,7 @@ metadata:
# Vercel React Best Practices # Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation. Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply ## When to Apply
@ -53,10 +53,8 @@ Reference these guidelines when:
### 3. Server-Side Performance (HIGH) ### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication - `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching - `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-serialization` - Minimize data passed to client components - `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches - `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-after-nonblocking` - Use after() for non-blocking operations - `server-after-nonblocking` - Use after() for non-blocking operations
@ -65,23 +63,16 @@ Reference these guidelines when:
- `client-swr-dedup` - Use SWR for automatic request deduplication - `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners - `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM) ### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks - `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components - `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects - `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values - `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks - `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values - `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates - `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
### 6. Rendering Performance (MEDIUM) ### 6. Rendering Performance (MEDIUM)
@ -90,10 +81,8 @@ Reference these guidelines when:
- `rendering-hoist-jsx` - Extract static JSX outside components - `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision - `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data - `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide - `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals - `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
### 7. JavaScript Performance (LOW-MEDIUM) ### 7. JavaScript Performance (LOW-MEDIUM)
@ -113,7 +102,6 @@ Reference these guidelines when:
### 8. Advanced Patterns (LOW) ### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs - `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs - `advanced-use-latest` - useLatest for stable callback refs
## How to Use ## How to Use
@ -123,6 +111,7 @@ Read individual rule files for detailed explanations and code examples:
``` ```
rules/async-parallel.md rules/async-parallel.md
rules/bundle-barrel-imports.md rules/bundle-barrel-imports.md
rules/_sections.md
``` ```
Each rule file contains: Each rule file contains:

View File

@ -1,14 +1,26 @@
--- ---
title: useEffectEvent for Stable Callback Refs title: useLatest for Stable Callback Refs
impact: LOW impact: LOW
impactDescription: prevents effect re-runs impactDescription: prevents effect re-runs
tags: advanced, hooks, useEffectEvent, refs, optimization tags: advanced, hooks, useLatest, refs, optimization
--- ---
## useEffectEvent for Stable Callback Refs ## useLatest for Stable Callback Refs
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
**Implementation:**
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
useLayoutEffect(() => {
ref.current = value
}, [value])
return ref
}
```
**Incorrect (effect re-runs on every callback change):** **Incorrect (effect re-runs on every callback change):**
```tsx ```tsx
@ -22,17 +34,15 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
} }
``` ```
**Correct (using React's useEffectEvent):** **Correct (stable effect, fresh callback):**
```tsx ```tsx
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch) const onSearchRef = useLatest(onSearch)
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300) const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout) return () => clearTimeout(timeout)
}, [query]) }, [query])
} }

View File

@ -33,19 +33,4 @@ const { user, config, profile } = await all({
}) })
``` ```
**Alternative without extra dependencies:**
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise
])
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)

View File

@ -0,0 +1,57 @@
---
title: Batch DOM CSS Changes
impact: MEDIUM
impactDescription: reduces reflows/repaints
tags: javascript, dom, css, performance, reflow
---
## Batch DOM CSS Changes
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
height: 200px;
background-color: blue;
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
const { width, height } = element.getBoundingClientRect()
}
```
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.

Some files were not shown because too many files have changed in this diff Show More