2026 React 最佳实践(中文整理版)

5 阅读16分钟

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.mdDefer 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.mdDependency-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.mdPrevent 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.mdPromise.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.mdStrategic 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.mdAvoid 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-reactreact-iconslodashdate-fnsrxjs 等。


2.2 条件模块加载

影响:高(仅在需要时加载大型数据)

仅在功能激活时才加载大型数据或模块。typeof window !== 'undefined' 检查可防止 SSR 时打包该模块。

原文: bundle-conditional.mdConditional 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.mdDefer 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.mdDynamic 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.mdPreload 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.mdAuthenticate 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.mdAvoid 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.mdCross-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.mdMinimize 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.mdParallel 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.mdPer-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.mdUse 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.mdDeduplicate Global Event Listeners


4.2 使用被动事件监听器优化滚动性能

影响:中(消除事件监听器导致的滚动延迟)

为 touch 和 wheel 事件监听器添加 { passive: true },启用立即滚动。浏览器通常会等待监听器完成以检查是否调用了 preventDefault(),导致滚动延迟。

原文: client-passive-event-listeners.mdUse 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.mdUse 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.mdVersion 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.mdCalculate Derived State During Rendering

// 错误
const [fullName, setFullName] = useState('')
useEffect(() => { setFullName(firstName + ' ' + lastName) }, [firstName, lastName])

// 正确
const fullName = firstName + ' ' + lastName

5.2 将状态读取延迟到使用点

影响:中(避免不必要的订阅)

如果只在回调中读取动态状态(如 searchParams),不要订阅它,在需要时直接读取。

原文: rerender-defer-reads.mdDefer 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.mdDo 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.mdExtract 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.mdExtract 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.mdNarrow 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.mdPut 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.mdSubscribe to Derived State

// 错误:每个像素变化都重渲染
const width = useWindowWidth()
const isMobile = width < 768

// 正确:仅布尔值变化时重渲染
const isMobile = useMediaQuery('(max-width: 767px)')

5.9 使用函数式 setState 更新

影响:中(防止闭包过期和不必要的回调重建)

基于当前状态更新时,使用 setState 的函数更新形式,防止闭包过期、消除不必要的依赖、创建稳定的回调引用。

原文: rerender-functional-setstate.mdUse 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.mdUse Lazy State Initialization

// 错误:每次渲染都执行 buildSearchIndex
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))

// 正确:仅初始渲染执行一次
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))

5.11 对非紧急更新使用 Transitions

影响:中(保持 UI 响应性)

将频繁的非紧急状态更新标记为 transition,保持 UI 响应性。

原文: rerender-transitions.mdUse 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.mdUse 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.mdAnimate 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.mdCSS content-visibility for Long Lists

.message-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 80px;
}

6.3 提升静态 JSX 元素

影响:低(避免重新创建)

将静态 JSX 提取到组件外部,避免每次渲染重新创建。对大型静态 SVG 尤其有效。

原文: rendering-hoist-jsx.mdHoist 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.mdOptimize SVG Precision

npx svgo --precision=1 --multipass icon.svg

6.5 防止水合不匹配且无闪烁

影响:中(避免视觉闪烁和水合错误)

当渲染内容依赖客户端存储(localStorage、cookies)时,注入一个同步脚本在 React 水合前更新 DOM,避免 SSR 报错和水合后闪烁。

原文: rendering-hydration-no-flicker.mdPrevent 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.mdSuppress Expected Hydration Mismatches

<span suppressHydrationWarning>{new Date().toLocaleString()}</span>

6.7 使用 Activity 组件实现显示/隐藏

影响:中(保持状态/DOM)

使用 React 的 <Activity> 为频繁切换可见性的昂贵组件保持状态/DOM,避免昂贵的重渲染和状态丢失。

原文: rendering-activity.mdUse Activity Component for Show/Hide

import { Activity } from 'react'
function Dropdown({ isOpen }) {
  return (
    <Activity mode={isOpen ? 'visible' : 'hidden'}>
      <ExpensiveMenu />
    </Activity>
  )
}

6.8 使用显式条件渲染

影响:低(防止渲染 0 或 NaN)

当条件可能是 0NaN 或其他 falsy 但会渲染的值时,使用显式三元运算符而非 &&

原文: rendering-conditional-render.mdUse 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.mdUse 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 避免布局抖动

影响:中(防止强制同步布局)

避免在样式写入和布局读取之间交错。当你在样式更改之间读取布局属性(如 offsetWidthgetBoundingClientRect())时,浏览器被迫触发同步回流。优先使用 CSS 类。

原文: js-batch-dom-css.mdAvoid 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.mdBuild 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.mdCache 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.mdCache 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)

localStoragesessionStoragedocument.cookie 是同步且昂贵的。将读取结果缓存在内存中,注意外部变更时失效缓存。

原文: js-cache-storage.mdCache 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.mdCombine 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.mdEarly Length Check for Array Comparisons

function hasChanges(current: string[], original: string[]) {
  if (current.length !== original.length) return true
  // 长度相同时再排序比较...
}

7.8 提前返回

影响:低-中(避免不必要的计算)

结果确定后立即返回,跳过不必要的处理。

原文: js-early-exit.mdEarly 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.mdHoist 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.mdUse 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.mdUse 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.mdUse 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.mdInitialize 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.mdStore 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.mduseEffectEvent 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 不再需要加入依赖
}

参考资料