别盯着 dist:Vite 应用的下载与执行成本治理闭环

别急着拆包:用用户旅程治理 Vite 构建成本

构建日志里的大 chunk 警告,很容易让人立刻开始拆包、加 manualChunks,甚至把整个 node_modules 塞进一个 vendor 文件。但这往往只是把"文件看起来大"的问题,替换成"用户仍然要下载、解析和执行大量代码"的问题。

对于典型的 Vite 单页应用,更值得治理的对象不是 dist 目录本身,而是关键用户旅程中的下载与执行成本:用户首次进入首页、登录后进入工作台、打开编辑器或图表页时,究竟请求了什么、传输了多少、主线程忙了多久。

先把"产物过大"拆成三个问题

同一个 JavaScript 文件至少有三种值得记录的尺寸或成本:

  1. 构建后的原始体积:适合发现异常增长。Vite 的 chunk 警告通常基于未压缩的 JavaScript 大小;它是构建阶段的粗略信号,不等同于用户实际下载量。
  2. 实际传输体积:用户从 CDN 收到的 gzip 或 Brotli 压缩后的字节数。它取决于服务器是否正确启用压缩、缓存是否命中,以及资源是否被重复请求。
  3. 解析、编译与执行成本:即使传输体积较小,浏览器仍需解压、解析、编译并执行脚本。启动阶段的大量 JavaScript 可能占用主线程,推迟可交互时间,并增加长任务,从而影响交互体验指标。

因此,不要用"入口文件从 800 KB 降到 300 KB"直接宣布优化成功。更可靠的结论应是:关键路由首次请求字节下降,关键请求链缩短,JavaScript 执行或长任务减少,并且真实用户指标没有恶化。

建立基线:先定义要保护的用户旅程

先选 2~4 条真正重要的路径,而不是只测首页。例如:

  • 首次访问首页;
  • 登录后进入工作台;
  • 从列表进入详情;
  • 点击"编辑""导出""查看图表"等重功能。

每条路径记录以下数据:

维度 要回答的问题
网络 首次访问传输了多少 JS、CSS、字体、图片?缓存后还需要多少?
请求链 动态 chunk 是否在用户操作后才被发现,形成串行瀑布流?
CPU 脚本解析、编译、执行和长任务是否阻塞交互?
缓存 公共依赖变更是否导致大量稳定资源同时失效?
体验 LCP、INP、路由切换等待和错误率是否改善?

Chrome DevTools 的 Network 面板可以同时观察传输大小与资源解压后的大小;Performance 面板则用于确认 JavaScript 执行、长任务和主线程阻塞。两者不能互相替代。测试时还应区分冷缓存、热缓存,以及接近真实设备和网络条件的场景。

从构建报告还原"为什么被带进来"

构建分析的目标不是找到最大的矩形,而是回答三个问题:它是什么、为什么被引入、是否应该在当前路径加载。

可以将 rollup-plugin-visualizer 接入仅在分析构建中启用的 Vite 配置,并打开 gzip 与 Brotli 尺寸统计。Treemap 适合快速定位大模块和疑似重复内容,Network 视图适合追溯模块之间的加载关系,而 raw-data、list 或 Markdown 报告适合提交到 CI,做版本间差异比对。具体输出选项应以所使用版本的插件文档为准。

排查时不要只看入口 JavaScript,还要盘点:

  • 入口 chunk、共享 chunk 与异步 chunk;
  • 多个版本同时存在的同类依赖;
  • CSS、字体、图片和媒体资源;
  • public 目录中被原样复制的文件;
  • source map;
  • polyfill、初始化脚本和分析 SDK。

尤其要注意:小资源可能因为 Vite 的 assetsInlineLimit 被转换为 base64。它减少了请求数,却会增大引用它的 JavaScript 或 CSS;是否划算必须结合复用率、缓存策略和关键路径判断。

先处理"不该加载",再讨论"怎么拆"

拆包无法消除无用代码,只能改变它出现的时间。依赖分析中最常见的根因通常有下面几类。

整包导入与按需引入失效

确认业务代码是否为了一个函数、一个图标或一个日期格式化能力,引入了整个库的入口。命名导入并不天然等于按需:这取决于依赖是否提供可静态分析的 ESM 导出,以及其发布方式是否让打包器能够进行 tree-shaking。

CommonJS、动态访问与副作用边界

Tree-shaking 依赖静态分析。CommonJS 兼容层、运行时动态属性访问、难以分析的导出方式,都可能让无用代码保留。另一方面,也不要把 sideEffects 或模块副作用配置当作通用瘦身开关:样式导入、polyfill 和初始化模块可能确实需要执行,错误标记会造成线上功能缺失。

重复版本与重复能力

报告中出现两套日期库、两套图表基础库,或同一依赖的多个版本时,优先追溯锁文件、间接依赖和封装层,而不是先切 chunk。能够删除、替换或统一版本的依赖,通常比复杂的拆包策略更稳定。

静态资源和初始化代码被低估

首屏成本不只来自业务 chunk。字体、CSS 背景图、监控 SDK、国际化字典、polyfill 和应用初始化逻辑,也可能在页面可交互前占用网络或主线程。应把它们放回具体用户旅程中评估,而不是只看 JavaScript 总量。

按用户旅程划分加载边界

