前端包体积优化是每个前端项目上线前的必修课:产物每多 100KB,首屏就可能慢几百毫秒,移动端流失率随之上升。本文用 Vite/Rollup 工具链,带你从”看清重量”到”一步步瘦身”,把臃肿的打包产物压缩下来。
一、先量化:为什么包体积值得优化
前端性能的核心指标(LCP、TTI、FID/INP)几乎都和 JS/CSS 体积强相关。在弱网或中低端手机上,一个 2MB 的 bundle 解析+执行就可能吃掉数秒。Google 的研究表明,页面加载时间从 1 秒升到 3 秒,跳出概率增加约 32%。所以前端包体积优化不是”锦上添花”,而是转化率的底线。
优化的总思路就四步:看清(分析产物构成)→ 删减(Tree-Shaking、按需引入)→ 拆分(代码分割、动态导入)→ 压缩(Gzip/Brotli + 产物策略)。下面逐一落地。
动手之前,先给项目拍张”体检照”:用浏览器 DevTools 的 Coverage 面板看哪些 JS 根本没执行,用 Lighthouse 跑一次拿到具体的体积与耗时建议。把优化前的数字记下来,等改完再跑一次对比,才能证明瘦身真的有效,而不是凭感觉。
二、可视化分析:看清每一克重量
盲目优化等于蒙眼砍树。先装一个可视化插件,把每个依赖占了多少 KB 摊在桌面上。在 Vite 项目里用 rollup-plugin-visualizer 最省事。
2.1 生成依赖关系图
在 vite.config.ts 里加一段配置,构建后会产出 stats.html,浏览器打开就能看到模块占比的”甜甜圈图”。
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'dist/stats.html',
open: false,
gzipSize: true,
brotliSize: true,
}),
],
});
打开 dist/stats.html,重点关注:哪些第三方库体积异常大、是否有被重复打包的模块、是否有整包引入却只用了其中一个函数的库。看报告时优先盯三个数:Initial JS(首屏必须下载的 JS)直接决定白屏时间;Largest chunk(最大单文件)往往是首屏瓶颈;Duplicate modules(重复模块)说明同一份依赖被多个入口各打了一份,需要靠分包或提取公共依赖解决。关于构建工具本身为什么快、怎么落地,可以回看我们之前的 Vite 实战 与 前端构建工具演进 两篇。
三、删减:Tree-Shaking 与按需引入
Tree-Shaking 依赖 ES Module 的静态结构,把”没被引用的导出”在打包时摇掉。前提是依赖必须是 ESM 且构建工具能静态分析。常见踩坑是引入了 CommonJS 版的库,导致整包被打入。
// ❌ 错误:整包引入,Tree-Shaking 失效
import _ from 'lodash';
const r = _.debounce(fn, 300);
// ✅ 正确:按需引入具名导出(ESM 可被摇树)
import debounce from 'lodash-es/debounce';
const r = debounce(fn, 300);
如果是 UI 组件库(如 Ant Design、Element Plus),务必开启官方按需加载插件(unplugin-vue-components / babel-plugin-import),避免把整个组件库拖进 bundle。
另一个常被忽略的点是 package.json 里的 "sideEffects" 字段。出于安全,构建工具默认会保留所有”有副作用”的模块(全局样式、polyfill 等),导致 Tree-Shaking 不敢下手。当你确认自己的代码没有副作用时,显式声明它能让摇树更彻底:
// package.json
{
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
上面的配置告诉打包器:除了 CSS 和 polyfills,其余模块都可以放心摇掉未引用的部分。
四、拆分:路由级与组件级动态导入
首屏只加载”当前可见”的代码,其余用动态导入(dynamic import)拆成独立 chunk,等用户真的用到时再懒加载。这是收益最高的一步。
// React 路由级懒加载
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
// 原生动态导入,Webpack/Rollup 会自动切分 chunk
button.addEventListener('click', async () => {
const { heavyCalc } = await import('./heavy');
heavyCalc();
});
也可以手动指定分包边界,把体积大且变动少的第三方库单独拆出来,利用浏览器长缓存。
拆分之后,浏览器默认是”用到才加载”,但体验上还能更进一步:用 <link rel="modulepreload"> 或构建插件,在首屏空闲时提前预载下一级路由的 chunk,让页面切换几乎无白屏。注意预载要克制,只对核心路径做,否则会抢占首屏带宽,反而拖慢首屏。
// vite.config.ts —— 手动分包
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom'],
echarts: ['echarts'],
},
},
},
},
});
五、第三方库瘦身:轻量替代一览
很多”体积刺客”都有更轻的替代品,直接换掉往往立竿见影。
| 原库(重) | 轻量替代 | 典型收益 |
|---|---|---|
| moment + moment-timezone | dayjs / date-fns | 从 ~70KB 降到 ~7KB |
| lodash(整体) | lodash-es 按需 / es-toolkit | 按需后近乎 0 多余体积 |
| echarts 全量 | 按需引入 + 定制打包 | 减少 40%~60% |
| 完整组件库 | 按需加载 + 图标瘦身 | 首屏少 100KB+ |
六、压缩与产物策略:Gzip / Brotli
源码压缩(minify)只是第一步,传输层再用 Gzip 或 Brotli 二次压缩,文本类资源通常再砍 70%。Vite 可在构建时直接预生成压缩文件。
// vite.config.ts —— 构建期生成 .gz / .br
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
viteCompression({ algorithm: 'gzip', ext: '.gz' }),
viteCompression({ algorithm: 'brotliCompress', ext: '.br' }),
],
});
同时把小于 4KB 的小文件内联成 base64,减少请求数:设置 build.assetsInlineLimit。压缩后的产物若线上报错,记得用 前端错误监控的 Source Map 方案 把堆栈还原到源码行号。
光生成压缩文件还不够,还得让服务器优先返回它们。下面是一段 Nginx 配置:当浏览器支持 Brotli(带 Accept-Encoding: br)且同名 .br 文件存在时,直接返回压缩版本,省去运行期再压缩的开销:
# nginx.conf(节选)
brotli on;
brotli_types text/css application/javascript application/json;
location ~ \.js$ {
add_header Content-Encoding br;
try_files $uri.br $uri =404;
}
七、把体积关进 CI:预算门禁
优化最怕”上线后有人又加回大依赖”。用 size-limit 在 GitHub Actions 里设一道体积预算,超阈值直接让构建失败,从流程上守住成果。
// .size-limit.json
[
{
"path": "dist/assets/index-*.js",
"limit": "180 KB",
"gzip": true
}
]
# .github/workflows/size.yml(节选)
- name: Check bundle size
run: npx size-limit
除了 size-limit,你也可以在构建脚本里遍历 dist/assets 打印各 chunk 体积,超过阈值就 process.exit(1) 让流水线变红。关键在于把这步放进合并前(pre-merge)的流水线,而不是上线后才发现体积又悄悄涨回去。
八、落地检查清单
把上面的动作收口成一份可复用的清单,每次发版前过一遍:
| 阶段 | 动作 | 预期效果 |
|---|---|---|
| 分析 | visualizer 生成 stats.html | 定位体积 Top3 元凶 |
| 删减 | ESM 按需引入 + 组件库按需 | 移除未用代码 |
| 拆分 | 路由懒加载 + manualChunks | 首屏体积下降 30%~50% |
| 压缩 | Gzip/Brotli + 小资源内联 | 传输体积再降 70% |
| 守门 | size-limit CI 门禁 | 防止体积回弹 |
按这份清单走一遍,多数中后台项目都能把首屏 JS 从 1MB+ 压到 300KB 以内。配合 VS Code 的 Import Cost 插件,写代码时就能实时看到每个依赖的代价,把优化前置到开发的每一行。




