实验基线:Vue
3.6.0-rc.5+ Quasar(@quasar/app-vite 内嵌 @quasar/vite-plugin 2.0.2)
结论速览:挂载快 20%~42%(列表越大越明显);单字段高频更新快 4.5×~21.5× 且随规模拉大;5000 条 ×1000 轮的极端压力档(常规业务不会触发),VDOM 在 dev 下主线程长时阻塞到页面不可交互,生产构建下页面失效(标签页重置为空白页),而 Vapor 仍以 ~1.3-6.3ms/次流畅可用;生产构建结论复现且差距更大(仅本次最优更新模型下)。
三点前置约束:① rc 预发布版本(3.6.0-rc.5);② 编译链依赖手工补丁,非开箱即用;③ 性能收益为组件内单点局部更新的最优场景,批量/重排/跨边界场景会衰减。耗时均为 JS 主线程执行口径(不含浏览器 Layout/Paint)。
一、背景
Vue 3.6 引入 Vapor Mode(无虚拟 DOM 编译模式):标记 <script setup vapor> 的组件,编译产物不再生成 VNode——模板在编译期克隆(_template)、列表用 _createFor、依赖更新走 _renderEffect 细粒度直写 DOM,省去组件级 re-render 与 patch 整条链路。
我们的业务系统里有大量“高频小字段变更”的典型场景:实时排名、考试倒计时、监控大屏。这类场景正是 Vapor 官方宣称的核心收益区,但社区里鲜有真实项目端到端量化的数据。于是在 Vue 3.6.0-rc.5 上做了一轮完整试点:
- VDOM 应用(Quasar
createApp)内嵌 vapor 组件的互操作是否可行; - 同形同逻辑组件在两种编译模式下的挂载与更新性能差异量化;
- 摸清 rc 版的风险边界,为后续是否扩大试点提供决策依据。
二、实验设计
A/B 两组组件同形同逻辑(同一份 template/script/style,仅编译模式不同),纯 HTML + material-icons 字体类,刻意隔离 q-icon 等 Quasar VDOM 子组件的开销,让“渲染模式”成为唯一变量:
- A 组基线:
<script setup>(VDOM 编译,即线上现状) - B 组:
<script setup vapor>(Vapor 编译)
挂载壳提供 URL 开关:?render=vapor 切组、?count=N 把基础 6 卡循环填充至 N 张、?updates=N 挂载后循环 N 次“随机改一张卡的 title + nextTick”模拟高频变更。
采集点两组同形埋点:
// 挂载:performance.mark('cards-setup') → onMounted 时 measure('cards-render')
// 更新:跑完 N 次写 window.__updateBench = { total, perUpdate, count }
采集方式:本机 dev server 单实例、浏览器真实渲染采样,逐样本核验 DOM 节点数与 URL 参数一致;每组 3~5 轮 reload 取中位数,全量样本 A/B 六组零 console 报错。
测试环境:
| 项 | 值 |
|---|---|
| 宿主机 | 笔记本,Intel Core Ultra 9 185H(16 核 22 线程)/ 32GB 内存 |
| 采样环境 | win11 WSL2(内核 6.18-microsoft-standard),分配 12 vCPU / ~11GB |
| 运行时 | Node.js v26.6.0 · pnpm 10.12.1 |
| 依赖版本 | vue 3.6.0-rc.5 · quasar 2.26.0 · @quasar/app-vite 3.8.0(内嵌 @quasar/vite-plugin 2.0.2) |
| 采样浏览器 | Chromium 内核内嵌浏览器(真实渲染,非 headless) |
| dev 环境 | quasar dev 单实例,固定端口,无其他后台负载 |
| 生产复测环境 | quasar build 产物 + 本地静态服务 |
口径备注:① 性能数据均为 JS 主线程执行耗时(performance.now() / performance.measure),挂载统计截止于 onMounted,不含浏览器 Layout/Paint——肉眼感知的渲染差距会小于表内数值;② 表内绝对 ms 仅代表本 WSL2 环境(虚拟机调度存在噪声),A/B 相对倍率才是可迁移的结论;③ 同一环境内 A/B 串行采集,可比性不受绝对机器性能影响。
三、打通编译链:两个必踩的坑
在 Quasar(VDOM 主体应用)里嵌 vapor 组件,前置条件是装互操作插件:
// boot/vapor.ts
import {
vaporInteropPlugin, type App } from 'vue'
export default ({
app }: {
app: App }) => {
app.use(vaporInteropPlugin)
app.config.performance = false // 见坑 2
}
坑 1:@quasar/vite-plugin 的 AST 变换直接崩。 原版遍历 context.components,而 vapor SFC 编译时它是 undefined,一行 patch 兜底即可(重装 node_modules 后须重打):
// vue-ast-transform.js
context.components ?? new Set() // 原版直接遍历,vapor 下为 undefined
坑 2:rc.5 的 performance 插桩缺陷。 dev 下 app.config.performance = true 时,vapor renderEffect 的 endMeasure 没有对应的 start mark,performance.measure 抛 SyntaxError 直接中断挂载。boot 里显式置 false 规避(只影响 dev timeline 火焰图)。
四、数据
4.1 挂载耗时(中位数,ms)
| 规模 | A-VDOM | B-Vapor | 差异 |
|---|---|---|---|
| 100 卡 | 6.5 | 4.6 | -29% |
| 1000 卡 | 21.3 | 15.4 | -28% |
| 5000 卡 | 333 | 192.9 | -42% |
挂载耗时随卡片数近似线性,Vapor 斜率更低;省的是 VNode 构建与 diff,但模板克隆和绑定创建还在,属结构性优势,量级中等。注:挂载耗时统计到 onMounted 的 JS 执行,两组 DOM 节点创建数量一致,Layout/Paint 开销相同且不在统计内,首屏感知差距小于表内数值。
4.2 更新路径(单字段高频写,核心价值区)
| 规模 | A perUpdate(ms) | B perUpdate(ms) | A/B 比 |
|---|---|---|---|
| 200 卡 × 100 次 | 1.034 | 0.131 | 7.9× |
| 1000 卡 × 1000 次 | 17.598 | 1.018 | 17.3× |
| 5000 卡 × 1000 次 | 无法完成 | 1.262 | — |
更新模型说明:每轮“随机改一张卡的 title + nextTick”——单组件内部单点字段变更,是 Vapor 收益的最优路径;数组整体替换、批量修改多条、跨 VDOM→Vapor 边界的 props 下发等模式未测,倍率会明显衰减(见 §五未覆盖边界)。
单字段变更只触发对应 renderEffect 直写 textContent,没有组件级 re-render + patch。A 组在 5000 卡 × 1000 次档位连续 3 次把主线程占死到自动化通道超时(按 1000 卡斜率外推约 50~90ms/次、累计 50~90 秒连续阻塞);同规模下 B 组全程可交互,总耗时 1.26 秒。这已经不是性能差异,是可用性差异(生产构建下同档位 A 组页面失效,见 §4.3)。需要强调:该现象仅在“5000 条 ×1000 轮高频更新”的极端压力下触发,常规规模(≤1000 条)两组均正常工作。
JS 堆内存(V8 heap)方面:5000 卡挂载后稳态,A 组 31.32 MiB vs B 组 27.68 MiB(-11.7%,组内波动 <0.03%)。注:省的是 VNode 对象;DOM 节点内存不在 JS heap 统计内,两组 DOM 侧占用基本一致。
4.3 生产构建复测(quasar build + 静态服务)
| 指标 | A-VDOM | B-Vapor | 差异 |
|---|---|---|---|
| mount-1000 | 37.3 ms | 29.7 ms | -20% |
| update 200卡×100次 | 1.171 ms/次 | 0.262 ms/次 | 4.5× |
| update 1000卡×1000次 | 28.07 ms/次 | 1.303 ms/次 | 21.5× |
| update 5000卡×1000次 | 页面失效,无法完成 | 6.247 ms/次 | — |
生产环境 VDOM 自身变快(无 dev 开销),小规模档(200 卡)两组差距收敛到 4.5×;但大列表档差距不仅复现还更大:1000 卡 21.5×(dev 为 17.3×)。5000 卡 A 组在生产构建下的表现:导航超时、连续 3 次读取无结果后标签页被重置为空白页(可能为渲染进程崩溃或 CDP 代理通道丢失,自动化采集难以完全区分;dev 下则是主线程长时阻塞、页面冻结但标签页存活)——“极端压力下不可用”的结论在 dev 与生产双环境确认,B 组同规模稳定完成,全程可交互。再次强调约束:这是 5000 条 ×1000 轮高频更新的极端工况,常规业务列表规模不会触发。
数据卫生说明:① 1000/5000 卡生产档为同批次 A/B 串行采集,组内互比有效;② 跨环境(dev/prod)绝对值受采集批次运行环境影响,不宜直接混比——生产 1000 卡 A 组 28.07ms 高于 dev 的 17.6ms 即属批次差异,不构成“生产比 dev 慢”的推论;③ A/B 相对倍率不受此影响。
4.4 扩大试点:考试倒计时(低复杂度端点)
再把一个真实业务形态——考试倒计时徽章墙(每秒 tick、多实例并排模拟监考墙)做成同形双版,interval=250ms 加速压测,各 3 样本中位数:
| 规模 | A perTick(ms) | B perTick(ms) | A/B 比 |
|---|---|---|---|
| 8 实例 | 0.7825 | 0.5725 | 1.37× |
| 100 实例 | 2.4475 | 1.6087 | 1.52× |
收益方向一致但量级小得多——倒计时场景组件树浅(单层 v-for、无子组件),VDOM patch 成本天然低。结论:Vapor 的收益随组件树复杂度分化,“深列表 + 子组件多 + 高频小变更”形态才能命中 7.9×+ 收益区。
五、风险边界清单(升级 rc/beta 后必须回归)
- 禁用能力:Options API、
v-memo、getCurrentInstance()、组件 refs 的$el/$props/$attrs/$slots; - 事件委托至 document:vapor 组件的事件是委托的,祖先链上任一
stopPropagation()会吞掉组件事件(@[event]动态事件名可绕过,该绕过属 rc 版行为,正式版存在变动风险)——试点中倒计时徽章的点击暂停/恢复交互实测正常,但这个边界要记牢; - 禁混 VDOM 子组件与具名插槽:官方 interop 只保证 props/events/slots 基本面,Quasar 这类 VDOM 组件库的复杂交互组件别往 vapor 组件里塞;
- 父级应用必须先装
vaporInteropPlugin; - 编译链 patch 重装即失:
vaporSFC 编译崩是典型症状,先检查 patch 还在不在。
主流组件库(Vuetify/Element Plus/Naive UI 等)目前均无 vapor 构建产物,所以现阶段合理姿势是:VDOM 主体不动,性能敏感的纯展示叶子组件局部 vapor 化。
本次实验未覆盖的边界(以下场景未验证,结论不适用):
- 列表 key 增删/移位/乱序重排——本次 v-for 固定条目只改字段,
_createFor的 key 重排行为与性能未验证; - 父级 VDOM 组件高频下发 props 给 Vapor 子组件——interop 桥接层的响应式适配开销未压测,该真实业务路径下倍率会衰减;
- 跨 VDOM-Vapor 边界的
provide/inject高频更新——仅静态可用性验证; - 自定义指令作用于 Vapor 组件的高频更新路径;
- SSR / Hydration——整套实验为客户端 CSR,rc 版 Vapor 水合存在官方已知边缘 case,结论不适用于 SSR 项目;
- Vapor 组件内部使用
Teleport。
六、结论
- 互操作可行:VDOM 应用内嵌 vapor 组件,props/events/事件交互实测正常,试点全程零报错、零视觉差异;
- 核心价值在细粒度更新:单点字段更新 4.5×~21.5× 且随规模拉大(最优路径口径,批量/重排/跨边界场景会衰减),5000 条极端压力档构成“VDOM 不可用 vs Vapor 可用”的可用性差异(dev 主线程阻塞、生产页面失效)——实时排名、倒计时、监控大屏这类场景收益显著;
- 挂载提升稳定但中等(20%~42%),别为挂载单独迁移;
- 收益随组件树复杂度分化:浅组件(如单层倒计时)只有 1.3~1.5×,深列表才是主战场;
- 当前定位试点:rc 版 API 不稳定 + 编译链依赖手工 patch,等 3.6 正式版和组件库生态跟上再扩大。
一句话:Vapor 不是噱头,在适配场景下数据撑得起“细粒度更新快一个量级”;但它高度挑场景——优先改造深嵌套、高频局部变更的展示列表,不要对简单浅组件寄予过高期望。