前端打包发布优化实战技巧分享
优化前:卡得不行
上周上线了个新项目,是个移动端的 H5 页面,主打轻量互动。结果刚推出去,用户反馈就来了:“点不动”“加载半天白屏”“滑动直接卡死”。我自己拿测试机一跑,好家伙,首屏加载 5 秒多,bundle.js 直接干到 3.8MB,gzip 后还有 1.1MB。这哪是 H5,这是 H5 巨无霸了。
更离谱的是,页面交互完全不跟手,touchmove 都能卡出残影。一开始还以为是 JS 逻辑太重,后来发现根本没跑多少业务代码,光是框架和依赖就把性能压死了。这时候就知道——打包策略必须重构,不能再拖了。
找到瘼颈了!
我先用 Chrome DevTools 的 Performance 面板录了一段加载过程,一看吓一跳:大量时间花在 Parse 和 Evaluate Script 上。再打开 Lighthouse 跑一遍,首屏渲染评分只有 32,建议优化项里全是“减少 JavaScript 执行时间”“预加载关键资源”这类警告。
然后切到 Coverage 工具扫了下,发现有将近 60% 的 JS 代码是加载后压根没执行的。比如某个组件只在二级页出现,却打包进了首页;又比如 moment.js 整个库都引了,其实只用了两个方法。
这时候基本定位清楚了:问题不在代码质量,而在打包策略太粗放,全量打包 + 没做分包 + 缺少懒加载,典型的“一股脑全塞进去”式构建。
动手开整:从分包开始
项目用的是 Webpack 5,本来就有 code split 的能力,但之前为了省事,配置里关掉了 async 分包,所有动态 import 都被打包进主 chunk。现在得改回来。
第一步就是把路由级组件全部改成动态 import,并配合魔法注释生成独立文件:
// 优化前
import DetailPage from './pages/DetailPage'
const routes = [
{ path: '/detail', component: DetailPage }
]
// 优化后
const routes = [
{
path: '/detail',
component: () => import(/* webpackChunkName: "page-detail" */ './pages/DetailPage')
}
]
这样 Webpack 就会在 build 时自动拆出 page-detail.js,首页只加载必要的 shell 代码。同时,在 webpack.config.js 里加了 splitChunks 规则,把公共依赖单独抽出来:
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendor',
chunks: 'all',
priority: 10
},
moment: {
test: /[\/]node_modules[\/]moment/,
name: 'chunk-moment',
chunks: 'all',
priority: 20
}
}
}
这里注意我踩过一次坑:一开始把 moment 单独抽包是因为它太大(200KB+),但后来发现有些小页面也用了 moment,反而多了一个 http 请求。最后权衡了一下,还是保留在 vendor 里,毕竟复用率高。
按需引入:别再全量引库了
接着处理那些“杀鸡用牛刀”的情况。最典型的就是 moment.js 改成 dayjs,体积从 200KB 干到 2KB。改起来也不难:
// 以前
import moment from 'moment'
const date = moment().format('YYYY-MM-DD')
// 现在
import dayjs from 'dayjs'
const date = dayjs().format('YYYY-MM-DD')
还有 lodash,之前到处都是 import _ from 'lodash',现在全部换成按需引入:
// 优化前
import _ from 'lodash'
_.debounce(func, 300)
// 优化后
import debounce from 'lodash/debounce'
debounce(func, 300)
或者更狠一点,用 babel-plugin-lodash 自动转换,一行都不用改。
字体文件也没放过。原来首页加载了全套中文字体,woff2 文件 800KB。现在改成仅加载英文基础字符集,中文通过系统字体 fallback,首屏字体请求直接归零。
Gzip 压缩开启了吗?别问,问就是没开
检查 nginx 配置才发现,线上环境根本没开 Gzip。虽然 Webpack 构建时用了 CompressionPlugin 生成 .gz 文件,但服务器没配 header,等于白搭。
补上配置后,再次请求对比:
- bundle.js:1.1MB → 320KB
- vendor.js:980KB → 270KB
这一波纯靠压缩,省了将近 1MB 流量。关键是用户感知明显——首屏白屏时间从 5s+ 直接掉到 1.8s 左右。
预加载关键资源,别让用户等
然后上了 resource hint。首页依赖的 vendor 和 main chunk 加了 preload:
<link rel="preload" href="/static/js/vendor.chunk.js" as="script">
<link rel="preload" href="/static/js/main.chunk.js" as="script">
另外,像 API 地址这种稳定不变的,也预连接一下:
<link rel="preconnect" href="https://jztheme.com">
别小看这几条 link,实测在弱网环境下,资源并行加载效率提升很明显,Lighthouse 的“消除阻塞资源”得分直接从 40 涨到 75。
懒加载图片和组件,别一开始就拉满
图片全部加上 loading=”lazy”,第三方组件也做了动态引入:
// 比如分享弹窗,不用一进来就加载
const loadShareModal = () => import(/* webpackChunkName: "share-modal" */ './components/ShareModal')
触发时再动态 mount,内存占用立马下来了。之前页面一打开就占 150MB 内存,现在压到 60MB 以内,低端机也能跑得动。
优化后:流畅多了
改完重新 build、deploy,再跑一遍 Lighthouse:
- 首屏加载时间:5.2s → 800ms
- JS 总体积(gzip):3.2MB → 900KB
- Lighthouse 性能评分:32 → 85
- FCP(First Contentful Paint):4.8s → 1.1s
- 可交互时间(TTI):6.1s → 1.6s
最关键的是用户体验反馈变了:“终于不卡了”“点一下就有反应”。这才是真实世界的指标。
性能数据对比
下面是优化前后几个核心指标的对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 首屏加载时间 | 5.2s | 800ms |
| JS 总体积(gzip) | 3.2MB | 900KB |
| Lighthouse 性能分 | 32 | 85 |
| 内存占用 | 150MB | 60MB |
说实话,还没到完美状态。比如某些 chunk 还是偏大,部分页面仍有轻微卡顿,但已经不影响主流程使用。现阶段优先级不高,后续可以考虑动态 polyfill 或进一步细粒度分包。
以上是我踩坑后的总结,希望对你有帮助
这次优化折腾了三天,中间试过几种方案都没啥效果,比如提前 runWebpack、SSR 渲染这些,对当前项目来说成本太高,收益不成正比。最后还是回归到最基础的分包 + 懒加载 + 压缩三件套,效果最好。
可能有人会说,用 Vite 不就好了?确实,但我们这项目是老 Webpack 体系,短期内不可能重构。所以我觉得,哪怕工具老旧,只要策略对,一样能跑出好性能。
以上是我个人对这个打包性能优化的完整讲解,有更优的实现方式欢迎评论区交流。

暂无评论