谁该为一个大 chunk 负责:Vite 体积问题的归因与治理
构建结束后看到大 chunk 警告,最容易做的事是调高阈值、拆一个 vendor 包,或把导入改成 import()。这些操作可能改变告警或文件分布,却未必改善用户实际加载的页面。
更可靠的做法,是把体积问题当成一条可验证的因果链:**哪个入口引入了哪些模块,这些模块为何在当前时机加载,改动后用户实际请求与功能行为是否变化。**本文聚焦于如何追踪责任、选择改动,并建立可复查的优化闭环。
先确认讨论的是哪种"体积"
排查前应明确所讨论的指标,避免用一个数字代替不同问题:
- 磁盘体积:构建文件在磁盘上的大小。
- 传输体积:文件经 gzip 或 Brotli 压缩后的大小。
- 解析与执行成本:代码下载后,浏览器还需要解析、编译和执行;不能仅由文件体积推断。
- 初始请求集合:页面进入时实际请求的入口资源及相关依赖。
- 按需请求集合:用户触发某项功能后才请求的异步资源。
因此,大 chunk 不必然意味着首屏下载了整个 chunk;首屏请求变少,也不必然意味着交互体验更好。构建产物、压缩后体积和浏览器网络请求需要分别验证。
从入口追到具体责任
生产构建会沿入口分析静态导入与动态导入关系,再生成部署资源。静态导入形成同步依赖关系;动态导入则可以创建异步加载边界。但 chunk 是构建组织模块的结果,不会自动对应业务功能,也不能仅凭文件名判断其中代码何时被请求。
可以先用构建报告定位占比突出的模块,再沿导入路径追查:它由哪个业务入口引入,哪些页面使用,是否存在重复版本,以及它是否有必须执行的副作用。报告适合提供线索,不应直接代替浏览器中的网络验证。
| 可能原因 | 排查证据 | 后续判断 |
|---|---|---|
| 依赖本身较重 | 某个库或资源在报告中占比明显 | 是否需要完整功能,是否有经项目验证的轻量入口或替代方案 |
| 重复依赖 | 相似模块出现在多个 chunk,或依赖树中存在多个版本 | 检查锁文件、依赖树、导入路径和入口关系 |
| 摇树优化未生效 | 未使用代码仍出现在产物中 | 检查 ESM 入口、模块格式、包的副作用声明及实际导入方式 |
| 加载边界不合适 | 低频功能随入口同步加载,或拆分后请求时机不符预期 | 检查静态导入链、动态导入触发点和网络瀑布 |
依赖可视化工具可以帮助定位模块,但不同工具的统计口径不一定等同于浏览器中的实际传输量。分析结论应以项目构建结果和真实页面请求复核。
摇树优化取决于模块条件,不取决于花括号
命名导入不保证产物更小。摇树优化通常要求构建工具能静态分析模块的导入和导出关系,并判断哪些代码可安全移除。以下因素都可能影响结果:
- 模块入口与发布格式:根入口可能经过多层重新导出;CommonJS 等不易静态分析的模块形态也可能限制优化效果。
- 副作用 :插件注册、全局状态修改等顶层行为可能必须保留。
sideEffects等元数据若声明不准确,既可能让无用代码留下,也可能错误删除必要代码。 - 导入路径:库若提供子路径入口,可在确认其 API、稳定性和依赖关系后比较结果;不要假设任意子路径都等价。
例如,import { Chart } from 'some-chart-library' 是否比默认导入更小,要看库的入口和发布方式,不能单凭语法判断。Rollup 文档也将副作用判断列为摇树优化的重要因素,并提醒不恰当地忽略副作用可能移除必要初始化逻辑。具体行为仍应按项目实际使用的 Vite 版本和底层构建器核对。
把低频功能移出同步入口
假设图表编辑器很少使用,却被应用入口静态导入:
js
import { openChartEditor } from './chart-editor.js'
button.addEventListener('click', () => {
openChartEditor()
})
如果确实希望用户触发时再加载,可以将导入移至功能调用处:
js
button.addEventListener('click', async () => {
button.disabled = true
button.textContent = '正在加载...'
try {
const { openChartEditor } = await import('./chart-editor.js')
openChartEditor()
} catch (error) {
showLoadError(error)
} finally {
button.disabled = false
button.textContent = '打开图表编辑器'
}
})
改动后不要只看主 chunk 是否变小,还要验证:
- 编辑器模块是否从入口同步依赖中移出;
- 用户首次进入页面时是否发出了相关请求;
- 点击后模块是否成功加载,功能是否正常;
- 加载失败时是否有反馈,部署切换时是否可能请求已被删除的旧 chunk。
动态导入提供了异步边界,但实际请求时机还可能受构建生成的预加载代码及页面行为影响。应查看网络面板,而不是仅凭文件名判断。动态导入的变量路径也受构建工具支持范围限制,任意运行时字符串不保证都能被正确纳入构建。
拆分边界要由访问方式决定
按路由拆分适用于相对独立、低频访问的页面;按功能拆分适用于少数操作才会打开的重型面板;资源拆分则适用于只在特定场景需要的资源包。拆分不是越细越好:每增加边界,都要考虑加载反馈、错误处理、请求时机和缓存影响。如果用户几乎总会立即使用该功能,或拆分没有降低入口同步负担,额外边界未必值得维护。
异步 chunk 加载失败可能由网络问题或部署版本错位引起,例如旧页面引用的资源已被新版本清理。Vite 文档介绍了 vite:preloadError 事件及相关处理方式;部署时是否保留旧资源、HTML 的缓存策略也会影响恢复能力。刷新可以作为恢复方案之一,但应防止错误持续时无限自动刷新。
配置之前,先核对 Vite 版本
Vite 的构建配置会随版本变化。当前 Vite 官方文档使用 build.rolldownOptions 配置底层构建器,并将 build.rollupOptions 说明为已弃用的别名。较早版本或既有项目可能仍采用 Rollup 相关选项。复制配置示例前,应先查看项目使用的 Vite 版本及对应文档,不要把不同版本的行为混为一谈。
也不建议把整个 node_modules 粗暴塞进单一 vendor chunk:这可能把变化频率不同的依赖绑在一起,或让原本无需进入入口链的代码被一并加载。手动分包规则还可能影响模块归属和执行时机;它不是通用的瘦身开关。是否采用自定义切分,应基于模块关系、版本支持和浏览器请求验证。
建立可复查的优化闭环
一次优化至少应留下改动前后的证据:
- 确定对象:记录受影响的页面、入口、生产构建方式和 Vite 版本。
- 保存基线:记录构建文件体积、压缩后体积、初始请求和关键交互请求。
- 追踪来源:从报告中的模块回到具体导入路径、依赖版本和业务功能。
- 形成判断:说明问题属于依赖过重、重复打包、摇树未生效,还是加载边界不合适。
- 选择最小改动:删除无用依赖、改用经验证的入口,或只在实际按需的功能处增加动态导入。
- 复测与记录:比较入口同步依赖、异步资源、页面请求和功能行为;记录未解决事项及复查安排。
chunkSizeWarningLimit 用于控制构建警告门槛,不是性能预算。提高阈值不会自动减少下载、解析或执行成本。若要在 CI 中监测体积变化,应基于项目自己的页面目标和历史基线设定指标,并为暂时无法消除的增量记录理由、责任人和复查时间。
结语
体积治理的重点不是让每个 chunk 看起来更小,而是弄清楚模块由谁引入、为何在当前时机加载,以及优化后用户是否真的少下载或更快使用功能。先追踪责任,再选择删除、替换或延迟加载;最后用构建数据、真实请求和功能回归共同验证。这样解决的才是加载问题,而不只是告警数字。
参考资料
- Vite:生产构建 --- 构建、预加载与加载错误处理。
- Vite:构建选项 --- 构建器选项及版本相关说明。
- Vite:依赖预构建 --- 开发阶段依赖优化。
- Vite:功能 --- 动态导入等功能说明。
- Rollup:配置选项 --- 摇树优化、副作用判断与手动分包说明。
- Vite:故障排查 --- 常见构建与运行问题。
- rollup-plugin-visualizer --- 构建产物可视化分析工具。