文章

性能优化策略对比深度解析

深入对比Vue的静态提升、Block Tree等编译时优化策略与React的时间切片、fiber并发调度等运行时优化策略,揭示两大框架不同的性能优化哲学

性能优化策略对比深度解析

一句话概括

Vue的性能优化主要在编译时完成——通过模板静态分析实现静态提升、patching优化等;React的性能优化主要在运行时完成——通过Fiber的可中断调度、并发模式和手动memoization来提升运行时效率,两种策略反映了”编译优于运行时”和”理解运行时才能优化运行时”的不同哲学。

背景与意义

性能优化是前端工程中最复杂也最核心的议题之一。不同的框架选择了截然不同的优化路径,这些路径的差异源于框架本身的架构设计决策。

Vue和React的优化哲学差异,可以追溯到它们对”声明式UI”的理解分歧:

  • Vue认为框架应该帮开发者做好优化,以最小化优化成本为目标
  • React认为框架应该为开发者提供优化的基础设施,但需要开发者了解何时及如何优化

这种差异直接反映在两者的优化策略上:Vue在编译时尽可能多地完成工作,减少运行时开销;React在运行时提供灵活的可中断机制和调度能力,把优化的选择权交给开发者和框架的运行时。

概念与定义

编译时优化(Vue核心策略)

编译时优化是指在模板/组件编译为渲染函数的阶段,编译器分析代码结构并注入优化标记和提升操作:

  • 静态提升:将不变的节点提升到渲染函数外部
  • Block Tree:仅追踪动态节点的更新树
  • patchFlags:标记动态节点的精确变化类型
  • 事件缓存:自动缓存内联事件处理函数

运行时优化(React核心策略)

运行时优化是指框架在组件渲染和更新过程中采用的优化机制:

  • Fiber可中断渲染:将渲染工作切分为可中断的单元
  • 时间切片:将渲染工作分散到多个帧中执行
  • 优先级调度:高优先级更新(用户输入)优先于低优先级更新(数据加载)
  • 手动memoization:useMemo/useCallback/React.memo

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Vue: 编译时自动优化 -->
<!-- 编译器会自动处理静态提升和Block Tree -->
<template>
  <div class="container">
    <h1>我的应用</h1>                    <!-- 静态节点:自动提升 -->
    <p>欢迎回来,{{ user.name }}</p>    <!-- 动态节点:标记patchFlags -->
    <footer>© 2026</footer>               <!-- 静态节点:自动提升 -->
  </div>
</template>

<!--
  编译结果(概念):
  静态节点 _hoisted_1 = <h1>
  静态节点 _hoisted_2 = <footer>
  渲染函数只追踪动态的 <p> 元素
-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React: 运行时手动优化
// 需要开发者手动标记
function App({ user }) {
  const greeting = useMemo(
    () => `欢迎回来,${user.name}!`,
    [user.name]
  )

  return (
    <div className="container">
      <h1>我的应用</h1>
      <p>{greeting}</p>
      <footer>© 2026</footer>
    </div>
  )
}
// React Compiler (React 19+) 可自动处理

核心知识点拆解

1. Vue的静态提升

Vue模板编译器会自动识别对象结构中那些永远不会变化的部分:

1
2
3
4
5
6
7
8
9
<template>
  <div>
    <p class="static">永远不会变</p>        <!-- 静态节点 → 提升 -->
    <span :class="{ active: isActive }">    <!-- 有绑定 → 不提升 -->
      {{ dynamic }}
    </span>
    <p>也永远不会变</p>                     <!-- 静态节点 → 提升 -->
  </div>
</template>

编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createElementVNode, openBlock, createBlock } from 'vue'

