12k
All articles

什么是 Vue Vapor Mode?

Vue Vapor Mode 详解:SFC 如何编译为直接 DOM 操作,Vue 3.6 RC 有何变化,以及事件和插槽的常见陷阱。

OpenReplay Team
OpenReplay Team
什么是 Vue Vapor Mode?

Vue Vapor Mode 是一种针对 Vue 单文件组件的编译模式,它将模板编译为直接的 DOM 操作,因此渲染和更新过程无需创建或 diff 虚拟 DOM,从而减少了基础包体积、更新开销和内存占用。

Vapor 已经在各类技术大会上亮相了一段时间,但 Vue 官方文档中至今仍没有关于它的页面。相关细节目前都记录在 Vue core 的发布说明中。

本文将介绍 Vapor Mode 改变了组件的哪些运行方式,以及它在发布周期中所处的位置,同时还会讲解 Vapor 中两个会静默失败、极易踩中的行为。

核心要点

  • Vapor Mode 将 Vue SFC 编译为直接的 DOM 操作,而非 VNode 创建与 diff,这正是其包体积更小、更新更快的根源。
  • Vapor Mode 在 Vue 3.6 的候选发布版中已功能完备,但尚不稳定:3.6 线仍处于预发布阶段,GitHub 上的 v3.6.0-rc.9(2026 年 9 月 18 日)标记为 Pre-release,而 Latest 标签仍停留在 3.5 线的 v3.5.43(2026 年 9 月 17 日)。
  • Vapor 通过 <script setup vapor>、<script vapor> 或 <template vapor> 按组件选择性启用,且仅适用于纯模板 SFC 以及使用 script setup 的 SFC;不支持 Options API。
  • createVaporApp() 挂载的是纯 Vapor 应用,永远不会加载虚拟 DOM 运行时;而安装 vaporInteropPlugin 以承载 VDOM 组件会把该运行时重新引入,抵消体积收益。
  • Vapor 组件没有 VNode,也没有公共实例代理,因此 getCurrentInstance() 返回 null,app.config.globalProperties 不生效,组件模板 ref 也不再暴露 $el、$props、$attrs、$slots 或 $refs。

Vue Vapor Mode 是如何工作的?

v3.6.0-rc.1 发布说明 将 Vapor 描述为编译单文件组件的第二种方式,目标是更小的初始包体积和更好的性能。这一切都不是默认发生的:需要你自己逐个组件地开启,而它所覆盖的那部分 Vue API 的行为大体上仍符合你的预期。你编写的源码不会改变,改变的是产物:编译器不再生成产出 VNode、交由运行时与上一棵树做 diff 的渲染函数,而是生成只创建一次节点、并把每个响应式依赖直接连线到它所控制的具体 DOM 更新的代码。

去掉虚拟 DOM 同时消除了三项开销。在纯 Vapor 应用中,diff 运行时本身根本无需打包,因此基础包体积更小。更新过程跳过了 VNode 分配和树比较,因此一个 ref 的变化只触及一个文本节点或一个属性。而且由于渲染之间不再保留影子树,每个组件的内存占用也随之下降。

组件本身看起来就是普通的 Composition API 代码:

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

const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>

<template>
  <button @click="count++">{{ count }} / {{ doubled }}</button>
</template>

与以 VDOM 模式编译的同一组件相比,唯一的差别就是 vapor 属性。

发布状态:功能完备,但尚未稳定

Vapor Mode 尚未随任何稳定版 Vue 发布。在 vuejs/core releases 列表 中,3.6 线仍处于候选发布阶段:2026 年 9 月 18 日发布的 v3.6.0-rc.9 带有 Pre-release 标签,而 Latest 标签属于 2026 年 9 月 17 日发布的 v3.5.43。npm 上的情况也一致:vue 包 的 latest dist-tag 指向 3.5.43,候选发布版则位于单独的 rc 标签下。并不存在 3.6.0 的稳定标签。

真正成立的说法范围更窄,且很容易与”已正式发布”混为一谈。v3.6.0-rc.1 发布说明 指出,Vapor Mode 在 Vue 3.6 RC 中已功能完备,这也正是 3.6 线得以进入候选发布阶段的原因。功能完备描述的是范围,而非稳定性。Vue 自己的发布策略对所有预发布版本一视同仁:不稳定,其存在是为了让团队测试某个构建与自身技术栈的契合度,而非用于生产环境,并且可以在相邻构建之间自由打破兼容性。如果你要安装,请锁定确切版本号。