路由级拆分应是默认边界。 用户在首页时,通常不应下载设置页、管理后台、富文本编辑器或地图页面的完整代码。React、Vue 等框架的具体路由 API 不同,但原则一致:让路由组件通过动态 import() 成为异步边界。

ts 复制代码
// 框架无关的示意:只有用户进入报表页时才请求重模块
async function openReport() {
  const { mountReport } = await import('./features/report/mountReport')
  mountReport()
}

组件级懒加载只用于明确的重功能。 常见对象包括:

  • 富文本或代码编辑器;
  • 图表、地图、3D 可视化;
  • 导入导出、PDF 预览与文件处理;
  • 低频弹窗、帮助中心和高级筛选;
  • 首屏不可见且用户不一定触发的交互模块。

不要把每个普通按钮、卡片和小组件都动态导入。过细的边界会增加依赖链、请求调度和状态协调复杂度;对于高概率、首屏必需的小模块,静态导入往往更直接。

动态 import() 返回 Promise,因此懒加载设计必须包含三种界面状态:加载中、加载失败、重试。Vite 在动态导入预加载失败时会触发 vite:preloadError 事件,可用于统一记录错误并设计刷新或重试策略。模块成功加载后通常会被模块系统缓存,但点击事件仍应防抖,或复用进行中的 Promise,避免重复挂载和重复状态初始化。

如果用户即将明确进入某个重功能,可以在空闲时间、悬停或满足业务条件时进行有边界的预取;但预取不应把所有异步模块重新变成首屏负担。

拆得更碎,为什么可能反而更慢

代码分割的收益来自"少发不需要的代码",不来自"chunk 数量更多"。每新增一个异步边界,都要检查四个副作用:

  1. 瀑布流:入口加载完才发现异步模块,而异步模块又依赖多个共享 chunk,用户会经历多轮请求发现。
  2. 预加载竞争:Vite 会根据构建结果为动态导入生成模块预加载关系,以减少部分串行请求;但过度预加载会与图片、字体、关键 CSS 等资源竞争带宽。
  3. 异步 CSS:Vite 默认开启 CSS 代码分割。异步 JavaScript 引入的 CSS 会作为独立资源随相关 chunk 获取,因此一次"拆 JS"可能同时改变 CSS 请求链。
  4. 缓存边界失控:把变化频繁的业务代码和稳定的大依赖捆在一起,会让稳定依赖随着业务发布反复失效;反过来,过度拆分也会产生大量小资源,增加请求与调度成本。

评估拆分方案时,至少对比首次访问、二次访问、跳转到目标路由、触发重功能这四种场景的网络瀑布图,而不是只比较某一个 chunk 的体积。

让 manualChunks 服务于稳定边界

在项目确实需要显式控制 Rollup 兼容配置时,manualChunks 的价值是控制共享依赖和缓存边界,例如将跨多个重页面复用、更新频率低且体积明显的大依赖隔离出来。

但它不是"把所有 node_modules 放进 vendor"的固定答案:

  • 一个巨型 vendor 可能重新抬高首屏下载;
  • 函数形式的手动分包可能连同依赖一起被合并,结果与直觉不同;
  • 构建器或 Vite 版本升级可能改变默认合并行为;
  • 不合理的公共 chunk 会让本不相关的路由互相牵连。

Vite 不同版本及其底层构建器对配置入口和默认分包行为可能存在差异。使用 manualChunks 前,应以当前项目版本的官方文档和类型定义为准,并把"配置变更、构建结果差异报告、关键路由瀑布图"作为同一个验收项,而不是直接复制旧项目的配置片段。

一套可重复执行的排查闭环

将优化过程固定为下面六步:

  1. 复现:在接近真实设备和网络条件下录制关键路径,保留构建产物清单与 Network/Performance 记录。
  2. 定位:通过可视化报告找出大模块、重复依赖和引入链;通过 Network 找出真实传输与请求瀑布。
  3. 提出假设:例如"编辑器被首屏静态引入""两个路由各自带入同版本库""共享 chunk 导致首页预加载了报表依赖"。
  4. 最小改动:优先删除或替换不必要依赖,再引入路由级或组件级动态导入;只有共享边界明确时才调整手动分包。
  5. 重新验证:重新构建并对比报告,同时检查首次、缓存后和目标功能触发时的请求链与执行时间。
  6. 上线回看:结合真实用户监测观察 LCP、INP、长任务、资源错误率和缓存命中。若只改善实验室体积,却让目标交互等待变长,就应回退或重设边界。

把一次性瘦身变成工程制度

最有效的治理不是规定"bundle 必须小于某个固定 KB 数",而是为关键路由建立分层预算:首访与回访分别限制脚本、样式、图片和字体的传输成本,并对交互延迟或长任务设置告警阈值。

在 CI 中保存构建差异报告;依赖升级、引入编辑器、图表或监控 SDK 等变更一旦超预算,就要求说明新增的用户价值、受影响路由和补偿方案。这样,性能讨论才能从"构建日志变红了"升级为"这次发布是否增加了用户的下载与执行负担"。

压缩、关闭公开 source map、提高警告阈值都可能是发布策略的一部分,但它们不能替代依赖治理与加载边界设计。真正的目标始终是:把用户当前不需要的代码留在未来需要它的时刻,而把当前必须运行的代码保持足够少、足够快、足够稳定。

参考资料

相关推荐
RobinDevNotes6 分钟前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神1 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神1 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss2 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真3 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment3 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么3 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模