NOTEFrontend Engineering

Vite SPA 首屏性能预算:测量真实依赖闭包

本文结论

利用 Vite manifest 计算 SPA 首屏真实静态依赖闭包,并设置 JavaScript 与 CSS gzip 预算,拦截总构建体积或单 chunk 检查遗漏的性能回归。

一次生产构建生成了数 MB JavaScript。完成拆包后,应用入口文件的压缩体积明显下降。两个数字都没有回答实际问题:未登录用户要下载多少代码,登录页才能进入可用状态?

一套经过脱敏的企业单页应用暴露了这个差异。原始构建在启动阶段同步注册数十个路由页面和完整组件库。第一轮优化大幅缩小了入口 chunk,也就是构建工具生成的一份代码块;这份文件却不再包含首屏路由、共享依赖和对应样式。若直接把入口体积当成结果,就会高估优化幅度。

最终方案把路由级懒加载和 Vite 构建清单预算结合起来。预算同时覆盖应用入口、指定首屏路由,以及从这两个根节点可达的全部静态 JavaScript 和 CSS 依赖。暂缓加载的业务页面不进入首屏集合。

表面问题是含义不清的构建数字

三种常见指标回答的是不同问题:

指标 能回答什么 会遗漏什么
总构建体积 本次构建生成了多少文件 首屏实际请求哪些文件
入口 chunk 体积 某个入口文件有多大 共享依赖和首屏动态入口
最大 chunk 体积 哪个单独产物最重 一条用户路径所需依赖的合计成本

原始应用让全部页面跟随启动同步加载,入口文件勉强能够反映首屏成本,同时也携带了登录用户用不到的代码。路由组件改为动态导入后,入口文件按预期变小;此时只检查该文件又会遗漏首屏必需依赖。

测量失真的根因是没有定义用户路径。“首屏 JavaScript”应表示某个指定页面的静态依赖闭包,也就是从选定入口出发、沿静态导入关系能够到达的全部文件集合。

先缩小启动依赖图,再设置预算

实现先把路由元数据和路由组件加载分开。路由名称、路径、访问规则和导航结构仍可在启动时存在;每个页面组件改为返回动态导入结果的函数,只在导航选中该页面时加载。

其他启动依赖采用相同原则:

  • UI 组件及样式按需解析,避免全局安装完整组件库;
  • 默认语言随启动提供,其他语言在用户选择后加载;
  • 地图等重型功能库留在真正使用它们的页面中;
  • 生产构建输出 manifest,让生成文件名和导入关系可以由程序读取。

Vite 官方文档说明,动态导入会形成独立 chunk;构建 manifest 会记录入口、动态入口、静态 imports、动态导入和关联 CSS。结合两项能力后,检查器不必依赖每次构建都会变化的哈希文件名,也能还原启动路径(Vite 功能文档Vite 后端集成文档)。

只遍历首屏需要的依赖

首屏预算选择两个根节点:

  1. 启动框架、路由、状态和默认样式的应用入口;
  2. 未登录导航会立即选择的登录页构建入口。

检查器读取每个根节点的 JavaScript 文件和 CSS 列表,再沿 imports 字段递归。已访问集合能够防止共享 chunk 被重复计数。

collect(root):
  已访问时返回
  记录已访问
  加入 root.file
  加入 root.css
  遍历 root.imports:
    collect(child)

collect(应用入口)
collect(首屏路由入口)

检查器不会遍历所有 dynamicImports 边。全部展开会把延迟加载的业务页面重新计入首屏,懒加载形成的边界也会失去意义。若另一条公开路由本来就属于启动过程,应把它显式加入根节点集合。

根节点缺失或 manifest 引用不存在时,检查直接失败。若悄悄按零处理,路由重命名或意外内联都可能得到失真的绿色结果。

分开限制 JavaScript 与 CSS 的压缩体积

检查器读取构建产物,用同一个 gzip 实现逐文件压缩,再分别汇总去重后的 JavaScript 和 CSS。Node 的 gzipSync 可以接收字节缓冲区并返回 gzip 压缩结果,因此构建任务能稳定复算该指标(Node.js zlib 文档)。

两类资源需要独立上限。合并预算可能让 JavaScript 增长被较小的样式体积掩盖,也可能在 JavaScript 下降时放任 CSS 膨胀。脱敏构建为两类资源分别设置固定上限,任一超限都会让命令以非零状态退出。

重构后,选定首屏依赖图的 JavaScript gzip 体积约为 202 KB,上限为 350 KB;CSS gzip 体积约为 16.5 KB,上限为 50 KB。入口 chunk 本身远小于完整首屏集合,也再次说明单文件不能代替整条路径的指标。

在同一交付链路验证行为与性能

体积检查通过仍不足以证明应用可用。最终验证覆盖了多种边界:

  • 单元测试覆盖路由契约、语言加载、认证错误和登录恢复;
  • 生产构建与 manifest 生成成功;
  • 构建产物通过静态依赖闭包预算;
  • 浏览器测试覆盖桌面与移动端登录流程;
  • 移动端没有横向溢出,主要操作位于首个视口内;
  • 发布工作流调用与本地一致的验证入口。

这套顺序针对两类不同失败。路由测试和浏览器测试保护拆包后的行为;manifest 预算保护打包后的资源依赖图。缺少任何一侧,都会留下未验证的可用性或性能风险。

适用边界

压缩传输体积只是加载性能的一部分。它无法代表网络延迟、缓存状态、JavaScript 解析和执行时间、渲染成本、接口响应时间,也不能反映低性能设备的主线程压力。gzip 结果还会与 Brotli 和生产 CDN 的实际行为存在差异。

根节点必须符合真实的未登录访问路径。使用服务端渲染、Service Worker、条件启动模块,或拥有多个同等常见落地页的应用,需要扩展测量模型。有条件时,静态预算还应结合浏览器时序或真实用户监控。

可复用结论是把一条用户路径作为依赖图来优化和约束:在真实导航边界拆分代码,选定首屏必需根节点,递归统计静态导入,每个构建文件只计一次,再把预算放进同时保护应用行为的统一验证链路。

遇到类似系统问题?

先说明系统,再说明症状

如果需要生产排障、DevOps 交付或物流系统集成协作,请提供当前表现、预期结果、受影响环境、可用日志或数据样例,以及发布限制。我会从现有证据开始判断。

通过邮件开始