// 静态节点提升到渲染函数外部
const _hoisted_1 = /*#__PURE__*/ createElementVNode('p', { class: 'static' }, '永远不会变', -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/ createElementVNode('p', null, '也永远不会变', -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (openBlock(), createBlock('div', null, [
    _hoisted_1,
    createElementVNode('span', { class: _ctx.isActive ? 'active' : '' }, _toDisplayString(_ctx.dynamic), 9 /* TEXT, CLASS */),
    _hoisted_2,
  ]))
}

静态提升的效果:_hoisted_1_hoisted_2只在最外层作用域创建一次,re-render时直接复用,不会重复创建虚拟节点。对于静态内容占比高的页面(如文章详情页、个人信息页),这个优化效果显著。

2. Vue的Block Tree优化

Block Tree是Vue3性能优化的核心创新。它的本质是只追踪动态节点

1
2
3
4
5
6
7
8
9
10
<template>
  <div>                              <!-- Block 根 -->
    <span>{{ count }}</span>         <!-- 动态节点 #0 -->
    <span>静态</span>                <!-- 静态节点 → 跳过 -->
    <span>{{ msg }}</span>           <!-- 动态节点 #1 -->
    <div>                            <!-- 子 Block -->
      <span>{{ nested }}</span>      <!-- 动态节点 -->
    </div>
  </div>
</template>

编译后,根Block节点上有一个dynamicChildren数组:

1
2
3
4
5
6
// dynamicChildren 只包含动态节点
block.dynamicChildren = [
  vnode(span, { textContent: count }),  // 动态节点
  vnode(span, { textContent: msg }),    // 动态节点
  block(div, { children: [...] }),      // 子block
]

count变化时,Vue只需要在dynamicChildren数组中查找需要更新的节点,跳过所有静态节点。对于大型静态页面,实际的Diff范围可以缩小到原来的10%甚至更少。

3. React的Fiber可中断渲染

React的Fiber架构将渲染过程切分成单元,可以在执行过程中暂停和恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Fiber架构的工作循环
function workLoopConcurrent() {
  // 浏览器每帧有约16.6ms
  // React使用requestIdleCallback或postMessage机制
  // 检查是否有剩余时间,没有则让出控制权给浏览器
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress)
  }
}

// 判断是否让出控制权
function shouldYield(): boolean {
  // 检查当前帧是否还有剩余时间
  // 如果没有足够时间处理下一个fiber单元,就让出
  if (getCurrentTime() - startTime > frameInterval) {
    return true
  }
  // 检查是否有更高优先级的更新
  return peek(runtime.scheduler).some(task => task.priority > currentPriority)
}

时间切片(Time Slicing)的效果:当一个组件树需要大量计算时(如渲染10000个列表项),React不会阻塞主线程5ms以上,而是将渲染工作分割成多个小片段,每执行一段就将控制权交还给浏览器,让浏览器能处理用户输入、动画渲染等更高优先级的工作。

4. React的优先级调度

React的调度器(Scheduler)将所有更新按优先级分为不同”车道”(Lanes):

1
2
3
4
5
6
7
// React Lane(车道)优先级分类
export const NoLane = 0b0000000000000000000
export const SyncLane = 0b0000000000000000001     // 最高优先级(用户交互)
export const InputDiscreteLane = 0b000000000000000010  // 离散输入(点击、键盘)
export const DefaultLane = 0b0000000000000010000     // 默认更新
export const TransitionLane = 0b0000000000010000000  // 过渡更新(页面跳转)
export const IdleLane = 0b1000000000000000000       // 空闲时执行

不同的更新类型自动分配到不同的Lane:

1
2
3
4
5
6
7
// 用户点击 → SyncLane
<button onClick={() => setCount(c => c + 1)}>+1</button>

// startTransition → TransitionLane
startTransition(() => {
  setSearchResults(data) // 低优先级,可被中断
})

优先级调度的实际效果:当用户在输入框中快速打字时,每次输入触发的高优先级状态更新(SyncLane)会打断正在进行的低优先级渲染工作(如搜索结果渲染),确保用户不会感到输入卡顿。

5. 手动memoization vs 自动优化

