跳到正文
前端知识库
性能

React 与 Vue 运行时优化

从更新边界、稳定引用、状态所有权、列表 key、批量更新到 React Compiler 和 Vue v-memo。

7 分钟React · Vue · memo · useMemo · useCallback · Context · v-once · v-memo · React Compiler

React 与 Vue 运行时优化

1. 优化目标:少做无效工作

框架优化不是“每个组件都包一层 memo”,而是找出一次状态变化实际影响了哪些视图:

状态变化 -> 组件更新/虚拟节点创建 -> Diff -> 提交 DOM/副作用 -> 浏览器布局绘制

先用 Profiler、Performance 面板和业务操作复现,再判断瓶颈在组件更新、昂贵计算、DOM 提交还是浏览器渲染。没有测量时,优先改善状态所有权和数据流,而不是堆缓存 API。

2. React 更新边界

2.1 React.memo、PureComponent 和比较函数

React.memo 对函数组件 props 做浅比较;PureComponent 对类组件 props/state 做浅比较。它们只在 props 引用稳定且组件重新渲染成本值得比较时有收益:

const Price = React.memo(function Price({ value }: { value: number }) {
  return <span>{value.toFixed(2)}</span>
})

自定义比较函数要保持纯、快速,并完整比较影响渲染的字段:

const Row = React.memo(
  function Row({ item, selected }: Props) {
    return <div aria-selected={selected}>{item.name}</div>
  },
  (prev, next) => prev.item.id === next.item.id
    && prev.item.name === next.item.name
    && prev.selected === next.selected,
)

漏比较字段会导致界面停留在旧值,深比较又可能比重新渲染更贵。不要把“子组件没有重新 render”当作绝对目标:有时计算比较本身就是瓶颈。

2.2 稳定的派生值和回调

父组件每次 render 都创建新的对象、数组和函数,会使浅比较失效:

function Parent({ items, onPick }: Props) {
  const visibleItems = React.useMemo(
    () => items.filter((item) => item.enabled),
    [items],
  )
  const handlePick = React.useCallback(
    (id: string) => onPick(id),
    [onPick],
  )

  return <List items={visibleItems} onPick={handlePick} />
}

useMemo/useCallback 有缓存维护和依赖正确性成本:

  • 只在计算昂贵、引用稳定能跳过下游工作或依赖 API 要求稳定时使用;
  • 依赖数组必须完整,不能为“稳定”故意省略依赖;
  • 如果依赖每次都是新对象,先重构数据边界;
  • 不能把缓存当作语义保证,React 允许在特定情况下丢弃 memoized 值。

必要时可用 useMemo 缓存一段昂贵的 JSX,但仍应以 profiler 证据为前提。

2.3 状态下放和订阅粒度

如果状态只被一小段子树使用,就把状态和交互组件提取到更低的公共祖先,避免每次输入都让昂贵兄弟树重新执行:

function Page() {
  return (
    <>
      <FilterForm />
      <ExpensiveReport />
    </>
  )
}

function FilterForm() {
  const [color, setColor] = React.useState('red')
  return (
    <>
      <input value={color} onChange={(event) => setColor(event.target.value)} />
      <p style={{ color }}>Preview</p>
    </>
  )
}

跨层共享状态可采用 Context、外部 store 或发布者/订阅者模式,但要控制订阅粒度:Context value 改变时,读取该 Context 的消费者会更新;把大对象和高频状态放在一个 Context 中会扩大影响范围。可拆分 Context、提供 selector API 或把高频状态放在局部 store。

2.4 列表 key 与复用

key 是 Diff 识别同一列表项的身份,应使用稳定业务 ID:

items.map((item) => <Row key={item.id} item={item} />)

数组头部插入、排序或删除时,索引 key 可能让组件状态错配,也削弱节点复用。key 不需要全局唯一,只需在同一父节点的兄弟列表中稳定且唯一。列表很长时再结合窗口化/虚拟列表,见渲染与运行时优化

2.5 提交阶段和 Effect

useLayoutEffect、生命周期或某些同步执行的 Effect 中调用 setState,可能导致额外 render/commit,并阻塞浏览器绘制。优先:

  1. 在 render 中直接计算派生值,不把它复制成 state;
  2. 用事件处理器更新由用户操作产生的状态;
  3. 只有必须读取 DOM 后才能确定的值才使用 layout effect,并尽量合并更新;
  4. Effect 订阅、定时器和 Observer 必须在 cleanup 中释放。
// 不推荐:把 props 派生值再写回 state,容易多一次更新
const [fullName, setFullName] = useState('')
useEffect(() => setFullName(`${first} ${last}`), [first, last])
// 推荐:render 时计算;若计算昂贵再使用 useMemo
const fullName = `${first} ${last}`

这也能避免“在 effect 中同步 setState 导致 cascading renders”的告警和额外提交。

2.6 批量更新与优先级

现代 React(尤其 React 18 的 createRoot)会在更多异步边界自动批量更新;不能再简单背诵“事件里异步、Promise 里同步”。旧版本或第三方调度边界可能需要 unstable_batchedUpdates,应以项目 React 版本和 profiler 为准。

需要让输入反馈先出现时,可以把非紧急更新标记为 transition:

const [isPending, startTransition] = React.useTransition()

function onChange(value: string) {
  setQuery(value) // 紧急:立即更新输入框
  startTransition(() => {
    setDeferredQuery(value) // 非紧急:派生结果随低优先级更新
  })
}

