Vue 3.6 Vapor 迁移实践:编译、测试与回归
这次把现有 Vue 项目从 3.5.44 迁到 3.6.0-rc.14,并在独立界面启用 Vapor。迁移涉及组件编译、应用入口、测试运行时和原有交互的回归,原来的业务处理可以继续保留。
最初从一个小型 Popup 开始,先验证兼容性,再把同一流程用于其他界面。后续又迁移了使用原生滚动条库的字体 Popup,以及保留 Nuxt、Element Plus 和 Reka 组件的搜索站。前者能完整采用纯 Vapor,后者需要混合模式,最终体积也呈现出不同结果。再扩展到其他独立工具和文档站,模板外的 VNode、程序化挂载与异步编辑器也暴露了新的边界。
下面保留实际代码、兼容问题和构建对照,分别看组件迁移、依赖导入、功能开关与构建链改变了什么。兼容性通过,不代表每个项目的包都会变小。
Vue 3.6 和 Vapor 分别改变什么
截至 2026 年 10 月 31 日,这次核对的 Vue 稳定版是 3.5.44,3.6 仍在 RC 阶段,试验使用的是 3.6.0-rc.14。RC 是候选发布版,后续行为和工具支持仍可能调整,因此依赖固定到完整版本号。
Vue 3.6 包含响应式系统的重构,Vapor 则是需要显式启用的 SFC 编译模式。普通组件升级到 3.6 后,仍然可以沿用原来的虚拟 DOM 渲染方式;给组件加上 vapor 标记,才是在选择另一条编译路径。官方 RC 功能说明把这两类变化分别列了出来。
SFC 是 Vue 的单文件组件,例如
App.vue。编译器会把模板、脚本和样式转换成浏览器能够执行的代码。Vapor 改变的是模板最终生成的渲染与更新代码。
普通 Vue 组件的渲染函数会生成虚拟节点,运行时根据新旧节点决定如何修改真实 DOM。Vue 已经通过静态提升、patch flag 等编译优化缩小更新范围,并非每次都完整比较整棵页面树。
Vapor 进一步把模板编译成直接创建和更新 DOM 的逻辑,并把响应式依赖与对应更新关联起来。状态变化时,可以执行相应的 DOM 更新,减少虚拟节点创建和比较的工作。

图里省略了调度、生命周期和结构控制等细节。Vapor 的更新逻辑在构建时生成,浏览器处理每次状态变化时执行这些代码。v-if、v-for 仍然需要相应的条件与列表管理。
这也解释了两个实际判断:模板代码可以大体保留,但依赖 VNode 或组件实例内部属性的代码需要检查;纯 Vapor 小应用有机会裁掉更多 VDOM 运行时代码,混用普通组件时则要重新看包体积。
先确定组件与运行时的边界
迁移前,先检查组件依赖的能力:是否使用组件库、旧式自定义指令、Options API、手写 render 函数、VNode,或通过组件 ref 读取实例内部属性。独立区域只使用 Composition API、模板与原生表单时,通常更容易完整切换和回归。
全部组件都能采用 Vapor 时,使用纯 Vapor 应用入口;需要保留普通组件时,按区域评估 vaporInteropPlugin,验证跨模式 props、事件和插槽。官方互操作说明建议保持清楚的渲染区域,减少反复交错嵌套。
最小代码示例来自一个现有 Popup。它只承担设置编辑,外部存储和执行端沿用原来的协议,适合检查模板、表单和持久化是否保持一致。
切换组件编译与应用入口
这次构建环境原本已经使用 Vite 8.3.4、@vitejs/plugin-vue 6.0.10 和 vue-tsc 3.3.13。实际编译确认,现有插件能够识别逐文件的 Vapor 标记,因此保留 vue() 配置。
首先,只更新目标包里的 Vue 依赖,并固定版本:
{
"dependencies": {
"vue": "3.6.0-rc.14"
}
}
接着,在根组件上增加标记:
<!-- src/popup/App.vue -->
<script setup vapor lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
// 原来的状态、设置读取和保存逻辑继续保留。
</script>
最后,把应用入口切换为 createVaporApp:
// src/popup/main.ts
import { createVaporApp } from 'vue';
import App from './App.vue';
createVaporApp(App).mount('#app');
vapor 标记负责选择组件的编译模式,createVaporApp 负责创建纯 Vapor 应用。这个面板没有普通 Vue 子组件,所以没有安装 interop。
我也没有打开全量 Vapor 编译开关。逐文件标记已经足够表达这次的范围,后续新增组件时也能直接看出它采用哪种模式。
Monorepo 还要检查共享依赖
同一 workspace 可以让已迁移的包使用 3.6 RC,其他包继续使用原版本。修改后同时核对 package.json、lockfile 和实际解析结果,特别是共享测试工具的 Vue peer 依赖;需要时在共享工具所在包固定原版本,避免一次局部迁移扩大到其他应用。
运行时、SFC 编译器与 SSR renderer 要保持对应版本。包管理器有发布时间检查时,只为经过确认的精确版本添加例外,保留其他依赖的检查策略。
生产能构建,组件测试却缺 helper
完成入口切换后,原来的 25 项测试和生产构建都通过了。但这些测试主要覆盖共享设置与内容脚本,并没有挂载 Popup。
补上真实 Vapor 组件测试后,出现了:
TypeError: defineVaporComponent is not a function
defineVaporComponent 是编译器生成的 Vapor 组件需要的运行时辅助函数。遇到这种错误,至少要检查两件事:编译器和运行时是否同版本,以及测试进程到底加载了 Vue 的哪个入口。
这次版本已经是 3.6.0-rc.14,问题出在入口。Vue 的包导出配置会根据 Node、bundler 等条件选择不同文件。当前 RC 的 Node 入口和普通 browser 构建不提供这次需要的 Vapor helper,生产 Vite 构建与组件测试因此走出了不同结果。