这是Vue和React最核心的体验差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// React需要主动优化
function ExpensiveList({ items, filter }) {
  // 开发者需要主动使用useMemo
  const filtered = useMemo(
    () => items.filter(i => i.name.includes(filter)),
    [items, filter]
  )

  // 开发者需要主动使用useCallback
  const handleClick = useCallback(
    (id) => console.log(filter, id),
    [filter]
  )

  // 子组件需要用React.memo包装
  return filtered.map(item => (
    <ListItem key={item.id} item={item} onClick={handleClick} />
  ))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Vue 自动优化 -->
<template>
  <div>
    <ListItem
      v-for="item in filteredItems"
      :key="item.id"
      :item="item"
      @click="handleClick"
    />
  </div>
</template>

<script setup>
// 不需要useMemo — Vue的响应式系统会自动追踪依赖
const filteredItems = computed(() =>
  props.items.filter(i => i.name.includes(props.filter))
)

// 不需要useCallback — Vue的响应式系统确保稳定性
function handleClick(id) {
  console.log(props.filter, id)
}
</script>

React 19的React Compiler会改变这个局面——它会在编译时自动插入memoization代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 编译前
function ExpensiveList({ items, filter }) {
  const filtered = items.filter(i => i.name.includes(filter))
  return filtered.map(item => <ListItem key={item.id} item={item} />)
}

// 编译后(React Compiler)
function ExpensiveList($) {
  const [items, filter] = $props($, ['items', 'filter'])
  const filtered = $memo(() => items.filter(i => i.name.includes(filter)))
  return $memo(() => filtered.map(item => <ListItem key={item.id} item={item} />))
}

实战案例:大型列表渲染对比

场景:渲染10000条商品列表,支持搜索过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- Vue3 版本 -->
<template>
  <div>
    <input v-model="searchQuery" placeholder="搜索商品..." />
    <ProductItem
      v-for="product in filteredProducts"
      :key="product.id"
      :product="product"
      @click="selectProduct"
    />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const searchQuery = ref('')
const { data: products } = useProducts() // 假设的 Composable

// Vue的computed自动追踪searchQuery的变化
// 只要searchQuery不变,即使products变了也不会重新计算
const filteredProducts = computed(() => {
  const query = searchQuery.value.toLowerCase()
  if (!query) return products.value
  return products.value.filter(p =>
    p.name.toLowerCase().includes(query)
  )
})

function selectProduct(product) {
  console.log('selected:', product.id)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// React 版本(手动优化)
import React, { useState, useMemo, useCallback, memo } from 'react'

// 需要React.memo防止不必要的重渲染
const ProductItem = memo(function ProductItem({
  product,
  onSelect,
}: {
  product: { id: string; name: string; price: number }
  onSelect: (product: any) => void
}) {
  return <div onClick={() => onSelect(product)}>{product.name}</div>
})

function ProductList() {
  const [searchQuery, setSearchQuery] = useState('')
  const { data: products } = useProducts()

  const filteredProducts = useMemo(() => {
    const query = searchQuery.toLowerCase()
    if (!query) return products
    return products.filter(p => p.name.toLowerCase().includes(query))
  }, [products, searchQuery])

  const handleSelect = useCallback((product: any) => {
    console.log('selected:', product.id)
  }, [])

  return (
    <div>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
        placeholder="搜索商品..."
      />
      {filteredProducts.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onSelect={handleSelect}
        />
      ))}
    </div>
  )
}

性能对比

优化维度Vue3React(手动优化)React(React Compiler)
搜索输入时只触发computed和依赖的计算需memo防止列表重渲染自动memo
子组件更新默认不重新渲染不变的部分需React.memo自动memo
事件回调自动稳定引用需useCallback自动
过滤计算自动缓存需useMemo自动
列表变化时Block Tree跳过静态节点整个列表Diff同左

在大型列表场景下,Vue3的优化是”默认就有”的,而React需要在正确使用memoization技术的前提下才能达到相近的性能。React 19的React Compiler有望消除这个差距,但普及还需要时间。

底层原理

1. Vue的patchFlags系统

