前端打包优化实战技巧与体积压缩方案
打包优化:我到底该用哪个?
最近又在搞构建优化,说实话这玩意儿每次上线前都得折腾一通。这次主要是解决首包太大、加载慢的问题,顺手把几个主流方案拉出来遛了遛。今天想聊的是三个我常用的手段:Code Splitting + 动态 import、Webpack 的 SplitChunksPlugin,还有 Vite 的预构建和按需加载。结论先放这儿:中小型项目我直接上 Vite,大型老项目 Webpack 还是稳一点,但动态拆分这块必须做,不然用户真要骂娘了。
谁更灵活?谁更省事?
先说感受:Vite 真的省事。尤其是开发阶段,启动快得离谱,热更新几乎无感。但我这边有个大后台系统是基于 Vue2 + Webpack 4 起家的,迁移到 Vite 成本太高,只能继续在 Webpack 上折腾。
核心问题就一个:怎么让首页加载的 JS 少一点?别一上来就下个 2MB 的 bundle.js,用户网速差一点直接白屏十几秒。
方案一:动态 import + React.lazy(我最常用)
这是我目前线上项目里用得最多的。原理简单:路由级拆分,用户访问哪个页面才加载哪个模块。写法也干净:
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
这里注意我踩过好几次坑:fallback 一定要加,否则会白屏;另外异步组件不能放在条件判断里,比如 if (user) import('admin'),Webpack 解析不了这种动态路径,会报错。
优点很明显:按需加载,打包后每个页面都是独立 chunk,首屏体积立竿见影地小了。而且代码层面很清晰,谁都能看懂。
缺点也有:如果页面太多,HTTP 请求会变多。不过现在 HTTP/2 普及了,这个问题没以前严重。再说了,总比全量加载强吧?
方案二:SplitChunksPlugin —— Webpack 老将出马
这个配置我改了不下十遍,文档看得头都大了。默认其实已经做了 vendor 拆分,但不够细。比如 lodash、moment 这种通用库,我想单独打成一个包,方便长期缓存。
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
},
moment: {
test: /[\/]node_modules[\/](moment|lodash)[\/]/,
name: 'libs',
chunks: 'all',
priority: 10,
},
},
},
},
};
这个配置亲测有效。打包后生成 vendors.chunk.js 和 libs.chunk.js,后面那个专门塞第三方工具库,版本不变的话浏览器能一直用缓存。
但这里有个坑:priority 得设高一点,不然会被 vendor 组吃掉。我之前没设,结果 moment 还是在 vendors 里,白配了。
还有一点要注意:如果你用了异步 import,SplitChunks 也会自动处理,不需要额外配置。也就是说它和动态加载不冲突,反而是互补的。
总体来说,SplitChunks 是 Webpack 里最硬核但也最可控的方案。适合那种对构建流程有洁癖、需要精细控制 chunk 分组的团队。
方案三:Vite 的预构建和按需加载 —— 开发体验赢麻了
新项目我基本都上 Vite 了。不是跟风,是真的香。尤其是它的依赖预构建机制,启动快得离谱。
// vite.config.js
export default {
build: {
rollupOptions: {
input: {
main: 'src/main.tsx',
about: 'src/pages/About/index.tsx',
},
},
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
return 'vendor';
}
if (id.includes('lodash') || id.includes('moment')) {
return 'utils';
}
},
},
},
},
};
Vite 底层是 Rollup,所以打包逻辑更现代一些。它默认就会把 node_modules 里的东西预构建一次,存在 node_modules/.vite 下,下次启动直接用缓存。
实际效果就是:第一次启动稍慢,之后几乎秒开。开发阶段幸福感飙升。
生产构建时,通过 manualChunks 也能实现类似 SplitChunks 的分组策略。语法稍微不一样,但逻辑一致。
不过 Vite 对 IE 支持不好,如果你还要兼容旧浏览器,建议三思。我们公司内部系统去年切 Vite 后,IE 用户投诉一堆,最后还得回 Webpack……
性能对比:差距比我想象的大
我拿同一个项目分别跑了三套方案,本地 build 后用 gzip 压缩算大小:
- 纯打包(无拆分):main.js 2.1MB → Gzipped 680KB
- SplitChunks + 静态拆分:main 320KB + vendors 450KB + libs 120KB → 总 Gzipped 590KB,首屏只下 main
- Vite + manualChunks:main 280KB + vendor 500KB → 总 Gzipped 570KB,首屏同样只下 main
看起来数字差不多?但别忘了 Vite 默认开启 brotli 压缩(可以手动关),而且产物是 ES Module,现代浏览器解析更快。实测首屏可交互时间快了 1.2s 左右。
更别说开发阶段的体验差距了。Webpack 开启 HMR 后修改文件平均热更新 800ms,Vite 基本在 200ms 内完成——这根本不是一个量级的。
我的选型逻辑
看场景,我一般选:
- 新项目,不用兼容 IE → 直接 Vite,省下的时间够喝两杯咖啡
- 老项目,Webpack 已有积累 → 上动态 import + SplitChunks,重点拆路由和公共库
- 超大型项目,多团队协作 → 自定义 chunk 分组,甚至按业务域拆包,比如 admin-vendor、report-utils 这种
还有一个小技巧:我在 Webpack 里会给每个 chunk 加 contenthash,并通过后端接口返回最新的资源列表,避免缓存问题。比如:
// 打包后生成 manifest.json
{
"main.js": "main.abc123.js",
"vendors.js": "vendors.def456.js"
}
然后前端请求这个 manifest,再动态插入 script 标签。这样哪怕 CDN 缓存了旧 HTML,也能拿到最新 JS。
这套流程改完后仍有一两个小问题,比如降级失败时的兜底策略还没完善,但无大碍,至少用户不再反馈“点进去转半天”了。
以上是我的对比总结,有不同看法欢迎评论区交流
这个话题其实还能深挖,比如结合 CDN 缓存策略、prefetch/priority 设置、甚至 SSR 下的打包差异。但我已经写了快两千字,脑子有点钝了。先这样吧。
以上是我踩坑后的总结,希望对你有帮助。如果你也在被打包折磨,不妨试试动态 import,改完立马见效。至于 Vite 和 Webpack 的战争,我觉得短期内还会持续,但趋势已经很明显了。

暂无评论