解决办法只放在目标包的测试配置中:把精确的 vue 导入指向同版本的完整 Vapor DOM 运行时。
// vite.config.ts:默认测试分支中的配置节选
import { createRequire } from 'node:module';
test: {
environment: 'happy-dom',
alias: [{
find: /^vue$/,
replacement: createRequire(import.meta.url)
.resolve('vue/dist/vue.runtime-with-vapor.esm-browser.js')
}]
}
这个 alias 不参与生产 Popup 的构建分支。生产构建继续交给 Vite 解析 bundler 入口和裁剪未使用代码,测试则使用完整 DOM 运行时验证行为。
Vue 的构建文件说明解释了 browser、bundler 和 Node 构建的用途差异。具体到 Vapor,还需要核对当前版本实际导出的 helper;只看到版本号相同,并不足以排除入口问题。
测试直接挂载 Vapor
组件测试里直接调用 createVaporApp,然后用 Vue Test Utils 的 DOMWrapper 操作真实 DOM:
// test/popup.test.ts:挂载部分节选
import { DOMWrapper, flushPromises } from '@vue/test-utils';
import { createVaporApp } from 'vue';
import Popup from '../src/popup/App.vue';
const host = document.createElement('div');
document.body.append(host);
const app = createVaporApp(Popup);
app.mount(host);
await flushPromises();
const wrapper = new DOMWrapper(host);
// 测试结束后调用 app.unmount() 并移除 host。
已有测试如果使用 Vue Test Utils 的 mount(),还要检查它是否依赖 VDOM 实例内部结构。这次下载站原有测试会读取 vm.$.devtoolsRawSetupState,直接套在 Vapor 组件上就会失败。适配时保留原来的行为断言,改成纯 Vapor 挂载和 DOM 操作;需要验证 props 更新与 emit 时,用一个真实的 Vapor 父组件作为测试宿主。
扩展存储 API 在组件测试中替换为 mock,用来检查写入的 key、字段和值;Vapor 的挂载、响应式更新、表单处理和卸载仍走实际运行时。
新增的六项测试分别覆盖旧版部分设置、总开关写回、下拉与数字滑块、页面范围至少一项、恢复默认与保存反馈,以及缺少 Chrome API 时的本地预览。
测试文件放进 test/ 后,也把它加入 vue-tsc 的 include 范围,避免测试只能执行却没有经过类型检查。
回归要同时检查数据和原生 DOM
新增的页面范围测试最先指出了一个界面不一致:仅保留「问题详情」时,点击取消,存储仍是 ["question"],但三个复选框都显示未选中。
我随后用迁移前的 Vue 3.5.44 生产产物加载真实扩展,执行同样操作,也复现了这个问题。它是原有表单实现的缺陷,这次补充回归才把它发现出来。

原来的模板手动绑定 checked,事件里计算下一组选项;当下一组为空时,又恢复为当前选项:
<!-- src/popup/App.vue:原实现节选 -->
<input
type="checkbox"
:checked="settings.enabledPageKinds.includes(option.value)"
@change="togglePageKind(option.value)"
/>
这里有两个状态生产者。用户点击时,浏览器先改变原生 checkbox 的 checked;随后业务代码拒绝空选择,恢复逻辑数据。对于原来的绑定表达式,计算结果前后仍然都是 true,更新过程可能跳过对 DOM 的再次写入,留下「数据选中、界面未选中」的状态。
修复时改用 Vue 对 checkbox 数组的 v-model,把原生控件同步交回表单指令处理,再保留「至少一项」的业务约束:
<!-- src/popup/App.vue:页面范围输入 -->
<input
type="checkbox"
v-model="settings.enabledPageKinds"
:value="option.value"
@change="savePageKindSelection(option.value)"
/>
// src/popup/App.vue:页面范围写回
const savePageKindSelection = (pageKind: ZhihuDocsPageKindSetting) => {
if (settings.enabledPageKinds.length === 0) {
settings.enabledPageKinds = [pageKind];
}
void saveSettings();
};
修复后,数组模型先接收用户操作,业务约束再恢复最后一项,表单绑定负责把选中状态同步回原生控件。同一修复分别放进 Vue 3.5 VDOM、3.6 VDOM 和 3.6 Vapor 的隔离构建后,三组都通过了浏览器回归。
这类问题只断言「存储里至少有一项」发现不了,必须同时检查实际 checkbox 的 checked。迁移表单时,数据和值类型、原生 DOM 状态、持久化结果应该一起验证。
真实扩展回归验证了哪些链路
组件测试之外,我用 Playwright 启动全新的 Chromium profile,加载最终 dist 目录,在 chrome-extension:// 页面里打开 Popup。
这一步使用真实的扩展存储、Manifest V3 环境和内容脚本。知乎请求由拦截返回的本地 HTML fixture 响应,保留问题详情页需要的最小 DOM,让真实 content script 注入并执行。
fixture 是测试用的固定输入。这里固定页面结构,目的是验证「Popup 保存设置后,内容脚本是否收到通知并改变状态」;线上知乎所有页面与动态布局仍属于另外的验收范围。
最终的七组浏览器检查如下:
| 检查 | 实际断言 |
|---|---|
| 扩展页面挂载 | 正确加载 Popup,标题和默认界面出现 |
| 旧设置兼容 | 只存部分字段时,保留旧值并补全缺失默认值 |
| 最后一项选择 | 存储保留问题详情,DOM 也恢复勾选 |
| 表单写回 | 改造强度、侧栏开关和页面范围写入原字段 |
| 内容脚本同步 | 总开关触发真实 storage.onChanged,页面状态和增强头部随之变化 |
| 键盘与重置 | 滑块按 End 后保存 1180,恢复默认回到 1040 |
| 重开与反馈 | 重载后设置仍保留,保存提示按时恢复,无页面异常 |
Vapor 产物分别在 Chromium 151.0.7922.34 和 156.0.8078.4 中通过这些检查。后者还检查了 console warning 和 error,结果均为空。默认设置截图的逐像素比较使用前者完成。
生产构建也做了范围核对:内容脚本 JS、内容脚本 CSS 和 manifest 与迁移前的 SHA-256 一致;Popup CSS 除生成的 scoped 标识外,样式声明一致。旧存储 key 和设置结构继续使用原实现。
这些证据能够支持当前 Popup 的迁移结果,但还没有覆盖线上知乎全部路由、复杂组件库互操作或其他浏览器。
包体积收益怎样比较才有意义
比较迁移阶段的采样,Popup 的 JS 从 70,186 bytes 降到 62,549 bytes,减少约 10.9%。不过这同时包含了 Vue 版本变化、渲染模式变化和小范围表单修复。
为了分清版本升级和 Vapor 的影响,我又复制同一份最终业务源码,在隔离目录里做了三组生产构建,只改变 Vue 版本、组件标记和应用创建入口。

