性能提升测试
首屏加载速度 | 更新耗时 | 更新内存使用 | |
---|---|---|---|
traditional Mode | 1.82s 到 2.28s | 143ms | 180 mb |
Vapor Mode | 1.72s 到 2s | 25ms | 144 mb |
测试方法
浏览器:Chorme 版本 135 隐私模式
创建项目,把 vite-plugin-vue-devtools
插件删除。删除默认的.vue 文件,删除样式文件。
template 核心代码如下:
js
<div class="flat-form">
<div v-for="item in formData" :key="item.id" class="flat-form-item">
<input type="text" v-model="item.name" placeholder="Name" />
<input type="email" v-model="item.email" placeholder="Email" />
<input
type="number"
v-model="item.age"
min="18"
max="120"
placeholder="Age"
/>
<label class="active-label">
<input type="checkbox" v-model="item.active" /> Active
</label>
</div>
</div>
数据生成逻辑如下:
js
const generateTestData = () => {
formData.value = Array.from({ length: totalItems }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
age: Math.floor(Math.random() * 50) + 18,
active: i % 2 === 0, // 确保正好一半数据的active为true
}));
isLoading.value = false;
}
active 确保正好一半数据的active为true,是为了测试数据更新的时候数据变量的量样本一致。
更新数据的关键代码如下:
js
const fixedUpdateCount = 20000;
formData.value.forEach((item, index) => {
if (index < fixedUpdateCount) {
item.active = !item.active;
}
})
性能监听关键代码如下:
js
// 等待DOM更新完成
await nextTick();
// 记录结束时间并计算差值
const endTime = performance.now();
updateTime.value = Math.round(endTime - startTime);
if (performance.memory) {
const memoryUsageBytes = performance.memory.usedJSHeapSize;
const memoryUsageMB = (memoryUsageBytes / (1024 * 1024)).toFixed(2);
memoryUsage.value = parseFloat(memoryUsageMB);
console.log(`Memory usage after update: ${memoryUsageMB} MB`);
}
console.log(`DOM update completed in ${updateTime.value}ms`);
之所以不完全使用控制台中的 Performance 来记录,是因为电脑比较卡,打开控制台本身就消耗了电脑不少性能。而且我们不需要多么精准的数据,需要的是对比。有了对比数据即可。
数据来源
Vapor Mode:


传统模式:


Vapor 工作原理
配置流程
基于 vue3@3.6.0-alpha.2 版本
渐进式使用方式(混合渲染模式)
- 从 vue 中导出 vaporInteropPlugin 并 use
js
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(vaporInteropPlugin)
app.use(router)
app.mount('#app')
- 在需要使用的 SFC 文件的 setup 中配置上
vapor
即可。
全量使用
修改 main.ts 中的引入组件
js
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
vue 的包体积从 50K 减少到 6k

vapor 执行过程


编译后的文件一般在 source 对应文件的下面,它的作用就是识别.vue 文件,然后把.vue 文件编译为 js 代码
简化代码如下
js
const t0 = _template("<button>add count</button>");
const t1 = _template("<div> </div>");
const t2 = _template("<button>update class</button>");
const t3 = _template("<div>我是什么颜色</div>");
function _sfc_render(_ctx, $props, $emit, $attrs, $slots) {
const n1 = t0();
const n2 = t1();
const n3 = t2();
const n4 = t3();
n1.$evtclick = _ctx.addCount;
const x2 = _child(n2);
n3.$evtclick = _ctx.updateClass;
_renderEffect(() => {
_setText(x2, _toDisplayString(_ctx.count));
_setClass(n4, _ctx.color);
});
return [n1, n2, n3, n4];
}
上面是编译后的结果。
编译时: 识别了我绑定的在 DOM 上面可以进行的动态变改的是什么类型的节点。这次我想要改变的是 div 标签中的文本和 div 中字体的颜色。所以在生成的代码中就提前导入了 setText
以及 setClass
两个方法。
以 setClass
为例,参数有两个:_template("<div>我是什么颜色</div>")
和 ctx.color
。
我们最后来看看_renderEffect
函数,看名字你可能已经猜出来了。这个函数和vue3
中的watchEffect
比较相似,会立即运行一个函数。并且追踪函数中用的的依赖,这里的依赖是msg
变量。当依赖的值变化时会再次执行这个函数。
这里的_setText(n1, _ctx.msg)
,实际就是执行了n1.textContent = _ctx.msg
。textContent
属性表示一个节点及其后代的文本内容,也可能通过给它赋值的方式删除它的所有子节点,并替换为一个具有给定值的文本节点,和innerText
功能比较相似。
_template 源码

