前端打包发布优化实战技巧分享

南宫志达 移动 阅读 2,284
赞 11 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周上线了个新项目,是个移动端的 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 体系,短期内不可能重构。所以我觉得,哪怕工具老旧,只要策略对,一样能跑出好性能。

以上是我个人对这个打包性能优化的完整讲解,有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论