| 隔离构建 | 原始 JS | gzip |
|---|---|---|
| Vue 3.5.44 + VDOM | 70.176 kB | 27.340 kB |
| Vue 3.6.0-rc.14 + VDOM | 73.023 kB | 28.266 kB |
| Vue 3.6.0-rc.14 + Vapor | 62.572 kB | 23.571 kB |
这里 1 kB = 1000 bytes。gzip 使用 Node zlib、level 6,对最终 JS 文件整体压缩;不计 sourcemap。隔离目录与实际应用路径不同,scoped 标识等生成细节会带来少量字节差异,因此隔离组和当时 dist 的数字分别记录。
在同一 Vue 3.6 RC14 下,Vapor 比 VDOM 的原始 JS 小约 14.3%,gzip 小约 16.6%。这组对照也说明,单纯升级到 3.6,并没有让这个面板的生产 JS 更小。
扩展从本地加载,gzip 只是便于横向比较,不能换算成实际网络流量节省。这里也没有测量 Popup 的用户可操作时间、主线程工作量或内存,因此不能把包体积变化写成启动速度提升。
完整版本、字节数、产物指纹和验证范围见迁移验证记录弹窗。
把同一流程用于其他独立界面
首个示例通过后,又迁移了下载站、上传面板,以及博客的搜索和播放器。前三类客户端分别覆盖父子 v-model、文件选择与预览、组件公开方法、原生 dialog 和 Audio;静态文章页面保留原来的 VDOM SSG。
| 产物 | 迁移前 JS/kB | 迁移阶段 JS/kB | 主要回归 |
|---|---|---|---|
| 下载站 | 89.797 | 89.717 | 24 项测试、6 组浏览器回归 |
| 上传面板 | 81.149 | 72.184 | 23 项测试、6 组浏览器回归 |
| 博客搜索与播放器 | 207.540 | 205.507 | 搜索、音频与页面切换回归 |
| 独立播放器构建 | 111.949 | 125.433 | 独立产物与 Fluid 入口的播放、表单、导入和卸载 |
这张表保留迁移阶段的采样,尚未计入后面的构建审计。它包含 Vue 版本、渲染模式与生产配置的变化,不能单独用于归因。下载站的原始体积几乎不变,上传面板减少约 9 kB,独立播放器却变大了。后续审计就从这个异常开始。
独立播放器为什么一度变大
播放器有两个发布入口:主主题把它合入客户端包,Fluid 兼容主题通过独立 ESM 文件加载。原来的独立入口使用 esbuild 和手写 SFC 插件,迁到 Vapor 后,JS 从 111,949 增至 125,433 bytes,gzip 从 40,145 增至 44,787 bytes;这时已经设置了生产编译和 Vue 的生产开关。
esbuild 的模块记录里,Vue 各运行包只解析到一份 3.6.0-rc.14,没有模板编译器或第二套 Vue。runtime-core 和 runtime-dom 仍会提供 Vapor 共用的能力,不能仅凭它们出现在模块图中就判定重复打包。
进一步检查生成代码,未启用的 hydration、过渡和互操作分支仍被保留。在保持同一份 esbuild 产物的前提下,仅使用 Oxc 做压缩,JS 就降至 100,081 bytes,gzip 降至 35,635 bytes;可读产物对照中,performHydration、hydrateNode、useVdomInterop 等未使用函数被消除。这个实验把主要增量定位到了构建阶段的死代码消除。

图中三组都已经采用 Vapor。这里只改变构建与压缩,得到的是构建链的差异;二次压缩还会改变注解、许可注释和输出格式,因此它用于定位问题,最终发布仍走保留许可注释的完整构建。
最终让独立入口复用项目已有的 Vite 与官方 Vue 插件,并保留 music-player.js、music-player.css、manifest 和 mountShengMusicPlayer 导出。这里的 ES 文件直接交给浏览器,所以显式启用完整压缩:
// build:music-player 中的输出选项,其他构建配置省略。
rolldownOptions: {
output: {
minify: true,
comments: { annotation: false, legal: true },
},
},
Vite 的 ES 库模式默认保留空白,以免影响下游构建使用的纯注解;这里通过 Rolldown 输出选项覆盖这一行为,并保留许可注释。供其他项目再次打包的库需要按消费方式决定,不能直接照搬这个设置。
最终独立 JS 为 99,900 bytes,gzip 35,386 bytes。相对优化前分别减少约 20.4% 和 21.0%;相对迁移前的稳定版产物分别减少约 10.8% 和 11.9%。这组结果包含构建工具变化,不是同构建条件下的 VDOM/Vapor 性能比较。
回归分别加载独立 ESM 和 Fluid 真实 bootstrap,检查播放、暂停、切歌、收藏、静音、音量、进度、手动添加、JSON 导入和 destroy 清理;构建检查同时保护公开文件、导出与体积预算。主主题继续覆盖音源失败重试、20 次切页连续播放、标签页播放接管和资源释放。
首轮几个 Vite 入口为什么只少了 39 字节
播放器缩小后,我又检查了知乎深色 Popup、上传 Popup、下载站和博客主题客户端。它们的模块图同样没有重复 Vue,也没有把测试用 browser 运行时或模板编译器带进生产包。
差别在构建起点:这些入口已经使用 Vite 8 的默认 Oxc 压缩。它们没有复现播放器那批未使用分支,继续换工具或追加压缩也就没有同样的空间。Vite 8 的迁移说明记录了默认压缩器的变化,不能沿用旧版本「Vite 默认使用 esbuild 压缩」的判断。