- isHydrating 为服务端渲染,服务 SSR,跳过
html[0] !== '<'
为 true 说明是纯文本内容,直接创建文本节点,跳过模板解析流程。- t 使用单例模式,不多于创建
<template>
。 if(!node)
缓存首次解析的 DOM 节点,避免重复解析 HTML 字符串, 如果已经存在了,通过cloneNode
复用已经创建,减少 DOM 操作成本。
_setText 源码
nodeValue
:直接修改 DOM 文本节点的内容(textContent
的底层实现)。
这里做了一个优化,如果值没有变化,就不重新个 el.nodeValue
赋值。
上面代码转化为下面代码片段,直接改变的是文本本身,因为传进来的 childNodes 只有文本。
js
const el = document.getElementById('test').childNodes[0];
el.nodeValue = el.$txt = value
_renderEffect 源码

renderEffect 继承于 ReactiveEffect
类,也就是说,我们传进去的函数作为一整个副作用在运行。
watchEffect
本质上是通过封装 ReactiveEffect
类实现的高级用户 API。

this.render()
就是执行我们传进行的下面的代码。
js
() => {
_setText(x2, _toDisplayString(_ctx.count));
_setClass(n4, _ctx.color);
})
_renderEffect(() => { ... })
会自动追踪其回调函数内访问的所有响应式变量(如 _ctx.count
和 _ctx.color
)。无论这些变量是否在本次更新中真正变化,只要它们被访问,就会被记录为依赖。
然后只有其中一个变量改变了,就会执行一次上面的 fn()
函数,_setText
和 _setClass
都会被执行。
这个应该是需要它们后期优化的,我明明只是修改其中一个变量,为什么另外一个不相关的 _setClass 也执行了呢?但是执行函数里面有了一定的优化,看_setText 源码部分。
上面的代码简而言之,就是 直接调用 DOM API 对变量进行精细化渲染。
也就是说?Vapor在编译时就确定了t1这个DOM节点需要更新 count 数据,不需要再 diff 一遍!
compiler-vapor 源码
IR 是重点.....
编译时,在解析文本,确定哪个 Dom 是需要精准更新的,
源码看不懂,放弃,不是做源码开发,是做业务开发的,不挣扎。
apiCreateFor
diff 不完全从运行时中删除了。比如在 v-for 中。源码位置如下:
packages/runtime-vapor/src/apiCreateFor.ts
源码如下:

在运行时 中,v-for 循环依旧使用了 diff 算法。
总结一下它执行的过程:
- 编译时:通过 AST 分析标记动态绑定与响应式依赖,生成精准的 DOM 操作指令;
- 运行时:通过
_renderEffect
实现细粒度更新,完全跳过虚拟 DOM;
所以等同于把 "diff 算法" 或者"虚拟 DOM"这些的操作提前在 vue 的Compiler 中进行了处理。运行时只需要操作 DOM API 精准渲染,不存在寻找这次改变的是哪一个位置的逻辑了,运行时简化了计算节点的逻辑。
这就是本地初始化的时候,首屏加载的速度没有明显的提升,那是因为 Compiler 增加了对于动态绑定标记与响应式依赖分析逻辑,会在本地开发或首次构建时带来额外开销。
传统 node 执行过程
具体代码如下:

简化如下
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
_Fragment,
null,
[
_createElementVNode("button", { onClick: $setup.addCount }, "add count"),
_createElementVNode(
"div",
null,
_toDisplayString($setup.count),
1
/* TEXT */
),
_createElementVNode("button", { onClick: $setup.updateClass }, "update class"),
_createElementVNode(
"div",
{
class: _normalizeClass($setup.color)
},
"我是什么颜色",
2
/* CLASS */
)
],
64
/* STABLE_FRAGMENT */
);
}

相比传统的执行过程,vapor 模式执行如下:
总结:
- diff 算法的删除减少了 patch 的执行时间。
- 虚拟 DOM 删除,减少了大量变量的存在,直接减少了内存的占用。
执行过程
1. 初始化阶段
模板编译:Vue 的编译器会将你的 <template>
部分编译为 渲染函数(render function),生成的虚拟DOM 结构大致如下:
js
function render() {
return h('div', [
h('h1', 'HelloWorld'),
h('button', { onClick: () => count.value++ }, `count is ${count.value}`)
]);
}
响应式数据绑定:count 被 ref(0) 包裹后,Vue 会将其转换为响应式对象,并建立依赖追踪。
2. 首次渲染
• 生成虚拟 DOM 树,执行渲染函数,生成初始虚拟 DOM(VNode)。
js
{
type: 'div',
children: [
{ type: 'h1', children: 'HelloWorld' },
{ type: 'button', props: { onClick: handler }, children: 'count is 0' }
]
}
• 挂载真实 DOM:Vue 将虚拟 DOM 转换为真实 DOM 并插入页面。
js
<div>
<h1>HelloWorld</h1>
<button>count is 0</button>
</div>
3. 更新阶段
- 触发响应式更新:点击按钮 → count.value++ → 触发 count 的 setter 通知依赖(副作用)。
- 重新生成虚拟 DOM:重新执行渲染函数,生成新的虚拟 DOM。
js
{
button: { ..., children: 'count is 1' } // 只有 count 文本变化
}
- Diff 算法对比差异:Vue 对比新旧虚拟 DOM,发现只有 button 的文本子节点变化(0 → 1)。
- 精准更新真实 DOM:直接修改按钮的 textContent,无需重新创建整个 DOM 树。
button.textContent = 'count is 1';
以上便是对 Vue 虚拟 DOM 工作流程的简化描述,Vue 的虚拟 DOM 工作流程在实际执行中还有一些优化细节和边界情况,这个大家有兴趣可以去看一下Vue的源码,这里不做过多的阐述,只是让大家有个印象。
感想
痛苦不会消失,只是会转移,Vapor 将耗时的逻辑从运行时转移到了编译时。但是优化编译时的逻辑,所以实现了总体性能的极大提升的同时,总的痛苦也减少了很多。
当前版本注意事项
官方的话:
- 适用场景 :
✅ 新项目初始化(createVaporApp
)
✅ 性能敏感页面(如首屏、数据看板)的局部启用(<script setup vapor>
) - 规避风险 :
❌ 避免老项目全量迁移
❌ 暂勿依赖高级功能(如 SSR 激活、异步组件)
❌ 大量依赖第三方 VDOM 组件库(兼容性存在边界问题);
不使用的具体 API
defineAsyncComponent、KeepAlive>
、<Teleport>
、<Suspense>
都没有支持,计划中。
列表渲染 v-for 中依旧存在 diff 算法,而且是完整的 diff 算法。
Vapor并不是简单粗暴地抛弃了所有diff算法,而是做了精细化的场景区分。
- 在90%的常规场景下,通过编译时优化实现了"无diff"更新
- 在复杂的列表场景下,保留了成熟稳定的diff算法
理出一个简单的表格总结:
场景 | Vapor的策略 | 原因 |
---|---|---|
简单绑定 {{ name }} | 直接更新,无diff | 编译时就能确定更新目标 |
条件渲染 v-if | 简单替换,无diff | 只需要显示/隐藏,不复杂 |
列表渲染 v-for | 保留diff算法 | 需要处理复杂的增删改移动 |