Vue3的每个动态VNode都带有patchFlags,精确标记需要什么类型的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Vue3的patchFlags(packages/shared/src/patchFlags.ts)
export const enum PatchFlags {
  TEXT = 1,           // 只有文本变化
  CLASS = 1 << 1,     // 只有class变化
  STYLE = 1 << 2,     // 只有style变化
  PROPS = 1 << 3,     // 非class/style的属性变化
  FULL_PROPS = 1 << 4, // 所有属性都需要比较
  NEED_HYDRATE = 1 << 5,  // 需要hydration
  STABLE_FRAGMENT = 1 << 6,  // 稳定的fragment
  KEYED_FRAGMENT = 1 << 7,   // 带key的列表
  UNKEYED_FRAGMENT = 1 << 8, // 无key的列表
  DYNAMIC_SLOTS = 1 << 9,    // 动态插槽
  HOISTED = -1,       // 已提升的静态节点
  BAIL = -2,          // 退出优化模式
}

利用patchFlags,Vue在patch时可以做精确更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function patchElement(n1, n2, container) {
  const { props: oldProps, patchFlag: oldFlag } = n1
  const { props: newProps, patchFlag: newFlag } = n2

  if (newFlag & PatchFlags.CLASS) {
    // 只有class变化 → 只更新class
    hostPatchProp(el, 'class', null, newProps.class)
  } else if (newFlag & PatchFlags.STYLE) {
    // 只有style变化 → 只更新style
    hostPatchProp(el, 'style', null, newProps.style)
  }
  // ... 更精确的更新分支
  // 不需要对props做完整的对比循环!
}

这就是”编译时标记,运行时精确更新”的精髓。React无法做到类似优化,因为JSX的动态性使编译器无法静态确定哪些属性可能会变化。

2. React的Fiber与时间切片

React通过Fiber架构实现可中断渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 简化的Fiber工作单元
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  const current = unitOfWork.alternate

  // 开始处理当前Fiber
  let next: Fiber | null
  next = beginWork(current, unitOfWork, subtreeRenderLanes)

  // 记录已经处理过的props,用于后续的memoization判断
  unitOfWork.memoizedProps = unitOfWork.pendingProps

  if (next === null) {
    // 当前Fiber处理完成,进入完成阶段
    next = completeUnitOfWork(unitOfWork)
  }

  return next
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress)
  }
}

时间切片的实现机制:

  1. React使用MessageChannel(或requestIdleCallback)作为时间切片的底层API
  2. 每次处理一个Fiber单元前,检查是否已超过预设的时间预算(通常是5ms)
  3. 如果超过,将剩余工作放回调度队列,设置一个宏任务在下一帧继续
  4. 当前宏任务返回,浏览器有机会处理输入、渲染等更高优先级的工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 时间切片的核心 - 使用了MessageChannel
const channel = new MessageChannel()
const port = channel.port2

channel.port1.onmessage = performWorkUntilDeadline

function requestHostCallback(callback: () => void) {
  scheduledHostCallback = callback
  // 通过postMessage创建宏任务
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true
    port.postMessage(null)
  }
}

3. 编译时与运行时优化的根本差异

Vue和React的优化策略差异源于更根本的选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue的路径:
模板编译
  ↓
编译器分析模板结构(静态分析)
  ↓
输出优化后的渲染函数(含patchFlags,静态提升等)
  ↓
运行时:根据标记精确更新

React的路径:
JSX编译(基本不做优化)
  ↓
渲染函数执行 → 生成虚拟DOM
  ↓
Fiber运行时调度(可中断)
  ↓
Diff比较
  ↓
DOM更新
  ↓
同时依赖开发者手动memoization减少不必要的组件渲染

核心差异:Vue将优化前置到编译时,React将优化放在运行时

高频面试题解析

面试题1:Vue中的v-for和v-if为什么不能放在同一个元素上?

问题:Vue官方文档提示不要在同一元素上使用v-if和v-for,为什么?

答案:在Vue2中,v-for的优先级高于v-if,这意味着:

1
2
3
4
5
6
7
8
9
10
11
<!-- Vue2中(v-for优先级高) -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
  {{ item.name }}
</li>

<!-- 等价于实际执行的代码 -->
<li v-for="item in items" :key="item.id">
  <template v-if="item.isActive">
    {{ item.name }}
  </template>
</li>