这几个客户端只用 Composition API,因此又单独对照了 __VUE_OPTIONS_API__ 的默认值 true 和显式 false。Vue 版本、Vapor 模式、业务源码和 Vite/Oxc 构建保持相同,CSS 指纹也一致。结果如下,单位均为 bytes:
| 入口及其静态依赖 | 原始 JS:开 → 关 | gzip:开 → 关 |
|---|---|---|
| 知乎深色 Popup | 62,549 → 62,510 | 23,562 → 23,550 |
| 上传 Popup | 72,184 → 72,145 | 26,615 → 26,603 |
| 下载站主页面 | 87,195 → 87,156 | 32,261 → 32,251 |
| 下载站试衣间 | 109,723 → 109,684 | 40,547 → 40,537 |
| 博客主题客户端 | 205,507 → 205,468 | 73,298 → 73,287 |
原始 JS 每个入口都只少了 39 bytes。检查生成代码,删掉的正是应用级 mixin 的追加分支。对应运行时逻辑可以简化为:
// app.mixin 内与本轮差异相关的节选,省略开发警告与返回值。
if (__VUE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) context.mixins.push(mixin);
}
其他大部分不需要的功能已经消除,所以关闭开关只剩这点差额。这是功能开关的收益;它既不能说明 Vapor 只值 39 bytes,也不能把播放器构建链的 25.5 kB 减少量算成 Vapor 自身收益。Vue 的编译期开关文档也提醒,关闭 Options API 前要核对第三方组件的依赖;博客共用的构建配置只对客户端关闭,SSR 保留原功能集。
下载站此时增加了试衣间入口,两页共享一份运行时。表中的每页数据包含该页及其静态共享 chunk,同一处 39 bytes 的物理裁剪只能计一次。这轮数字也不能和前面的历史单页面产物直接相减:页面源码与分包方式已经变了。
两个 Popup 追加压缩时,还各少了 34 bytes。逐段比较后,差异只是末尾的 sourceMappingURL 注释,没有新的运行时裁剪,因此没有给生产流程加第二遍压缩。
字体 Popup:DOM 库可以复用,指令要换契约
字体 Popup 的主要场景是长列表:本机字体的搜索、筛选、预览和键盘选择,以及域名例外列表。6 个自有 SFC 都能使用 Vapor,入口直接调用 createVaporApp,没有引入 VDOM interop。
这里使用的 OverlayScrollbars 2.16.0 是原生 DOM 库。项目直接把元素交给它增强,没有使用 Vue wrapper,因此它不需要识别组件采用哪种编译模式。需要调整的是应用怎样把 DOM 生命周期交给库,以及退出分支后怎样释放实例。
原来的 ObjectDirective 在 mounted/unmounted 中初始化和销毁;RC14 的 Vapor 使用函数指令,返回清理函数。指令执行时元素还可能没有插入,先用 watchPostEffect 等当前分支完成 DOM 插入,再测量和初始化:
// 字体 Popup 的滚动条指令节选,省略外观配置。
import { OverlayScrollbars } from 'overlayscrollbars';
import { watchPostEffect, type VaporDirective } from 'vue';
export const vOverlayScrollbar: VaporDirective<HTMLElement> = (element) => {
let instance: ReturnType<typeof OverlayScrollbars> | undefined;
const stop = watchPostEffect(() => {
instance = OverlayScrollbars({
target: element,
elements: { viewport: element, padding: false, content: false },
scrollbars: { slot: element.parentElement },
}, {
overflow: { x: 'hidden', y: 'scroll' },
});
});
return () => {
stop();
instance?.destroy();
};
};

这里复用原元素作为 target 和 viewport,关闭 padding/content 包装,把滚动条放在局部父层。Vue 管理的子节点和模板 ref 保持原样,库负责滚动条,不接管列表渲染。OverlayScrollbars 的对象初始化接口允许指定已有 viewport 和 scrollbar slot,适合这种固定 DOM 结构。
因此,「原生库可以继续用」有明确的适配前提:初始化时机正确、不移动框架管理的子节点、资源随所属分支释放。这不能自动证明某个 Vue wrapper 也兼容。回归除了看滚动条出现,还检查节点身份、列表重排、内容缩短、容器缩小、隐藏重显、12 次条件分支重建和最终卸载。
真实扩展又加载了 216 个本机字体,验证滚轮、手柄拖动、键盘预览、Enter 保存、搜索筛选、撤销、域名列表和 8 轮面板切换;原生工具栏 action Popup 的 380×480 窗口也实际检查。Chromium 151/156 各通过 27 项浏览器检查,121 条单测、类型与生产检查通过;24 个非 Popup 源文件及后台、内容脚本、用户脚本和 manifest 的指纹保持不变。
纯 Vapor 也可能比 VDOM 大一点
兼容性跑通后,同版本、同源码、同 Vite/Oxc 与关闭 Options API 的条件下,VDOM 是 111,161 bytes,Vapor 是 116,554 bytes,反而多了 5,393 bytes。
模块图里仍只有一份 Vue。最终 sourcemap 的已映射部分中,五个 Vue 运行包合计增加 4,311 bytes,六个 SFC 的代码却减少 2,469 bytes;未映射的常量、标点与胶水另增加约 3.2 kB。OverlayScrollbars 没有更换版本或源码,映射字节仅差 37 bytes,并不是主要增量。
这个面板引入了 Vapor 的组件、动态分支、列表、绑定和指令路径,节省的 VDOM 与模板代码不足以抵消新增运行能力。对同一生产文件再压缩,也没有找到播放器那种大块未消除代码,不能仅凭「变大了」就判定构建错误。
后续能确定消除的部分来自两个 boolean 筛选项。它们只表达「仅看等宽候选」或「仅看中文候选」,改成 :checked 加原生 change 写回后,不再需要通用 checkbox model 的数组、Set 和深比较支持,computed setter 与禁用条件继续保留。
这个改动只适用于明确的 boolean。前文的页面范围是数组,还带有「至少保留一项」的回滚约束;那里要同时恢复原生 DOM,不能照搬这个简化。
| RC14,同样绑定与构建配置 | 原始 JS / bytes | gzip / bytes |
|---|---|---|
| VDOM | 110,323 | 44,989 |
| 纯 Vapor | 114,770 | 45,697 |
| 模式差额 | +4,447 | +708 |
Vapor 组的绑定优化少了 1,784 bytes,VDOM 做相同优化也少了 838 bytes,所以模式差额只是从 5,393 降到 4,447 bytes。相对实际迁移前的 115,739 bytes,最终整体少了 969 bytes;这个总差额同时包含版本、功能开关和绑定变化,不能写成 Vapor 瘦身 969 bytes。
还试过用 composable 接管三个 viewport 的 ref 来替代通用指令,额外只能少约 0.6 kB,最后保留了已验证的指令实现。完整对照与验证范围见字体与搜索站迁移记录弹窗。
搜索站:保留组件库的 VDOM 边界
sheng-deepseek-search 是 Nuxt 4.6.2 的静态 SPA,设置了 ssr: false。这里通过 Nuxt 的官方配置安装互操作能力,而不是替换框架创建根应用的流程:
// nuxt.config.ts:与 Vapor 接入有关的配置。
export default defineNuxtConfig({
ssr: false,
vue: { vapor: true },
});
自有组件仍逐文件增加 vapor 标记。最终 18 个搜索、结果、阅读、收藏和对话框组件采用 Vapor;Nuxt 根应用、路由壳及必要的组件库包装层保留 VDOM。混合模式并没有要求一次改写所有第三方组件,但跨模式的实际调用需要单独验证。
编译和类型通过,弹层仍可能打不开
Reka 的 as-child 会把触发行为交给子节点,Anchor 与弹层又依赖 VNode 和 ref 的传递。首次将自有包装层全部改为 Vapor 后,编译、类型检查和静态构建都通过,点击高级搜索却打不开弹层。只保留按钮 VDOM 仍不足以修复,将整个搜索表单适配层保留为 VDOM 后,触发、Anchor 定位、输入和焦点恢复才通过。
另一个问题发生在 VueUse 虚拟列表。100 条收藏已经载入,但只使用 v-bind="containerProps" 没有建立真实 viewport ref,列表测量和 End 定位失败。保留动态 props 展开的同时,显式绑定 ref 后恢复:
<!-- 收藏虚拟列表 viewport,其他属性与列表模板省略。 -->
<div v-bind="containerProps" :ref="containerProps.ref">
<!-- 虚拟行 -->
</div>
测试同时要求渲染行数大于 0 且少于总数,再按 End 到第 100 条、Enter 预览、搜索缩短和恢复焦点,避免把空列表误判成虚拟化成功。这些结论针对当前 Reka、VueUse、Nuxt 与 RC14 的组合,不推广成整个组件库都不兼容。
全局指令还有类型边界:当前 vueApp.directive() 接受 VDOM 指令类型,Vapor SFC 因此局部导入函数指令;VDOM 包装层继续使用对象接口,两边共用 DOM 库配置。这里保留各自准确的类型,没有用断言把两套契约混成一个。
先修依赖导入,再解释互操作增量
最先确认的构建冗余来自 Reka 2.11.0 的 useId 探测:
// Reka ESM useId 模块中的能力探测节选。
import * as vue from 'vue';
if ('useId' in vue) {
// 使用 vue.useId。
}
这处 namespace 能力判断让同一份 Vue 的大量导出保留,不是安装了第二份 Vue。项目已固定 RC14,公开的 useId 确定存在,因此用 pnpm patch 改成具名导入,保留显式 ID、ConfigProvider 自定义来源与前缀,只移除当前宿主不可达的旧版本 fallback。
同条件混合产物从 1,032,146 降到 973,793 bytes,减少 58,353 bytes。这属于依赖导入修复。补丁有固定宿主版本的前提,上游修复后应删除;Nuxt 自己另一处可静态解析的 namespace 属性读取改成具名导入,最终字节完全没变,没有为它再加补丁。
即使已经修复 Reka,对照 VDOM 的 851,746 bytes,混合模式仍多 122,047 bytes。最终 sourcemap 的分区给出了主要来源:
- 新增
runtime-vapor已映射代码 115,305 bytes,其中直接vdomInterop.ts区域 31,740 bytes,其他还包括组件、props、slot、DOM、列表和过渡。 - 应用与构建生成模块的已映射部分减少 10,768 bytes,未映射部分增加 16,721 bytes;其余运行包和依赖净增加 789 bytes。
这些数值加起来才是总增量。源文件区域表示最终代码的映射归属,不等于整个模块原样打包,也不能当作该模块独立 gzip 大小;未映射常量和胶水没有强行归给模板或运行时。