const results = useMemo(
  () => filterLargeDataset(deferredQuery),
  [deferredQuery],
)

startTransition 只改变状态更新的优先级,不会自动把 filterLargeDataset 变成可抢占任务;计算本身仍应通过 useMemo、分块调度或 Worker 降低主线程成本。setTimeout 也只是把任务推迟到下一个宏任务。

3. React Compiler

React Compiler 通过编译阶段分析组件依赖,自动插入部分 memoization,目标是减少手写 useMemo/useCallback 的负担。它不是性能保证,也不会修复错误的状态建模、过大的 Context、昂贵同步计算或副作用。

3.1 接入前检查

在存量项目中先运行官方健康检查(版本命令随工具发布变化):

npx react-compiler-healthcheck@latest

重点处理:不兼容库、违反 Hooks 规则、非纯 render、依赖隐式可变数据等问题。开发和生产应设置不同的错误阈值,并保留关闭编译器的回滚开关。

3.2 Vite/Babel 配置示意

import babel from 'vite-plugin-babel'
import type { Plugin } from 'vite'

export function reactCompilerPlugin(): Plugin {
  const development = process.env.NODE_ENV === 'development'
  return babel({
    babelConfig: {
      presets: ['@babel/preset-react'],
      plugins: [[
        'babel-plugin-react-compiler',
        {
          compilationMode: 'infer',
          panicThreshold: development ? 'critical_errors' : 'none',
          target: '19',
        },
      ]],
    },
  })
}

接入时固定 React、编译器、Babel/Vite 插件版本,检查 source map 和构建产物,并用代表性页面比较 render 次数、INP、包体积和错误率。不同 React 版本的 runtime 依赖和配置字段可能变化,不能照抄旧配置。

4. Vue 更新优化

4.1 稳定 props

让父组件先计算子组件真正关心的布尔值,避免所有列表项都订阅高频的原始状态:

<ListItem
  v-for="item in list"
  :key="item.id"
  :id="item.id"
  :active="item.id === activeId"
/>

相比让每个 ListItem 同时接收整个 activeId,稳定的 active prop 能缩小更新范围。传递大对象时要留意对象引用和深层响应式开销。

4.2 v-once

v-once 将一棵子树视为静态内容,后续更新跳过它:

<section v-once>
  <h2>{{ initialTitle }}</h2>
  <p>{{ initialDescription }}</p>
</section>

只有确认内容在组件生命周期内不会变化时才使用;误用会让界面不再响应更新。

4.3 v-memo

v-memo 根据固定长度依赖数组跳过子树更新,适合大型 v-for 的微优化:

<div
  v-for="item in list"
  :key="item.id"
  v-memo="[item.id === selectedId]"
>
  <p>{{ item.id }} - {{ item.id === selectedId }}</p>
</div>

依赖数组必须包含所有影响该子树输出的值;漏依赖会显示旧内容。Vue 官方将其定位为性能至上场景的优化,不应替代合理组件拆分、分页和虚拟列表。

4.4 计算属性与事件

computed 缓存纯派生值,避免在模板中重复昂贵计算;不要在 computed 中执行网络请求或修改状态。高频 scroll/input 事件需防抖/节流,并在 onUnmounted 清理监听器和定时器。

5. 性能验证矩阵

改动 关注指标 必测边界
memo/props 稳定 render 次数、INP、CPU props 深比较成本、字段遗漏
状态下放/Context 拆分 交互延迟、更新组件数 路由切换、订阅清理、SSR 隔离
列表 key/虚拟化 滚动帧率、内存 插入/删除/排序、键盘焦点、动态高度
transition/任务拆分 INP、长任务、完成时间 更新被打断、错误回滚、低端设备
React Compiler/v-memo 构建错误、运行时指标 不兼容库、旧浏览器、关闭开关回滚

6. 面试追问速答

Q: useMemoReact.memo 有什么区别?

A: useMemo 缓存组件内部的值或 JSX,React.memo 包装组件并基于 props 比较决定是否执行;二者都依赖稳定引用和正确依赖,不能脱离 profiler 盲目使用。

Q: Context 为什么会导致大面积更新?

A: Provider 的 value 引用改变时,读取该 Context 的消费者会收到更新。拆分 Context、稳定 value、selector store 或下放状态可以缩小订阅范围;memo 不能阻止消费者因 Context 改变而更新。

Q: React 事件外的 setState 一定不批量吗?

A: 取决于 React 版本和 root API。React 18 createRoot 已扩大自动批量范围,旧版本和特殊调度边界不同;应以实际版本和 profiler 验证,不要背旧结论。

Q: React Compiler 是否替代所有手写优化?

A: 不能。它主要自动化部分 memoization,仍要求组件纯、Hooks 规则正确,并不能解决状态边界、数据结构、长任务和 DOM/网络瓶颈;接入要做健康检查、灰度和可回滚验证。

7. 资料来源与现有专题

本篇综合性能 PDF 关于 React 不必要更新、PureComponent/React.memoshouldComponentUpdate、稳定 props、发布订阅、状态下放、key、批量/优先级更新、IntersectionObserver、Vue v-once/v-memo 和 React Compiler 的内容,并按 React 18+ 与当前 Web Vitals 口径修正边界。相关专题:React 组件性能优化Vue3 性能提升渲染与运行时优化