这会导致性能问题:即使大部分items不需要渲染,Vue2仍然会遍历所有items并创建VNode。最佳实践是使用计算属性预过滤:

1
2
3
4
5
6
7
8
9
10
11
<template>
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
</template>

<script setup>
const activeItems = computed(() =>
  items.value.filter(item => item.isActive)
)
</script>

在Vue3中,v-if的优先级高于v-for,这是一个破坏性变更。但最佳实践仍然是将过滤逻辑放在计算属性中。

这个面试题看似在讲语法规则,实则考察了对Vue编译过程和性能优化的理解。

面试题2:React的useMemo是否总是必要的?

问题:React中的useMemo在什么情况下是必要的?什么情况下是过度优化?

答案:useMemo不是免费的——它本身也有内存开销。需要评估是否值得使用:

适合使用useMemo的场景

  1. 计算开销大的操作:对大量数据进行排序/过滤、深度嵌套对象遍历、正则匹配
  2. 作为依赖传递给子组件的对象/数组:避免每次渲染创建新引用,跳过子组件不必要的重渲染
  3. 复杂JSX结构:条件复杂的JSX子树

不适合使用useMemo的场景

  1. 基本类型计算const total = price * count完全不需要memo
  2. 简单的字符串拼接const greeting = 'Hello, ' + name
  3. 初次渲染就执行的计算:memo只影响re-render,不影响首次渲染
  4. 没有子组件传递的计算结果:纯使用在同一个组件内的简单值

React的官方建议是:先不写memo,当确认性能瓶颈后,再针对性地添加。React 19的React Compiler将消除这个决策负担。

Vue的响应式系统在这个问题上更为省心——computed默认带缓存,且自动追踪依赖,不需要开发者判断”是否需要memo”。

面试题3:Vue的Block Tree和React的useMemo哪个优化效果好?

问题:Vue3的Block Tree和React的useMemo,哪个优化效果更好?

答案:两者是不同层面的优化,解决的是不同的问题:

Block Tree解决的问题:减少虚拟DOM比较的范围(跳过静态节点)

useMemo解决的问题:避免不必要的高开销计算

在大多数场景下,Block Tree的效果比useMemo更显著,因为:

  1. Block Tree是框架层面全局生效的,不需要开发者手动介入
  2. Block Tree的效果是”减去不需要比较的节点”,优化幅度取决于静态节点比例
  3. useMemo的效果取决于开发者的正确使用,且容易被遗漏或错误

但React有Block Tree不具备的能力:

  1. useMemo可以在任意逻辑上使用,不限于模板解析
  2. useMemo可以精确控制计算频率(依赖数组精确控制)

一个有意思的对比:在一个静态内容占90%的页面中,Block Tree可以跳过90%的节点比较。而React如果不使用React.memo,即使使用useMemo缓存了值,父组件重渲染时子组件仍会重渲染(除非也使用了React.memo)。

总结与扩展

Vue和React的性能优化策略的根本差异:

维度VueReact
优化时机编译时运行时
优化方式自动(框架负责)手动+框架基础设施
静态处理静态提升、Block TreeDiff时跳过(较少静态分析)
动态处理patchFlags精确更新Fiber可中断调度
开发者成本低(默认优化好)中到高(需理解机制)
优化上限中等(受限于模板)高(可精细控制)

扩展思考

React 19的React Compiler和Vue3的Vapor Mode代表了两种有趣的方向聚合:

React通过编译器做更多编译时优化(向Vue靠拢),Vue通过Vapor Mode实现无虚拟DOM的运行时(向更极致的响应式靠拢)。

同时,Signal(信号)模式在前端界广泛普及——Solid.js、Preact Signals、Angular都采用类似模式。Signal的本质是”推拉结合”的响应式:变化发生时push更新通知,渲染时pull最新值。这在理论上比Vue的纯push和React的纯pull都更高效。

未来的前端性能优化趋势是:更多的工作在编译时完成,更少的依赖虚拟DOM,更细粒度的更新追踪。Vue和React都在沿着这个方向演进,只是起点不同、路径不同。

本文由作者按照 CC BY 4.0 进行授权