如何启用 Vapor Mode?

Vapor 是按组件启用的,而非按项目启用。有两类组件符合条件:只包含模板的单文件组件,以及使用 script setup 编写的组件。基于 Options API 的组件完全无法编译为 Vapor。对于符合条件的组件,有三种标记方式:完整形式 <script setup vapor>、其简写 <script vapor>,以及在 template 标签上加 vapor 标记,这会把整个文件编译为 Vapor。

<!-- Form 1: the explicit form -->
<script setup vapor>
  // ...
</script>

<!-- Form 2: shorthand for <script setup vapor> -->
<script vapor>
  // ...
</script>

<!-- Form 3: marks the whole SFC as Vapor -->
<template vapor>
  <!-- ... -->
</template>

实际带来的后果是需要一次审计工作。任何仍用 data、methods 或 mounted 编写的组件,都必须先转换为 script setup,vapor 标记才会对它产生任何作用。

可以混用 Vapor 和虚拟 DOM 组件吗?

Vapor 与虚拟 DOM 组件可以混用,而应用的挂载方式决定了最终打包进来的内容。如果所有组件都是 Vapor,就用 createVaporApp() 挂载:这条路径会把虚拟 DOM 运行时排除在构建之外,这正是基础体积大幅下降的来源。用 createApp() 挂载的应用必须先安装 vaporInteropPlugin,才能渲染 Vapor 子组件。Vapor 应用同样可以安装该插件来承载虚拟 DOM 子组件,但这样做会把运行时重新引入,从而放弃大部分体积收益,正如 3.6.0-rc.1 发布说明 所解释的那样。

// Pure Vapor: the VDOM runtime is never loaded
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')

// Existing VDOM app hosting Vapor components
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App).use(vaporInteropPlugin).mount('#app')

以渲染函数或 JSX 编写的组件同样属于虚拟 DOM 组件,因此在 Vapor 应用中也需要 interop。互操作并非万无一失。将一种模式嵌套进另一种模式能处理常规的 props、事件和插槽,但尚未覆盖所有边界情况,基于虚拟 DOM 构建的组件库在 Vapor 下仍可能出现异常。Vue 团队的建议是:让应用的每个区域使用单一渲染模式,并尽量减少混合嵌套。

Vapor 组件放弃了哪些能力?

Vapor 组件没有 VNode,也没有公共实例代理。rc.1 发布说明中不支持清单上的每一项,都源自这一条边界,且每一项都有具体后果:

Vapor 中不支持的能力实际会出什么问题
Options API使用 data、methods 或生命周期选项的组件完全无法编译为 Vapor。
app.config.globalProperties插件注入的全局属性在 Vapor 组件内不存在;需要什么就显式注入什么。
getCurrentInstance()返回 null,因此依赖内部实例的第三方代码在 Vapor 组件中会失效。
@vue:xxx 元素生命周期事件元素级钩子已移除;改用模板 ref 配合 onMounted。
v-memo手动 memo 化的”逃生舱”不可用,必须移除。
组件模板 ref 上的 $el、$props、$attrs、$slots、$refs父组件伸手访问子组件的模式会失效;应把契约迁移到 props 和 emits 上。

发布说明对”行为一致到什么程度”也措辞谨慎。Vapor 的目标是与虚拟 DOM 行为一致,但两个渲染器的构建方式差异如此之大,预计在边界情况下会出现细微不一致;而这类细微差异只有在旧行为已被文档化的前提下,才算作破坏性变更。

开始之前值得知道的两个陷阱

事件委托与 stopPropagation()

在 rc.1 的设计中,可委托的事件在 document 层级处理。元素保留了它的处理函数,但并没有任何监听器绑定到元素本身。document 上的单个监听器承担全部工作:它沿着事件的传播路径,触发沿途遇到的任何处理函数。如果某个祖先元素在冒泡过程中调用了 stopPropagation(),事件就永远到不了 document,因此该处理函数也永远不会执行。有三种写法会跳过委托、直接把监听器挂到元素上:@[event]="onClick"、v-bind="{ onClick }" 和 v-on="{ click: onClick }"。

<script setup vapor>
const onClick = () => save()
</script>

<template>
  <!-- the ancestor stops propagation, so a delegated handler never fires -->
  <div @click.stop>
    <button @click="onClick">Save</button>
  </div>

  <!-- binds directly to the element instead -->
  <div @click.stop>
    <button v-on="{ click: onClick }">Save</button>
  </div>