纯 Vapor 也会复用 runtime-core、runtime-dom 的基础能力,出现这些包名不等于还保留完整 VDOM renderer。搜索站则确实仍用 VDOM 根应用和组件库,interop 要协调跨模式挂载、props、slot、ref、过渡和清理,不能按纯 Vapor Popup 的预期把它们删掉。
SPA 中出现水合代码,也不代表 SSR 被重新打开。当前 Nuxt 客户端入口仍根据页面数据在 createApp 与 createSSRApp 间选择,interop 对象和 hydrateVNode 又保留通用水合接口;打包器看到这些可达路径,不能仅凭 ssr: false 删除全部内部实现。RC14 互操作源码与 Nuxt 入口对应了这两条路径。
配置还能省一点,但不会消除混合模式成本
检查实际客户端图中的组件定义后,确认当前应用与组件库没有 Options API 依赖,才仅在客户端关闭该功能;同时用 Rolldown 公开的 codeSplitting.groups 将 Vue 运行包归为一组,不递归合入应用依赖,保留页面和错误页的动态入口。
单独关闭 Options API 少 4,104 bytes,单独调整运行包分组少 3,125 bytes,两项一起少 7,241 bytes。分组的收益主要在导出、标识符布局和压缩边界,不能解释为删除了大块互操作能力。全量合成单包的实验反而增加 26,473 bytes,没有采用。手动分包也会影响模块求值顺序,必须检查实际 import 图和运行结果。
| 相同最终配置,程序化 Nuxt 构建 | 原始 JS / bytes | gzip / bytes |
|---|---|---|
| VDOM | 846,619 | 297,535 |
| Vapor 混合模式 | 966,552 | 338,320 |
| 模式差额 | +119,933 | +40,785 |
VDOM 使用相同配置也减少了 5,127 bytes。因此 7,241 bytes 是混合组的构建优化总量,模式差额只减少 2,114 bytes,仍保留约 119.9 kB。它主要反映这个版本组合下两种渲染路径与互操作的成本,未来更专用的入口或版本可能继续优化;本次没有改写 Vue 私有变量或删除框架方法。
正常 pnpm build 的 CLI 产物单独从 974,082 降到 966,864 bytes,gzip 从 342,280 降到 338,495 bytes,减少 7,218 / 3,785 bytes。CLI 与程序化入口的配置有少量差异,不能拿一边的优化前数字减另一边的优化后数字。
149 条测试、类型、双模式模板类型反例、样式、维护源码 Lint 及完整静态与离线边缘构建通过。Chromium 151/156 各通过 14 组 UI 回归,包括 Element Plus 表单、Reka 弹层、100 条收藏虚拟列表、滚动条与 tooltip 清理、错误重试、取消、手机布局,以及 404 页加载和返回首页后再次搜索。优化前产物也跑了同一套检查;接口使用本地响应,没有把真实搜索和云端写入作为回归输入。全库 check 的既有限制仍是并行 .tools/ 实验中的 Lint 错误,维护源码单独复验。
两种模式的体积差异,要在项目内部比较
两次迁移都保留了相同版本、相同源码和最终构建配置下的 VDOM 对照。字体 Popup 比较的是纯 Vapor,搜索站比较的是有组件库的混合模式;图中的原始 JS 与 gzip 分轴,各轴从 0 起始:

