Common分离实践:提升前端项目可维护性的关键技术方案
优化前:卡得不行
上个月接手一个老项目,首页加载时间动不动就5秒往上飙,用户反馈“点进去以为页面挂了”。我本地跑起来也是一言难尽——白屏时间长,JS bundle 超过 3MB,首屏关键资源全塞在一个 vendor.js 里。最离谱的是,明明只是进个登录页,却把整个后台系统的组件、工具函数、甚至图表库全打包进来了。
这哪是前端,简直是“前端负重训练”。
找到瓶颈了!
先用 Chrome DevTools 的 Performance 面板录了一次加载,发现主线程被一大块 JS 解析和执行占满,持续 2.8 秒。接着打开 Network 看资源,vendor.js 3.2MB(gzip 后也有 980KB),而且是 render-blocking 的。再用 Webpack Bundle Analyzer 一分析,好家伙,Lodash、Moment、ECharts、Axios、一堆公共组件全挤在同一个 chunk 里,不管哪个页面都得背这个大包。
问题很明显:**没有做合理的代码分割(Code Splitting),所有公共依赖一股脑打包进初始 bundle**。这就是典型的“Common 不分离”问题。
试了几种方案,最后这个效果最好
一开始我想着用 Webpack 的 SplitChunksPlugin 自动拆分,但默认配置太保守,只拆 node_modules 里大于 30KB 的包,结果 Lodash 和 Moment 还是混在一起。后来我手动配置 cacheGroups,把高频、大体积的公共依赖单独抽出来。
折腾了半天,发现光靠自动拆还不够,有些业务组件虽然被多个页面引用,但并不是首屏必需的,比如“用户设置弹窗”、“导出功能模块”,这些也应该动态加载。
最后定下来的策略是:核心运行时依赖 + 高频公共库 单独拆成 common.js,非首屏组件用动态 import 按需加载。
核心代码就这几行
重点看 Webpack 的 optimization.splitChunks 配置。我直接上最终版配置:
// webpack.config.js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 抽离核心运行时和框架
vendor: {
name: 'vendor',
test: /[\/]node_modules[\/](react|react-dom|react-router|axios)[\/]/,
priority: 20,
chunks: 'initial'
},
// 抽离大型工具库
common: {
name: 'common',
test: /[\/]node_modules[\/](lodash|moment|echarts)[\/]/,
priority: 10,
chunks: 'initial'
},
// 公共业务组件(非 node_modules)
shared: {
name: 'shared',
test: /[\/]src[\/](components|utils|hooks)[\/]/,
minChunks: 2, // 至少被两个 chunk 引用
priority: 5,
reuseExistingChunk: true
}
}
}
}
这里注意我踩过好几次坑:chunks: 'initial' 很关键,它确保这些公共包只在入口文件中加载,而不是每个异步 chunk 都重复打包。另外 priority 必须设清楚,否则 Webpack 会按默认规则合并,导致拆分失效。
对于非首屏组件,比如一个“高级筛选面板”,我直接改成动态导入:
// 优化前
import AdvancedFilter from '@/components/AdvancedFilter';
// 优化后
const AdvancedFilter = React.lazy(() => import('@/components/AdvancedFilter'));
配合 Suspense 使用,体验完全无感,但 bundle 体积立马减了 120KB。
踩坑提醒:这三点一定注意
- 别过度拆分:我一开始把每个 npm 包都单独拆,结果 HTTP/1.1 下请求太多反而更慢。后来合并成 vendor + common 两个核心包,平衡了并行加载和请求数。
- 缓存策略要跟上:拆出来的 common.js 如果内容不变,必须加 long-term cache(比如 [contenthash])。不然用户每次更新都要重新下载,白优化了。
- 动态 import 别滥用:像 Header、Footer 这种首屏就要用的组件,千万别 lazy,否则会闪白屏。我吃过这个亏,改完后 FCP 反而变差了。
优化后:流畅多了
改完上线后,实测效果立竿见影。用 Lighthouse 测了一下:
- 首屏加载时间从 5.2s 降到 820ms
- 初始 JS bundle 从 3.2MB 降到 680KB(gzip 后)
- TTI(可交互时间)从 4.1s 降到 1.1s
最关键的是,用户再也不用盯着白屏发呆了。现在进登录页,不到 1 秒就能点按钮,体验提升不是一点半点。
当然,也不是完美无缺。比如那个 shared chunk,因为包含了一些通用 hooks,偶尔会因为 tree-shaking 不彻底带进没用的代码。但影响不大,后续可以再细拆,目前这个方案已经够稳了。
性能数据对比
为了更直观,我整理了优化前后关键指标(本地 3G 网络模拟):
| 指标 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| 首屏加载时间 | 5200ms | 820ms | 84% |
| 初始 JS 体积 (gzip) | 980KB | 680KB | 31% |
| 最大内容绘制 (LCP) | 4800ms | 1200ms | 75% |
| 可交互时间 (TTI) | 4100ms | 1100ms | 73% |
数据不会骗人。虽然拆包不是银弹,但在这种“大杂烩”项目里,绝对是性价比最高的优化手段之一。
结尾碎碎念
其实 Common 分离说白了就是“别让所有页面背同一个大锅”。该拆的拆,该懒加载的懒加载,别怕麻烦。Webpack 的 SplitChunks 虽然配置有点绕,但一旦配对了,收益巨大。
以上是我这次优化的完整过程,有细节没写全的地方欢迎评论区交流。如果你也在搞老项目性能优化,不妨试试这套组合拳——亲测有效。
这个技巧的拓展用法还有很多,比如结合 prefetch、预加载关键路由,后续我会继续分享这类实战博客。毕竟,前端优化这事儿,永远在路上。

暂无评论