React 最佳实践(中文整理版)
基于 Vercel Engineering 2026年1月发布的 React Best Practices v1.0.0 原文共 8 大类 40+ 条规则,按影响程度从关键到渐进排列 原文仓库:vercel-labs/agent-skills
目录
| 类别 | 影响等级 | 规则数 |
|---|---|---|
| 1. 消除瀑布流请求 | 关键 | 5 |
| 2. 包体积优化 | 关键 | 5 |
| 3. 服务端性能 | 高 | 7 |
| 4. 客户端数据获取 | 中高 | 4 |
| 5. 重渲染优化 | 中 | 12 |
| 6. 渲染性能 | 中 | 9 |
| 7. JavaScript 性能 | 低-中 | 12 |
| 8. 高级模式 | 低 | 3 |
1. 消除瀑布流请求
影响:关键 — 瀑布流是性能的头号杀手。每个顺序 await 都会增加完整的网络延迟。消除它们能带来最大收益。
1.1 延迟 await 到真正需要时
影响:高(避免阻塞未使用的代码路径)
将 await 操作移到实际使用的分支中,避免阻塞不需要数据的代码路径。尤其在被跳过的分支频繁执行、或延迟的操作本身很昂贵时,该优化价值最高。
原文: async-defer-await.md — Defer Await Until Needed
// 错误:两个分支都被阻塞
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
return { skipped: true } // 已经等待了 userData
}
return processUserData(userData)
}
// 正确:只在需要时阻塞
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
return { skipped: true } // 立即返回
}
const userData = await fetchUserData(userId)
return processUserData(userData)
}
1.2 基于依赖关系的并行化
影响:关键(2-10 倍提升)
对于具有部分依赖的操作,使用 better-all 或手动构建 Promise 链来最大化并行度,让每个任务在最早可能的时刻启动。
原文: async-dependencies.md — Dependency-Based Parallelization
// 错误:profile 不必要地等待 config
const [user, config] = await Promise.all([fetchUser(), fetchConfig()])
const profile = await fetchProfile(user.id)
// 正确:config 和 profile 并行运行
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const [user, config, profile] = await Promise.all([
userPromise, fetchConfig(), profilePromise
])
1.3 防止 API 路由中的瀑布链
影响:关键(2-10 倍提升)
在 API 路由和 Server Actions 中,立即启动独立操作,即使暂时不 await 它们。
原文: async-api-routes.md — Prevent Waterfall Chains in API Routes
// 错误:config 等待 auth,data 等待两者
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 })
}
// 正确:auth 和 config 立即启动
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 })
}
1.4 对独立操作使用 Promise.all()
影响:关键(2-10 倍提升)
当异步操作之间没有依赖关系时,使用 Promise.all() 并发执行。
原文: async-parallel.md — Promise.all() for Independent Operations
// 错误:顺序执行,3 次往返
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// 正确:并行执行,1 次往返
const [user, posts, comments] = await Promise.all([
fetchUser(), fetchPosts(), fetchComments()
])
1.5 策略性地使用 Suspense 边界
影响:高(更快的首次绘制)
不要在异步组件中 await 数据后再返回 JSX,而是使用 Suspense 边界在数据加载时先展示包裹 UI。Sidebar、Header、Footer 立即渲染,仅 DataDisplay 等待数据。
原文: async-suspense-boundaries.md — Strategic Suspense Boundaries
// 正确:包裹 UI 立即显示,数据流式注入
function Page() {
return (
<div>
<div>Sidebar</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // 仅阻塞此组件
return <div>{data.content}</div>
}
不适用场景: 布局决策所需的关键数据、首屏 SEO 关键内容、快速的小查询、需要避免布局偏移时。
2. 包体积优化
影响:关键 — 减小初始包体积可改善 TTI(可交互时间)和 LCP(最大内容绘制)。
2.1 避免桶文件导入
影响:关键(200-800ms 导入开销,构建变慢)
直接从源文件导入,而非桶文件(barrel files),避免加载数千个未使用的模块。桶文件是 re-export 多个模块的入口文件(如 index.js 中的 export * from './module')。
原文: bundle-barrel-imports.md — Avoid Barrel File Imports
// 错误:导入整个库
import { Check, X, Menu } from 'lucide-react' // 加载 1,583 个模块
import { Button, TextField } from '@mui/material' // 加载 2,225 个模块
// 正确:仅导入需要的
import Check from 'lucide-react/dist/esm/icons/check'
import Button from '@mui/material/Button'
Next.js 13.5+ 替代方案: 在 next.config.js 中配置 optimizePackageImports 自动转换。
常受影响的库: lucide-react、@mui/material、@tabler/icons-react、react-icons、lodash、date-fns、rxjs 等。
2.2 条件模块加载
影响:高(仅在需要时加载大型数据)
仅在功能激活时才加载大型数据或模块。typeof window !== 'undefined' 检查可防止 SSR 时打包该模块。
原文: bundle-conditional.md — Conditional Module Loading
function AnimationPlayer({ enabled, setEnabled }) {
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])
// ...
}
2.3 延迟加载非关键第三方库
影响:中(水合后加载)
分析、日志、错误追踪等不阻塞用户交互,应在水合后再加载。
原文: bundle-defer-third-party.md — Defer Non-Critical Third-Party Libraries
// 错误:阻塞初始 bundle
import { Analytics } from '@vercel/analytics/react'
// 正确:水合后加载
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
2.4 重型组件动态导入
影响:关键(直接影响 TTI 和 LCP)
使用 next/dynamic 懒加载初始渲染不需要的大型组件。
原文: bundle-dynamic-imports.md — Dynamic Imports for Heavy Components
// 错误:Monaco 打包进主 chunk ~300KB
import { MonacoEditor } from './monaco-editor'
// 正确:按需加载
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
2.5 基于用户意图预加载
影响:中(减少感知延迟)
在需要之前预加载重型 bundle,降低感知延迟。
原文: bundle-preload.md — Preload Based on User Intent
function EditorButton({ onClick }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
}
3. 服务端性能
影响:高 — 优化服务端渲染和数据获取,消除服务端瀑布流,减少响应时间。
3.1 像 API 路由一样认证 Server Actions
影响:关键(防止未授权访问服务端变更)
Server Actions(带 "use server" 的函数)作为公共端点暴露。必须在每个 Server Action 内部验证身份和授权,不要仅依赖中间件或页面级检查。
原文: server-auth-actions.md — Authenticate Server Actions Like API Routes
'use server'
export async function deleteUser(userId: string) {
const session = await verifySession()
if (!session) throw unauthorized('Must be logged in')
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 }
}
3.2 避免 RSC Props 中的重复序列化
影响:低(减少网络负载)
RSC→Client 序列化按对象引用去重,而非按值。相同引用仅序列化一次;新引用则重复序列化。应在客户端做转换(.toSorted()、.filter()、.map())。
原文: server-dedup-props.md — Avoid Duplicate Serialization in RSC Props
// 错误:发送 6 个字符串
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
// 正确:发送 3 个字符串,客户端排序
<ClientList usernames={usernames} />
// Client: const sorted = useMemo(() => [...usernames].sort(), [usernames])
3.3 跨请求 LRU 缓存
影响:高(跨请求缓存)
React.cache() 仅在单个请求内有效。对于跨请求共享的数据,使用 LRU 缓存。搭配 Vercel Fluid Compute 时效果尤佳,多个并发请求可共享同一函数实例和缓存。
原文: server-cache-lru.md — Cross-Request LRU Caching
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({ max: 1000, ttl: 5 * 60 * 1000 })
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
}
3.4 最小化 RSC 边界的序列化数据
影响:高(减少数据传输体积)
React Server/Client 边界会将所有对象属性序列化为字符串嵌入 HTML。仅传递客户端实际使用的字段。
原文: server-serialization.md — Minimize Serialization at RSC Boundaries
// 错误:序列化 50 个字段
async function Page() {
const user = await fetchUser() // 50 个字段
return <Profile user={user} />
}
// 正确:仅序列化 1 个字段
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
}
3.5 通过组件组合实现并行数据获取
影响:关键(消除服务端瀑布流)
React Server Components 在树中顺序执行。通过组合重构来并行化数据获取。让父组件不做异步操作,将 fetch 分散到独立子组件。
原文: server-parallel-fetching.md — Parallel Data Fetching with Component Composition
// 错误:Sidebar 等待 Page 的 fetch 完成
export default async function Page() {
const header = await fetchHeader()
return <div><div>{header}</div><Sidebar /></div>
}
// 正确:两者同时获取
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return <div><Header /><Sidebar /></div>
}
3.6 使用 React.cache() 进行请求内去重
影响:中(请求内去重)
使用 React.cache() 进行服务端请求去重。身份验证和数据库查询最受益。注意避免传入内联对象作为参数(Object.is 浅比较)。
原文: server-cache-react.md — Per-Request Deduplication with React.cache()
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 } })
})
// 同一请求中多次调用仅执行一次查询
Next.js 注意: fetch API 已自动内置请求记忆化。React.cache() 仍用于数据库查询、重计算、认证检查等非 fetch 异步操作。
3.7 使用 after() 进行非阻塞操作
影响:中(更快的响应时间)
使用 Next.js 的 after() 将日志、分析等副作用安排在响应发送之后执行。
原文: server-after-nonblocking.md — Use after() for Non-Blocking Operations
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
after(async () => {
// 响应发送后再执行日志
logUserAction({ userAgent: request.headers.get('user-agent') })
})
return Response.json({ status: 'success' })
}
常见场景: 分析追踪、审计日志、发送通知、缓存失效、清理任务。
4. 客户端数据获取
影响:中高 — 自动去重和高效的数据获取模式减少冗余网络请求。
4.1 全局事件监听器去重
影响:低(N 个组件实例仅 1 个监听器)
使用 useSWRSubscription() 在组件实例间共享全局事件监听器,避免 N 个实例注册 N 个监听器。
原文: client-event-listeners.md — Deduplicate Global Event Listeners
4.2 使用被动事件监听器优化滚动性能
影响:中(消除事件监听器导致的滚动延迟)
为 touch 和 wheel 事件监听器添加 { passive: true },启用立即滚动。浏览器通常会等待监听器完成以检查是否调用了 preventDefault(),导致滚动延迟。
原文: client-passive-event-listeners.md — Use Passive Event Listeners for Scrolling Performance
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
何时使用: 追踪/分析、日志、不调用 preventDefault() 的监听器。
何时不用: 自定义滑动手势、缩放控制等需要 preventDefault() 的场景。
4.3 使用 SWR 实现自动去重
影响:中高(自动去重)
SWR 可跨组件实例实现请求去重、缓存和重验证。
原文: client-swr-dedup.md — Use SWR for Automatic Deduplication
// 错误:无去重,每个实例各自请求
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers) }, [])
}
// 正确:多个实例共享一个请求
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
4.4 版本化和最小化 localStorage 数据
影响:中(防止 schema 冲突,减少存储体积)
给 key 加版本前缀,仅存储需要的字段,始终用 try-catch 包裹(无痕浏览、配额超限时会抛错)。
原文: client-localstorage-schema.md — Version and Minimize localStorage Data
const VERSION = 'v2'
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
} catch {}
}
5. 重渲染优化
影响:中 — 减少不必要的重渲染,最小化浪费的计算并提升 UI 响应性。
5.1 在渲染期间计算派生状态
影响:中(避免冗余渲染和状态漂移)
如果一个值可从当前 props/state 计算得出,就不要存入 state 或在 effect 中更新。直接在渲染时推导。
原文: rerender-derived-state-no-effect.md — Calculate Derived State During Rendering
// 错误
const [fullName, setFullName] = useState('')
useEffect(() => { setFullName(firstName + ' ' + lastName) }, [firstName, lastName])
// 正确
const fullName = firstName + ' ' + lastName
5.2 将状态读取延迟到使用点
影响:中(避免不必要的订阅)
如果只在回调中读取动态状态(如 searchParams),不要订阅它,在需要时直接读取。
原文: rerender-defer-reads.md — Defer State Reads to Usage Point
// 错误:订阅所有 searchParams 变化
const searchParams = useSearchParams()
const handleShare = () => { const ref = searchParams.get('ref') }
// 正确:按需读取,无订阅
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
}
5.3 不要用 useMemo 包裹简单的原始类型表达式
影响:低-中(每次渲染浪费计算)
当表达式简单(少量逻辑或算术运算符)且结果是原始类型(boolean、number、string)时,不要用 useMemo 包裹。useMemo 的依赖比较开销可能比表达式本身还大。
原文: rerender-simple-expression-in-memo.md — Do not wrap a simple expression with a primitive result type in useMemo
// 错误
const isLoading = useMemo(() => user.isLoading || notifications.isLoading, [...])
// 正确
const isLoading = user.isLoading || notifications.isLoading
5.4 将 memo 组件的非原始默认参数值提取为常量
影响:中(恢复被破坏的 memo 化)
当 memo 化组件的可选参数有非原始类型默认值(数组、函数、对象)时,未传参会导致每次渲染创建新实例,破坏 memo() 的严格相等比较。应将默认值提取为常量。
原文: rerender-memo-with-default-value.md — Extract Default Non-primitive Parameter Value from Memoized Component to Constant
// 错误:每次渲染 onClick 值不同
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }) { ... })
// 正确:稳定的默认值
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }) { ... })
5.5 提取为 memo 化组件
影响:中(启用提前返回)
将昂贵的工作提取到 memo 化组件中,在计算之前即可提前返回。
原文: rerender-memo.md — Extract to Memoized Components
// 错误:loading 时仍计算 avatar
function Profile({ user, loading }) {
const avatar = useMemo(() => { ... }, [user])
if (loading) return <Skeleton />
return <div>{avatar}</div>
}
// 正确:loading 时跳过计算
const UserAvatar = memo(function UserAvatar({ user }) { ... })
function Profile({ user, loading }) {
if (loading) return <Skeleton />
return <div><UserAvatar user={user} /></div>
}
注意: 如果项目启用了 React Compiler,memo() 和 useMemo() 的手动记忆化是不必要的。
5.6 缩窄 Effect 依赖
影响:低(最小化 effect 重运行)
用原始值而非对象作为依赖,减少 effect 的重复执行。对派生状态先在 effect 外计算。
原文: rerender-dependencies.md — Narrow Effect Dependencies
// 错误:user 的任何字段变化都重新运行
useEffect(() => { console.log(user.id) }, [user])
// 正确:仅在 id 变化时重新运行
useEffect(() => { console.log(user.id) }, [user.id])
// 对派生状态,先在外部计算
const isMobile = width < 768
useEffect(() => { if (isMobile) enableMobileMode() }, [isMobile])
5.7 将交互逻辑放在事件处理函数中
影响:中(避免 effect 重运行和重复副作用)
如果副作用由特定用户操作触发,就在对应的事件处理函数中执行。不要将操作建模为 state + effect。
原文: rerender-move-effect-to-event.md — Put Interaction Logic in Event Handlers
// 错误:事件建模为 state + effect
const [submitted, setSubmitted] = useState(false)
useEffect(() => { if (submitted) post('/api/register') }, [submitted, theme])
// 正确:在事件处理函数中执行
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
5.8 订阅派生状态
影响:中(降低重渲染频率)
订阅派生的布尔状态而非连续值,减少重渲染频率。
原文: rerender-derived-state.md — Subscribe to Derived State
// 错误:每个像素变化都重渲染
const width = useWindowWidth()
const isMobile = width < 768
// 正确:仅布尔值变化时重渲染
const isMobile = useMediaQuery('(max-width: 767px)')
5.9 使用函数式 setState 更新
影响:中(防止闭包过期和不必要的回调重建)
基于当前状态更新时,使用 setState 的函数更新形式,防止闭包过期、消除不必要的依赖、创建稳定的回调引用。
原文: rerender-functional-setstate.md — Use Functional setState Updates
// 错误:需要 items 作为依赖,有闭包过期风险
const addItems = useCallback((newItems) => {
setItems([...items, ...newItems])
}, [items])
// 正确:稳定回调,无闭包过期
const addItems = useCallback((newItems) => {
setItems(curr => [...curr, ...newItems])
}, [])
5.10 使用惰性状态初始化
影响:中(避免每次渲染浪费计算)
给 useState 传入函数来处理昂贵的初始值。不用函数形式时,初始化器每次渲染都会执行。
原文: rerender-lazy-state-init.md — Use Lazy State Initialization
// 错误:每次渲染都执行 buildSearchIndex
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
// 正确:仅初始渲染执行一次
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
5.11 对非紧急更新使用 Transitions
影响:中(保持 UI 响应性)
将频繁的非紧急状态更新标记为 transition,保持 UI 响应性。
原文: rerender-transitions.md — Use Transitions for Non-Urgent Updates
import { startTransition } from 'react'
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
5.12 对瞬态值使用 useRef
影响:中(避免频繁更新导致的不必要重渲染)
当值频繁变化且不需要每次更新都触发重渲染时(如鼠标追踪),使用 useRef 替代 useState。通过 ref 直接操作 DOM 样式。
原文: rerender-use-ref-transient-values.md — Use useRef for Transient Values
// 错误:每次鼠标移动都重渲染
const [lastX, setLastX] = useState(0)
// 正确:无重渲染
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
// 在事件处理中直接操作 DOM:
// dotRef.current.style.transform = `translateX(${e.clientX}px)`
6. 渲染性能
影响:中 — 优化渲染过程,减少浏览器的工作量。
6.1 动画 SVG 包裹元素而非 SVG 本身
影响:低(启用硬件加速)
很多浏览器不对 SVG 元素的 CSS3 动画进行硬件加速。将 SVG 包裹在 <div> 中并动画化包裹器。
原文: rendering-animate-svg-wrapper.md — Animate SVG Wrapper Instead of SVG Element
// 错误:直接动画 SVG — 无硬件加速
<svg className="animate-spin"> ... </svg>
// 正确:动画包裹 div — 硬件加速
<div className="animate-spin"><svg> ... </svg></div>
6.2 使用 CSS content-visibility 优化长列表
影响:高(更快的初始渲染)
对列表项应用 content-visibility: auto 延迟屏幕外的渲染。1000 条消息时浏览器跳过约 990 个屏幕外项(10 倍更快的初始渲染)。
原文: rendering-content-visibility.md — CSS content-visibility for Long Lists
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
6.3 提升静态 JSX 元素
影响:低(避免重新创建)
将静态 JSX 提取到组件外部,避免每次渲染重新创建。对大型静态 SVG 尤其有效。
原文: rendering-hoist-jsx.md — Hoist Static JSX Elements
// 正确:复用同一元素
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />
function Container() {
return <div>{loading && loadingSkeleton}</div>
}
注意: React Compiler 会自动提升静态 JSX 元素。
6.4 优化 SVG 精度
影响:低(减少文件大小)
降低 SVG 坐标精度以减少文件体积,可使用 SVGO 自动化。
原文: rendering-svg-precision.md — Optimize SVG Precision
npx svgo --precision=1 --multipass icon.svg
6.5 防止水合不匹配且无闪烁
影响:中(避免视觉闪烁和水合错误)
当渲染内容依赖客户端存储(localStorage、cookies)时,注入一个同步脚本在 React 水合前更新 DOM,避免 SSR 报错和水合后闪烁。
原文: rendering-hydration-no-flicker.md — Prevent Hydration Mismatch Without Flickering
function ThemeWrapper({ children }) {
return (
<>
<div id="theme-wrapper">{children}</div>
<script dangerouslySetInnerHTML={{ __html: `
(function() {
try {
var theme = localStorage.getItem('theme') || 'light';
document.getElementById('theme-wrapper').className = theme;
} catch (e) {}
})();
`}} />
</>
)
}
6.6 抑制预期的水合不匹配警告
影响:低-中(避免已知差异的嘈杂警告)
对服务端和客户端已知会不同的值(随机 ID、日期、时区格式),使用 suppressHydrationWarning。不要用它来隐藏真正的 bug。
原文: rendering-hydration-suppress-warning.md — Suppress Expected Hydration Mismatches
<span suppressHydrationWarning>{new Date().toLocaleString()}</span>
6.7 使用 Activity 组件实现显示/隐藏
影响:中(保持状态/DOM)
使用 React 的 <Activity> 为频繁切换可见性的昂贵组件保持状态/DOM,避免昂贵的重渲染和状态丢失。
原文: rendering-activity.md — Use Activity Component for Show/Hide
import { Activity } from 'react'
function Dropdown({ isOpen }) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
}
6.8 使用显式条件渲染
影响:低(防止渲染 0 或 NaN)
当条件可能是 0、NaN 或其他 falsy 但会渲染的值时,使用显式三元运算符而非 &&。
原文: rendering-conditional-render.md — Use Explicit Conditional Rendering
// 错误:count 为 0 时渲染 "0"
{count && <span>{count}</span>}
// 正确:count 为 0 时不渲染
{count > 0 ? <span>{count}</span> : null}
6.9 使用 useTransition 替代手动 loading 状态
影响:低(减少重渲染,改善代码清晰度)
使用 useTransition 替代手动 useState 管理 loading 状态,提供内建的 isPending 并自动管理过渡。
原文: rendering-usetransition-loading.md — Use useTransition Over Manual Loading States
const [isPending, startTransition] = useTransition()
const handleSearch = (value) => {
setQuery(value)
startTransition(async () => {
const data = await fetchResults(value)
setResults(data)
})
}
优势: 自动 pending 状态、错误恢复、更好的响应性、自动取消上一次过渡。
7. JavaScript 性能
影响:低-中 — 热路径上的微优化积少成多,可带来有意义的提升。
7.1 避免布局抖动
影响:中(防止强制同步布局)
避免在样式写入和布局读取之间交错。当你在样式更改之间读取布局属性(如 offsetWidth、getBoundingClientRect())时,浏览器被迫触发同步回流。优先使用 CSS 类。
原文: js-batch-dom-css.md — Avoid Layout Thrashing
// 错误:读写交错强制回流
element.style.width = '100px'
const width = element.offsetWidth // 强制回流
element.style.height = '200px'
// 正确:批量写入后一次读取
element.style.width = '100px'
element.style.height = '200px'
const { width, height } = element.getBoundingClientRect()
7.2 构建索引 Map 进行重复查找
影响:低-中(100 万次操作降至 2000 次)
多次 .find() 调用用同一个 key 查找时,应使用 Map。构建一次 Map(O(n)),后续查找全部 O(1)。
原文: js-index-maps.md — Build Index Maps for Repeated Lookups
// 正确
const userById = new Map(users.map(u => [u.id, u]))
orders.map(order => ({ ...order, user: userById.get(order.userId) }))
7.3 循环中缓存属性访问
影响:低-中(减少查找次数)
在热路径中缓存对象属性查找。
原文: js-cache-property-access.md — Cache Property Access in Loops
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) { process(value) }
7.4 缓存重复的函数调用
影响:中(避免冗余计算)
使用模块级 Map 缓存相同输入的函数结果。用 Map 而非 Hook,使其在工具函数、事件处理中也可使用。
原文: js-cache-function-results.md — Cache Repeated Function Calls
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) return slugifyCache.get(text)!
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
7.5 缓存 Storage API 调用
影响:低-中(减少昂贵的 I/O)
localStorage、sessionStorage、document.cookie 是同步且昂贵的。将读取结果缓存在内存中,注意外部变更时失效缓存。
原文: js-cache-storage.md — Cache Storage API Calls
const storageCache = new Map<string, string | null>()
function getLocalStorage(key: string) {
if (!storageCache.has(key)) storageCache.set(key, localStorage.getItem(key))
return storageCache.get(key)
}
7.6 合并多次数组遍历
影响:低-中(减少遍历次数)
多次 .filter() 或 .map() 会多次遍历数组,合并为一个循环。
原文: js-combine-iterations.md — Combine Multiple Array Iterations
// 错误:3 次遍历
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
// 正确:1 次遍历
const admins: User[] = [], testers: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
}
7.7 数组比较前先检查长度
影响:中-高(长度不同时避免昂贵操作)
在排序、深比较等昂贵操作前,先检查数组长度。长度不同则直接返回。
原文: js-length-check-first.md — Early Length Check for Array Comparisons
function hasChanges(current: string[], original: string[]) {
if (current.length !== original.length) return true
// 长度相同时再排序比较...
}
7.8 提前返回
影响:低-中(避免不必要的计算)
结果确定后立即返回,跳过不必要的处理。
原文: js-early-exit.md — Early Return from Functions
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) return { valid: false, error: 'Email required' }
if (!user.name) return { valid: false, error: 'Name required' }
}
return { valid: true }
}
7.9 提升 RegExp 创建
影响:低-中(避免重复创建)
不要在渲染中创建 RegExp。提升到模块作用域或用 useMemo() 记忆化。注意全局正则(/g)有可变的 lastIndex 状态。
原文: js-hoist-regexp.md — Hoist RegExp Creation
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }) {
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, 'gi'), [query])
// ...
}
7.10 用循环代替 sort 查找最值
影响:低(O(n) 代替 O(n log n))
查找最大或最小元素仅需一次遍历,排序是浪费。
原文: js-min-max-loop.md — Use Loop for Min/Max Instead of Sort
function getLatestProject(projects: Project[]) {
let latest = projects[0]
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) latest = projects[i]
}
return latest
}
7.11 使用 Set/Map 实现 O(1) 查找
影响:低-中(O(n) 降至 O(1))
将数组转换为 Set/Map 以进行重复的成员检查。
原文: js-set-map-lookups.md — Use Set/Map for O(1) Lookups
const allowedIds = new Set(['a', 'b', 'c'])
items.filter(item => allowedIds.has(item.id))
7.12 使用 toSorted() 替代 sort() 保证不可变性
影响:中-高(防止 React 状态中的变更 bug)
.sort() 原地修改数组,会导致 React state 和 props 的 bug。使用 .toSorted() 创建新的排序数组。
原文: js-tosorted-immutable.md — Use toSorted() Instead of sort() for Immutability
// 错误:变更了 props 数组
const sorted = users.sort((a, b) => a.name.localeCompare(b.name))
// 正确:创建新数组
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
其他不可变数组方法: .toReversed()、.toSpliced()、.with()
8. 高级模式
影响:低 — 针对特定场景的高级模式,需谨慎实现。
8.1 应用初始化仅执行一次
影响:低-中(避免开发环境重复初始化)
不要把全局初始化放在组件的 useEffect([]) 中。组件可能重新挂载,effect 会重复执行。使用模块级守卫。
原文: advanced-init-once.md — Initialize App Once, Not Per Mount
let didInit = false
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
}
8.2 将事件处理函数存储在 Refs 中
影响:低(稳定的订阅)
当回调用在不应随回调变化而重新订阅的 effect 中时,使用 useEffectEvent 创建稳定的函数引用。
原文: advanced-event-handler-refs.md — Store Event Handlers in Refs
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
8.3 useEffectEvent 实现稳定回调引用
影响:低(防止 effect 重运行)
在回调中访问最新值,无需将其添加到依赖数组。防止 effect 重运行的同时避免闭包过期。
原文: advanced-use-latest.md — useEffectEvent for Stable Callback Refs
import { useEffectEvent } from 'react'
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch)
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300)
return () => clearTimeout(timeout)
}, [query]) // onSearch 不再需要加入依赖
}