字体面板和搜索站的业务量、依赖、入口与分包方式不同,不能把两个项目的总包差额当作互操作成本。纯 Vapor 也不保证每个小应用都比 VDOM 更小;但混合模式明确仍要保留两种渲染能力,评估时需要把这部分成本放进预算。
播放器修复的是构建链的死代码消除,字体面板简化的是明确的 boolean 绑定,搜索站另外修复了 Reka 导入并调整公开构建配置。这些改变可以改善最终产物,但只有同条件 VDOM/Vapor 对照的差额,才用于讨论该样例中渲染模式对包体积的影响。精确数字和产物指纹见两次迁移与构建审计数据弹窗。
把剩余独立入口接进来:配置里也可能藏着 VNode
后续把飞书表格工具的 Popup、Jenkins 构建历史面板和列表阅读器接入了纯 Vapor。三者都能保留原来的数据处理逻辑,但不能只批量修改 .vue 文件的脚本标记。
飞书 Popup 的一个配置模块原本是 .tsx:复选框标签由函数返回 JSX,再通过 slot 渲染。主组件改成 Vapor 后,这个标签仍是 VNode。它只负责一小段带 <code> 的说明,改成静态模板组件就能保持原样,也可以去掉该包的 JSX 插件与 TSX 配置。Jenkins 的 QuickLinks 类似,原来用 defineComponent、h 和 withDirectives 拼排查链接,改成私有模板组件后,仍复用原来的链接配置和 Tippy 实现。
另一个容易漏的是没有脚本的静态空态。纯 Vapor 根下,每个自有模板组件都要确认实际编译模式;仅有 template 的文件也显式补上标记:
<template>
<p>等待读取内容</p>
</template>
<script setup vapor lang="ts">
</script>
Tippy 本身仍使用原生 DOM。适配集中在指令:局部导入函数指令,在 post effect 中读取 getter、更新提示,节点分支销毁时停止 effect 并销毁实例。选择树继续按 tag/key 保存选择,切换语言、半选、搜索和筛选后清空预览都需要实测。

截图来自加载真实 MV3 产物的独立浏览器,表格读取、目标页写入和剪贴板使用固定替身。它验证 Popup 的组件与数据传递,不代表线上飞书复制协议已经重新验收。
程序化根 props 要按当前 RC 的协议传入
阅读器在 ShadowRoot 中挂载界面,Jenkins 则先显示可见构建,再异步替换详情数据。ShadowRoot 不是这里的障碍,根入口传 props 的方式才需要调整。
RC14 的根 props 读取实现会把函数视作 getter。直接传回调可能在读取 prop 时就执行;直接传某一刻的数组,则不能追踪后续整组替换。对应的程序化入口写成:
createVaporApp(Panel, {
rows: () => state.rows,
onClose: () => handleClose,
}).mount(host);
这里外层函数负责返回当前 prop。普通 SFC 中的 @click 等模板绑定由编译器处理,不需要给所有事件增加包装;这也是固定 RC 的实测规则,升级后还要重新核对。
阅读器回归同时检查了 frame-host 身份、iframe、草稿保护、分享 URL、窄屏布局与设置同步。Jenkins 则验证异步数据替换、120 条记录的虚拟窗口、筛选、排查链接和站点局部刷新。飞书、阅读器、Jenkins 分别通过 70、34、130 条测试,以及 Chromium 151/156 的实际产物回归。
三个纯 Vapor 入口,体积方向仍然不同
三个应用都没有重复的 Vue 运行包或浏览器模板编译器,最终变化却不同。同版本 RC14、相同业务逻辑和生产配置下,飞书 Popup 多 15,041 bytes,阅读器两页共享文件与内容脚本合计少 22,357 bytes,Jenkins 内容脚本少 3,629 bytes。

飞书多出的部分主要在所需运行时功能。最终 sourcemap 中,runtime-vapor 已映射约 53.8 kB;runtime-core 从约 44.0 kB 降到 10.1 kB,runtime-dom 从 10.4 kB 降到 6.1 kB,应用 SFC 的已映射代码还少了 945 bytes。Tippy 和 Popper 只有几十字节的压缩联动,增量不是库重复引入。
飞书的下拉菜单和选择树使用了 <Transition>。隔离实验移除过渡后,JS 少了 17,365 bytes,说明这组能力让更多运行时代码保持可达。不过,删除过渡会改变现有交互,正式版本继续保留。这个实验不能作为等价构建优化采用,也不能把 17.4 kB 全部精确分摊到某一个函数。
阅读器能裁掉的 VDOM 相关代码更多;Jenkins 原有 DOM 增强、Tippy 与业务代码仍需保留,因此整体收益较小。阅读器的两页共享文件只计一次,内容脚本属于另一个运行上下文,需要单独运行时,不能跨上下文强行共用一个全局 Vue。
关闭 Options API 的实验只让每份纯 Vapor 运行包少 39 bytes,三者都保留原生产配置,没有为了这个数字追加压缩步骤。与迁移前 Vue 3.5 实际产物比较时,还会混入版本和适配变化,所以整体前后数据另放在剩余入口的迁移与体积记录弹窗中。
VitePress:局部组件迁移,主题继续使用 VDOM
两个 lint 插件的文档站采用了不同范围。ESLint 站有自己的 Monaco RuleDemo,适合试点局部 Vapor;Stylelint 站没有自有 Vue SFC,升级构建链后继续使用默认 VDOM 主题。博客那些只在构建时生成 HTML 的 SSG 模板也保持原样。
实际验证固定 VitePress 2.0.0-alpha.19 与 Vue/SSR renderer 3.6.0-rc.14。主题的 enhanceApp用于注册组件和插件,互操作插件只在客户端安装,避免 Node SSR 无条件加载客户端 helper。为了限制动态导入的导出面,增加一个很小的客户端入口:
// vapor-client.ts
export { vaporInteropPlugin } from 'vue';
if (!import.meta.env.SSR) {
const { vaporInteropPlugin } = await import('./vapor-client');
app.use(vaporInteropPlugin);
}
默认主题和 Markdown 页面仍由 VitePress 管理,只给 RuleDemo 的 SFC 加 Vapor 标记。规则引擎在 Node 中使用的另一份 compiler-sfc 不属于浏览器运行时,也没有为文档迁移一并强制升级;它继续由原有规则测试验证。
静态生成通过,编辑器仍可能没有出现
浏览器回归先发现一处既有映射问题:公开规则已经增加场景前缀,文档传入新 ID,演示数据与诊断函数还使用旧 ID,导致在线区域空白。对齐 17 组示例、公开规则和诊断 ID 后,再加上文档关联检查;尚未提供演示的规则则显示明确提示。Monaco 0.57.0 的 hover 类型也改用当前接受的 'on'。