</template>

这种失败模式是静默的。不抛异常,不打日志,错误监控报告的是一次健康的会话,而用户正点击着一个毫无反应的控件。能把它暴露出来的手段是会话回放(session replay),因为你可以观察到”点击之后没有任何 DOM 变更”——这正是处理函数从未执行的特征。同样的情况也出现在 Vapor/VDOM 边界处,那里的互操作缺口往往表现为渲染异常而非抛出错误。

RC 线中的大部分改动集中在 hydration 和插槽上,只有少数几处修复涉及事件监听器的合并方式,因此请锁定一个确切的 RC 版本,并阅读你所安装版本对应的 minor 分支 CHANGELOG,而不要想当然地认为 rc.1 的描述依然适用。

slots.default() 是在渲染,而不是在查询

在 Vapor 中,调用 slots.default() 并不是对插槽的一次无副作用查看。该调用会执行插槽的渲染代码,可能会构建 Block 和 DOM 节点、建立响应式副作用,并且在页面正在 hydration 时接管服务端已发送的 DOM。因此,VDOM 中常见的”调用插槽以判断是否渲染后备内容”的习惯,在 Vapor 中是有副作用的。

<script setup vapor>
import { useSlots } from 'vue'
const slots = useSlots()
// Wrong: this call renders the slot rather than inspecting it
const showFallback = !slots.default?.()
</script>

应在模板中表达这一判断,并让模板来负责插槽渲染:

<template>
  <slot>Fallback</slot>
</template>

现阶段谁适合尝试 Vapor Mode

发布说明在当前阶段列出了两种用法:把 Vapor 引入现有应用的某一部分,例如某个对渲染速度敏感的页面;以及从零开始用 Vapor 编写一个小型新应用。相应的推论也值得明说:只允许使用稳定版依赖的项目、基于 VDOM 组件库构建的界面、以及仍停留在 Options API 的代码库,都不是好的首批候选对象——第一种根本无法安装预发布版本,后两种恰好正中互操作缺口和不支持清单的痛点。

围绕 Vapor Mode 做规划

把 Vapor Mode 当作一项你今天就能在单个页面上评估的编译器变更,而不是一次需要排期的迁移。锁定一个确切的候选发布版本,把某个列表密集或动画密集的页面转换为 <script setup vapor>,并在围绕稳定版 3.6 做任何规划之前先查看 vuejs/core 的 releases 列表。

常见问题

Vapor Mode 能和 Nuxt 一起用吗?

可以,但仅作为实验性特性,且只支持 Vue 3.6 及以上版本。Nuxt 会让应用根部保持在虚拟 DOM 上,同时允许你把单个组件或页面标记为 Vapor,从而逐步采用。目前还无法构建全 Vapor 的 Nuxt 应用;指向 Vapor 组件的模板 ref 不会返回元素;如果安装的 Vue 版本过旧,Nuxt 会发出警告并自动关闭该选项。

要获得 Vue 3.6 的响应式性能提升,必须启用 Vapor Mode 吗?

不需要。3.6 线在 alien-signals 之上重建了 Vue 的响应式核心,由此带来的速度和内存收益适用于该发布线上的每一个应用,无需开启任何开关。Vapor Mode 是一项独立的、按组件生效的编译变更,因此运行在 3.6 上的标准虚拟 DOM 应用无需在任何地方启用 Vapor,就已经享受到了响应式方面的改进。

Vapor Mode 支持服务端渲染和 hydration 吗?

在早期 alpha 构建未包含该能力之后,SSR hydration 已被纳入 Vue 3.6 候选发布版的 Vapor 特性集。hydration 的正确性是整个预发布线中改动最密集的领域之一,因此请锁定一个确切的候选发布版本,并在真实页面上测试服务端输出、hydration 以及 mismatch 恢复,而不要想当然地认为它与虚拟 DOM 渲染完全对等。

第三方组件库能在 Vapor 组件内部正常工作吗?

先测试再决定。以渲染函数或 JSX 形式发布的组件仍属于虚拟 DOM 组件,需要互操作支持;而调用 getCurrentInstance 或读取 globalProperties 的库代码在 Vapor 组件内部将一无所获。基于 provide 和 inject(而非组件实例)构建的东西通常仍能正常工作,这也是 Nuxt 表示其自身大部分组合式函数和内置组件在 Vapor 下无需改动的原因。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.