Vue 3.6 加一个 vapor 关键字:10k 方块更新从 ~16ms 降到 ~0.2ms
仓库:vuejs/core | 内存 benchmark:nicodevs/demo-vue-memory-usage
Vue 3.6 进入 RC(v3.6.0-rc.1),Vapor Mode 功能集完成。3.6 还基于 alien-signals 重写了 @vue/reactivity,响应式性能与内存占用都有改善。
Vapor Mode 是什么
Vapor Mode 是 Vue SFC 的新编译模式,目标是减小基础包体积、提升性能。它是 100% opt-in 的,只支持现有 Vue API 的一个子集,行为基本一致。依赖 VNode 或组件公共实例 proxy 的功能,在 Vapor 组件中不可用。在第三方 benchmark 中,它达到与 Solid、Svelte 5 同级的性能。
RC 阶段推荐的用法有两种:在现有应用里局部使用(性能敏感的页面用 Vapor),或者全新的小应用整体用 Vapor。
opt-in 语法
Vapor Mode 支持 template-only SFC 和使用 `` 的 SFC,不支持 Options API。
// ...
是 的简写。vapor 标记也可以放在 template 上,为整个 SFC 启用 Vapor 编译:
落到单个组件上:
import { ref } from 'vue'
const count = ref(0)
Count is {{ count }}
createVaporApp 与 vaporInteropPlugin
纯 Vapor 应用用 createVaporApp() 创建,不会引入 Virtual DOM runtime,基础包体积可大幅减小:
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
在 createApp() 创建的 VDOM 应用里使用 Vapor 组件,需要安装 vaporInteropPlugin:
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App).use(vaporInteropPlugin).mount('#app')
反过来,Vapor 应用实例也可以安装 vaporInteropPlugin,以允许内部使用 VDOM 组件,但这会引入 VDOM runtime,抵消小包体积的好处。用 render 函数或 JSX 写的组件仍是 VDOM 组件,在 Vapor 应用中使用时也需要 interop。
安装 interop 插件后,Vapor 与非 Vapor 组件可以互相嵌套,目前覆盖标准 props、events、slots,但未覆盖全部边缘情况。例如在 Vapor Mode 下使用基于 VDOM 的组件库仍可能有粗糙边缘。一般建议在应用里划分清晰的「区域」,一个区域只用一种渲染模式,尽量避免混合嵌套。
支持与不支持的 API
以下功能当前不支持,或对 Vapor 不适用:
- Options API
app.config.globalPropertiesgetCurrentInstance()在 Vapor 组件中返回null@vue:xxx逐元素生命周期事件v-memo- 组件模板 ref 不暴露
$el、$props、$attrs、$slots、$refs等属性
Vapor Mode 尽量匹配 VDOM Mode 的行为,但两种渲染模式机制根本不同,边缘情况仍可能存在细微不一致。
事件委托与 stopPropagation 陷阱
Vapor 把符合条件的事件委托到 document。每个元素存自己的 handler,一个 document 监听器遍历事件路径并调用匹配的 handler。如果任一祖先调用了 stopPropagation(),事件到不了 document,被委托的 handler 就不会执行。
以下写法会绕过委托、直接把监听器挂在元素上:
slots.default() 不是安全的 dry run
在 Vapor 中,slots.default() 不是无副作用的探查 API。调用它会执行 slot 的渲染逻辑,可能创建 Blocks 和 DOM 节点、注册响应式 effect、在 hydration 期间认领已有的 SSR DOM。不要在决定还要渲染什么之前调用 slot 去检查其输出,把 slot 渲染交给模板:
自定义指令接口
Vapor 的自定义指令接口与之前不同:
type VaporDirective = (
node: Element | VaporComponentInstance,
value?: () => any,
argument?: string,
modifiers?: DirectiveModifiers,
) => (() => void) | void
value 是响应式 getter,返回绑定值。可以用 watchEffect() 建立响应式 effect,组件卸载时自动释放。指令也可以返回清理函数:
const MyDirective = (el, source) => {
watchEffect(() => { el.textContent = source() })
return () => console.log('cleanup')
}
Benchmark 数字
JS Framework Benchmark 分数(越低越快):Vanilla JS 1.03;React 1.52;Vue 3.6 alpha 1.26;Svelte 1.15;Solid 1.11;Vue Vapor 1.08,接近 Vanilla JS。
一个实测 demo:同一个组件渲染 10,000 个方块,点按钮更新其中一个。Vue 3.5 约 16ms;Vue 3.6 约 10ms;Vue 3.6 + Vapor 不到 1ms(约 0.2ms)。同一组件只加一个 vapor 关键字,约 50 倍提升。
内存方面有三个实验,代码公开在 demo-vue-memory-usage:
| 场景 | Vue 3.6 | Vue 3.6 + Vapor | 缩减 |
|---|---|---|---|
| 10k rows | ~9.9 MB | ~2.9 MB | 3.4× |
| 10k components | ~23.2 MB | ~3.2 MB | 7.3× |
| 10k static components | ~19.2 MB | ~1.9 MB | 10.1× |
自己跑一遍:
git clone https://github.com/nicodevs/demo-vue-memory-usage
cd demo-vue-memory-usage
node benchmark.mjs
为什么能快这么多
Vapor 渲染模板不依赖 Virtual DOM。传统 VDOM 在状态变化时创建新树、逐节点对比、再 patch 真实 DOM。Vue 3 已有静态提升(static hoisting)、patch flags、树扁平化等优化,但仍要重建整棵对象树、重新检查每个动态值。
Vapor 不用 VDOM——不复制、不重建、不 diff。它靠编译器:编译组件时,编译器追踪每份 state 到它控制的那个 DOM 节点的精确路径,把它们连起来,这就是 fine-grained reactivity。这也是 Vapor 要求用 `` 的原因:给编译器一个清晰可预测的结构。
安装 3.6 beta
新建项目时 pnpm create vue@latest 会问是否使用 Vue 3.6 (beta)。已有的 3.5 应用可以在 package.json 里把 vue 指向 "beta":
{ "dependencies": { "vue": "beta" } }
但 Vue 是一整个包家族(runtime、compiler 等),构建工具也依赖其中一些。只升 vue,编译器可能还停在 3.5,而 3.5 编译器不认识 vapor 关键字。npm 下可能需要用 overrides:
{
"overrides": {
"@vue/compiler-core": "beta",
"@vue/compiler-dom": "beta",
"@vue/compiler-sfc": "beta",
"@vue/compiler-ssr": "beta",
"@vue/compiler-vapor": "beta",
"@vue/reactivity": "beta",
"@vue/runtime-core": "beta",
"@vue/runtime-dom": "beta",
"@vue/runtime-vapor": "beta",
"@vue/server-renderer": "beta",
"@vue/shared": "beta",
"@vue/compat": "beta"
}
}
然后删掉 lock 文件和 node_modules 重新安装,才会到 3.6。目前建议先试,不要急着把生产应用迁到 Vapor,它还在 beta/RC。