编辑代码后波浪线能更新,五轮正常切页也能返回编辑器,但首次懒加载还有另一条路径:库尚未加载完成时离开页面,卸载钩子只能清理已经创建的实例;旧 Promise 随后完成,仍会在脱离 DOM 的宿主上创建模型。再次进入同一示例时,模型 URI 重复,出现 ModelService: Cannot add model because it already exists!。
修复是在每轮初始化时记录 generation,示例变化或卸载时使旧 generation 失效。每个 await 之后、创建编辑器之前,检查 generation、宿主身份与连接状态;已创建的编辑器、模型和计时器照常释放,旧任务的错误也不回写新状态。
这条回归故意延迟首次 Monaco 分块,在 SPA 内导航离开并返回,再放行请求。整页刷新会销毁 JavaScript 上下文,反而掩盖模型泄漏。修复后的 151/156 浏览器都通过相同用例。这是异步资源生命周期问题,应与 Vapor 的兼容结果分开记录。
混合模式仍增加公共运行时成本
相同 RC、业务修复和最终导入配置下,ESLint 站完整静态生成后的物理 JS 从 VDOM 的 4,741,689 到混合模式的 4,838,532 bytes,增加 96,843 bytes;gzip level 6 从 1,268,052 到 1,302,386 bytes。应用入口及静态依赖为 194,614 → 291,376 bytes,增加 96,762 bytes。后一个范围不包含路由、metadata 和异步 Monaco/search 分块。
这里主要保留的是默认主题的 VDOM 与 Vapor 互操作能力。把直接 await import('vue') 改成窄客户端入口,另外减少了 4,972 bytes,但没有消除混合模式成本。这部分单独归为导入优化,不能宣称 RuleDemo 迁移让整个站点变小。
全站约 4.8 MB 的 JS 包含按需语言与路由文件,不是一次首屏下载量。统计还必须等完整 SSG 结束:仅枚举 generateBundle 中的 chunk,或跳过静态生成,都会漏掉 JS asset、后生成的 lean 页面与 metadata。最终数据以实际发布目录为准,再关联模块图追踪来源。
Stylelint 站从 VitePress 1.6.4/Vue 3.5.43 升级到相同的 VitePress 2/Vue RC,物理 JS 为 477,116 → 491,326 bytes,多 14,210 bytes。框架、主题、搜索、页面与元数据都有变化,但产物没有 runtime-vapor 或 interop,因此这笔差异全部属于版本与工具链升级。
ESLint 的 105 条、Stylelint 的 102 条测试和完整构建通过;两个站都完成 151/156 的水合、主题、本地搜索与手机布局回归。ESLint 另保留了可重复的 Monaco 延迟加载测试入口。各组文件、指纹、gzip 和计量范围见扩展与文档站构建数据弹窗。
这轮构建审计怎样复验
先从真实消费者找产物:Popup 看扩展 manifest 和 HTML,博客同时看主题客户端与 Fluid 的独立 ESM,下载站分别追主页面和试衣间。只检查一个叫 main.js 的文件,容易漏掉共享 chunk 或兼容入口。
接着沿模块图核对 Vue 的版本、入口变体、编译器和生产开关,再回看生成代码。runtime-core、runtime-dom 是 Vapor 会复用的模块,出现它们不能直接判定重复打包;模块元数据里的 renderedLength 又发生在最终压缩之前,最终体积仍以生成文件为准。
定位实验只改一个变量:播放器对同一份 esbuild 产物追加 Oxc,用来查死代码消除;首轮 Vite 入口只切换 Options API;字体面板的 boolean 绑定和搜索站的分包配置分别设置对照,最后再给 VDOM 和 Vapor 应用同样的优化。每组保留原始字节、gzip level 6、产物 SHA-256 与源码指纹。源码还在修改时,对受控构建输出单独留档,再从留档目录启动预览,保证浏览器验收和数据对应同一组文件。
普通 Vite 项目的审计使用 build({ build: { write: false, watch: null } }) 读取生成输出;不过 write: false 只控制正常输出,插件的 closeBundle 等钩子仍可能写 manifest,它不等同于完全只读。Nuxt 则通过自身的 loadNuxt / build 和客户端 Vite hook 捕获产物,不绕过框架配置调用裸 Vite;程序化比较结束后,再运行正常生产命令并回归该产物。
最后按实际入口回归。知乎的 31 项、上传面板的 23 项和下载站的 32 项测试通过,两个真实扩展分别完成 7 组、6 组浏览器检查;下载站主流程、试衣间 40 个风格切换、双栏与表单绑定也实际运行。博客继续检查连续切页播放、跨标签接管和编辑器清理。两张构建配图使用的数字、文件指纹与回归范围见构建审计对照记录弹窗。
测试音频和上传接口使用固定的本地输入,分别验证真实 Audio 和扩展运行时;远端供应商可用性属于另外的检查范围。首轮各界面的迁移记录见迁移阶段记录弹窗。这里没有测启动耗时、交互延迟或内存,包体积变化只说明当前发布文件的大小。
迁移复杂页面前,还要检查什么
最初那个 Popup 的业务适配量很小,主要因为它刚好处在 Vapor 支持较直接的范围内。换到依赖丰富的后台或网站,先从实际源码里查以下边界:
- 使用 Options API、JSX、render 函数或手工操作 VNode 的组件,需要选择保留 VDOM 区域还是改写。
- 读取组件公开实例内部属性、依赖
getCurrentInstance()的工具,需要检查调用发生在哪种渲染模式中。 - 自定义指令要核对 Vapor 接口。rc.14 的指令运行时使用函数形式和响应式 getter,旧对象式指令不能直接照搬。
- 引入普通 Vue 组件库时,要验证互操作和最终包体积;加上 interop 后,VDOM 运行时代码可能重新进入产物。
- 测试、类型工具、SSR 或框架集成,都要核对实际版本和入口。
事件处理还需要留意 RC 之间的变化。rc.14 的事件编译实现要求显式 .delegate 才可能使用 document 委托,普通 @click 默认直接监听。启用委托后,祖先阻止冒泡会影响事件到达 document。迁移时应看当前编译产物,再验证宿主中的真实交互。
本次 Popup 使用普通事件监听,没有为适配早期委托描述而批量改事件写法。
需要回退时,重新构建原来的依赖与 VDOM 入口即可。vapor 属于编译期选择,已经编译好的同一个组件不能靠运行时布尔开关切回 VDOM;保留可恢复的源码与构建基线,比额外维护一套长期双实现更适合这类小试验。
为迁移保留可重复的验证入口
每个项目沿用自己的类型检查、测试和生产构建入口;带有发布副作用的脚本要拆开执行。下面是首个示例保留的验证命令:
pnpm --filter @m-chrome-extensions/zhihu-feishu-docs-dark-extension run check
pnpm --filter @m-chrome-extensions/zhihu-feishu-docs-dark-extension exec playwright install chromium --no-shell
pnpm --filter @m-chrome-extensions/zhihu-feishu-docs-dark-extension run test:browser
check 包含类型检查、31 项测试、生产构建和浏览器产物检查;test:browser 使用独立 profile,输出 JSON、截图和录像。手动查看时,在 Chrome 扩展管理页打开开发者模式,加载目标包的 dist 目录;已有安装要先重新加载扩展,再打开 Popup。
总结
最初的小 Popup 在同版本对照里缩小了,但字体 Popup 的纯 Vapor 产物仍比 VDOM 大约 4.4 kB;搜索站兼容性通过后,混合模式仍多约 119.9 kB。后续阅读器缩小,飞书 Popup 因过渡等能力增大,文档站也保留了混合运行时成本。是否适合迁移,需要分别回答「运行是否正确」和「当前产物是否值得」这两个问题。
原生 DOM 库通常可以沿用,关键在初始化、DOM 归属和清理;依赖 VNode 的组件库包装层则要保留或重新验证 VDOM 边界。类型检查和构建能发现一部分问题,真实表单、虚拟列表、弹层、滚动与卸载才能补足行为证据。
体积优化也要保留归因。构建器更换、依赖导入修复、功能开关和业务绑定都可能让包变小,把相同优化应用到 VDOM 对照后,剩下的差额才反映这个样例的渲染模式影响。本文没有测启动、交互延迟或内存,后续若要判断性能,还需要针对实际页面另做测量。
迁移评估参考了《Vue 3.6: 给组件加个 vapor 就快了?先看看哪八样东西没了》,具体 API 与事件行为按本文所用的官方固定版本核对。
Vue 3.6 Vapor 的构建与回归记录
记录日期为 2026 年 10 月 31 日。实际扩展使用 Vue 3.6.0-rc.14;三组隔离构建使用相同最终业务源码,构建环境为 Node.js 26.12.0、Vite 8.3.4、Vue 插件 6.0.10。
原始基线有 25 项测试,新增六项 Popup 测试后共 31 项通过。Vapor 产物在 Chromium 151 和 156 中通过七组真实扩展检查;3.5 VDOM 与 3.6 VDOM 的修复后隔离产物也通过同组检查。
旧基线的最后一项复选框问题已在 Chromium 151 中复现。默认设置的原始截图为 400 × 560 像素,同环境比较差异为 0。图片中的界面来自真实扩展页面,机制图只用于解释渲染与工程边界。
JSON 中保留了版本、原始字节数、gzip 字节数、产物 SHA-256 和回归范围。gzip 固定 level 6;content script 使用本地页面 fixture 验证设置同步。没有启动耗时、主线程或内存测量,也没有把外部基准数字当作本项目结果。
字体 Popup 与 Nuxt 搜索站的迁移、兼容和构建审计
打开原文件 font-search-migrations.json
两组记录均采样于 2026 年 10 月 31 日,使用 Vue 3.6.0-rc.14。字体 Popup 为 6 个 SFC 的纯 Vapor 应用,搜索站保留 Nuxt 根应用和组件库的 VDOM,18 个自有组件采用 Vapor。JSON 中保留各项目内部的构建条件、同源码对照、原始字节、gzip level 6、产物指纹与验证范围。
字体组的 bindingOptimization 将两个 boolean 筛选的收益与模式差额分开;sourceMapping 记录运行包和六个 SFC 的已映射部分,未映射字节只保留约数。原版 OverlayScrollbars DOM API 继续使用,关键适配是函数指令、插入后初始化、原 viewport 与分支清理。121 条单测和两个浏览器各 27 项检查支持当前宿主的兼容结果。
搜索组先固定 Reka useId 补丁,再比较 VDOM 与混合模式;公开配置的两项优化同时应用到两组,最后差额为 119,933 bytes。finalCli 单独记录正常生产命令的前后产物,不与 controlledGroups 混算。149 条测试和两个浏览器各 14 组 UI 检查包括 404 与回首页恢复;全库 check 的 .tools/ 既有限制单列保留。
源码区域归属来自最终压缩 JS 的 sourcemap 区间,不表示模块独立 gzip;无 map 文件和未映射常量、标点与胶水单独累计。机制图是能力关系示意,不按框面积表达字节。记录没有启动耗时、交互延迟、内存或真实网络加载收益。
多个独立界面的构建与回归记录
下载站迁移 8 个 SFC,24 项测试与 6 组浏览器回归通过。上传面板迁移 9 个 SFC,原有 20 项测试和新增 3 项组件测试通过,6 组真实扩展回归使用本地后台响应与剪贴板替身。
博客客户端迁移搜索面板、播放器与图标组件,服务端静态页面保持 VDOM。主题浏览器主流程、边界流程和独立播放器均实际运行。独立播放器先经过 esbuild 构建,再改用 Vite 与 Oxc 优化;原始数据保留优化前后的体积、产物指纹和独立 ESM/Fluid 回归结果。
这些记录没有启动耗时、内存或交互延迟测量。构建体积的比较同时包含 Vue 版本、Vapor 编译和当前生产配置的变化。
构建审计的同条件对照与回归
打开原文件 build-audit-results.json
播放器三组固定 Vue 3.6.0-rc.14 与 Vapor,分别记录 esbuild 生产构建、同产物追加 Oxc 的定位实验,以及最终 Vite/官方 Vue 插件/Oxc 构建。其他客户端固定业务源码和 Vite/Oxc,只改变 Options API 开关;每个入口的 39 bytes 变化与播放器的构建链变化分开归因。
JSON 保留生产文件指纹、源码指纹、各页依赖的静态 chunk、二次压缩差异和浏览器回归。下载站使用受控构建的输出留档;两个页面的共同运行时只计同一处裁剪。相关字段只记录字节和验证事实,没有启动、交互延迟或内存数据。
剩余扩展与 VitePress 文档站的迁移和体积对照
三个扩展固定 Vue RC14、相同业务与原生产配置,对照只改变模式标记、根挂载与指令的必要适配。阅读器两页共享文件计一次,内容脚本独立统计,未变化的背景等文件单列。production 保留整体升级前后的全部 JS,controlled 保留同版本的 Vue 入口比较;二者范围不同。
ESLint 的 VDOM 与混合组都包括演示数据、异步生命周期修复和最终导入策略。全站 JS 在完整 SSG 结束后扫描,包含 lean 页面与 metadata;入口闭包单独记录,不包含异步 Monaco 和路由。宽导入对照另用于衡量导入优化。Stylelint 没有 Vapor 组件,其变化属于工具链升级。
所有 gzip 使用 level 6,JSON 保留逐文件指纹、模块来源、实验和两个浏览器的回归范围。受控样本不覆盖线上飞书协议、真实 CI 提交或站点登录;没有启动、交互延迟、内存或网络收益测量。