Webpack Chunk 分割实战:提升前端加载性能的关键策略
项目初期的技术选型
去年接手一个中后台管理系统重构,前端用的是 React + Webpack 5。项目上线后,首屏加载时间居然要 6 秒多,用户反馈“点开就卡住”,老板直接问“能不能快点”。我一查 Network,发现 main.js 快 3MB 了——所有页面、组件、第三方库全打在一个包里,光 lodash 就占了 100KB+。
这时候自然想到代码分割(Code Splitting)。Webpack 官方文档里提过 dynamic import + React.lazy 是最简单的方案,而且不用改太多架构。于是拍板:上 Chunk 分割,优先拆路由,再拆大组件。
核心代码就这几行
其实基础用法真不难。比如首页和设置页,本来是同步 import:
import HomePage from './pages/Home';
import SettingsPage from './pages/Settings';
改成动态加载:
const HomePage = React.lazy(() => import('./pages/Home'));
const SettingsPage = React.lazy(() => import('./pages/Settings'));
然后在 Router 外面包一层 Suspense:
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
Webpack 会自动把每个 lazy 的模块打成单独的 chunk。跑完 build,dist 目录里多了 10 多个 .js 文件,main.js 直接从 3MB 降到 800KB。看起来很美,对吧?
最大的坑:性能问题
结果上线后 QA 报 bug:切换到“数据分析”页面时,页面白屏 2 秒,比之前还慢。我本地复现一看,Network 里那个 data-analysis.chunk.js 要 1.2MB!原来这个页面用了 ECharts + 一堆自定义图表组件,全被 webpack 打进同一个 chunk 了。
更糟的是,有些公共依赖(比如 moment.js)被重复打包进多个 chunk。用 Webpack Bundle Analyzer 一扫,发现 moment 在 5 个 chunk 里都存在,总大小加起来比单文件还大。
折腾了半天发现,问题出在两点:
- 动态 import 只按路由拆,但大组件内部没再细分
- 第三方库没做共享处理,导致重复打包
这时候才意识到,光靠 React.lazy 不够,得配合 Webpack 的 splitChunks 配置精细控制。
踩坑提醒:这三点一定注意
先说 splitChunks。我一开始照着文档抄了个通用配置:
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
}
}
}
}
结果 vendors.js 背了 1.5MB 的锅,首屏还是慢。后来调整策略:把大库单独拆,小库合并。比如 ECharts 和 Ant Design 这种大块头,必须独立出来;而像 dayjs、lodash-es 这种小工具库,可以合在一起。
cacheGroups: {
echarts: {
test: /[\/]node_modules[\/]echarts/,
name: 'echarts',
chunks: 'async',
priority: 20
},
antd: {
test: /[\/]node_modules[\/]antd/,
name: 'antd',
chunks: 'async',
priority: 15
},
defaultVendors: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'async',
priority: 10,
reuseExistingChunk: true
}
}
注意这里 chunks 设为 ‘async’,只对异步 chunk 生效,避免影响首屏同步加载的资源。
第二个坑是预加载(prefetch/preload)。用户大概率会点“设置”页,但默认不会预加载。我加了 magic comment:
const SettingsPage = React.lazy(() => import(
/* webpackPrefetch: true */
'./pages/Settings'
));
这样浏览器会在空闲时偷偷下载 settings.chunk.js,实测切换速度提升明显。但别滥用,prefetch 太多反而吃带宽。
第三个坑最隐蔽:动态 import 的路径不能写变量。比如我原本想按权限动态加载页面:
// 错误示范!webpack 无法静态分析
const Page = React.lazy(() => import(./pages/${pageName}));
这会导致整个 pages 目录被打进一个 chunk,完全失去分割意义。最后只能老老实实用 switch 或 map 显式写死路径。
最终的解决方案
综合下来,我的策略是三层拆分:
- 路由级拆分:每个顶级路由一个 chunk,用 React.lazy + Suspense
- 大组件拆分:ECharts 图表、富文本编辑器等重型组件单独 lazy 加载
- 依赖精细化分组:通过 splitChunks 把大库独立,小库合并,避免重复
关键配置如下(webpack.config.js):
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
cacheGroups: {
// 单独拆 ECharts
echarts: {
test: /[\/]node_modules[\/]echarts/,
name: 'echarts',
chunks: 'async',
enforce: true
},
// Ant Design 拆出来
antd: {
test: /[\/]node_modules[\/]antd/,
name: 'antd',
chunks: 'async',
enforce: true
},
// 其他 node_modules 合并
defaultVendors: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'async',
reuseExistingChunk: true
}
}
}
}
};
同时在入口处加 preload 提示:
<link rel="prefetch" href="/static/js/settings.chunk.js" as="script">
(实际项目中用 HtmlWebpackPlugin 自动生成)
回顾与反思
改完后首屏 JS 降到 400KB,Lighthouse 性能分从 35 提到 78。用户反馈“快多了”,虽然数据分析页首次加载还是稍慢(毕竟 1MB 的图表数据),但加了 loading skeleton 后体验好多了。
不过有几个小问题没彻底解决:
- 某些低配手机上,chunk 切换时还是有轻微卡顿(可能是解析 JS 耗时)
- splitChunks 配置太复杂,新同事容易改错
但整体收益远大于成本。如果重来一次,我会更早介入——在项目初期就规划好 chunk 策略,而不是等包炸了才救火。
另外,现在 Vite 用 Rollup 做分包更简单,但老项目迁移到 Vite 成本太高,只能先这么凑合着。
以上是我踩坑后的总结,希望对你有帮助。splitChunks 配置千人千面,建议结合 Bundle Analyzer 多试几次。有更优的实现方式欢迎评论区交流,比如你们怎么处理超大组件的?

暂无评论