手把手教你做Bundle分析实现精准体积优化

Good“红娟 前端 阅读 2,614
赞 90 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线三个月,用户反馈越来越多——页面打开慢,首屏要等好几秒,尤其是一些低配手机上,点个按钮要一两秒才有反应。我自己拿旧款安卓机试了下,确实离谱,首页 loading 从5秒起步,bundle 大小直接飙到3.8MB(gzip后1.6MB),这哪是网页,这是加载游戏吧?

手把手教你做Bundle分析实现精准体积优化

最离谱的是,我们其实没做多复杂的功能,就是个内容展示+表单提交的管理后台,结果打包完发现 lodash 单独占了400KB,moment.js 又塞进来280KB,还有各种组件库全量引入……整个项目像被灌了水的大海绵,虚胖得不行。

找到病根了!

我第一反应是上工具看看到底谁在“偷体积”。先跑了一把 webpack-bundle-analyzer,本地启了个 analyze 模式:

npm run build -- --report

然后浏览器弹出一个可视化 treemap,一眼就看到红色巨块:lodash、moment、echarts、antd 全堆在一起,其中 lodash 因为用了 _.debounce、_.cloneDeep 这种常用方法,但项目里是 import from ‘lodash’,导致全量打包。

另一个问题是动态图表组件用到了 echarts,但我们只用了折线图和柱状图,结果把所有图表类型都打进去,又多了600KB。

还有个隐藏坑:某些第三方库内部引用了 moment,而 moment 自带200多个语言包,默认全打进去。后来查了下,光是 zh-CN 以外的语言包就占了140KB,纯浪费。

开干:拆、剪、换

问题清楚了,接下来就是动手。我定了三个方向:按需引入、代码分割、替换重型依赖。

第一步:lodash 按需引入

原来代码是这么写的:

import _ from 'lodash'

_.debounce(handleScroll, 300)
_.cloneDeep(obj)

改成了按方法单独引入:

import debounce from 'lodash/debounce'
import cloneDeep from 'lodash/cloneDeep'

debounce(handleScroll, 300)
cloneDeep(obj)

或者更省事的,装个 babel 插件自动处理:

// .babelrc
{
  "plugins": ["lodash"]
}

配合这个插件,你继续写 import _ from ‘lodash’,它会在编译时自动替换成模块化引入,实测节省了350KB左右。

第二步:moment 替换成 dayjs

moment 真的是性能杀手,不仅体积大,API 还不可变(每次操作生成新对象)。我们项目里只用到了格式化和时差计算,完全可以用 dayjs 替代。

改造很简单,API 基本兼容:

// 原来
import moment from 'moment'
moment().format('YYYY-MM-DD')

// 改成
import dayjs from 'dayjs'
dayjs().format('YYYY-MM-DD')

顺手把语言包也加上:

import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')

体积从280KB干到了12KB,省了268KB,血赚。

第三步:组件库按需加载(以 antd 为例)

我们用的是 antd 4.x,之前是直接 import { Button, Modal } from ‘antd’,虽然用了 babel-plugin-import,但配置没开压缩。

检查了下 .babelrc:

{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

确保 libraryDirectory 是 es 而不是 lib,这样能利用 tree-shaking。另外 style 如果用 less 可以进一步按需加载样式,但我们项目为了省事用了 css,也能接受。

这一步省了将近200KB。

第四步:路由级 code splitting

首页加载时就把所有路由组件全拉下来,明显不合理。我们用的是 React + React Router,改用 React.lazy 动态加载:

const Home = lazy(() => import('./pages/Home'))
const Report = lazy(() => import('./pages/Report'))
const Setting = lazy(() => import('./pages/Setting'))

// 路由渲染
<Suspense fallback={<Spinner />}>
  <Route path="/report" component={Report} />
</Suspense>

配合 webpack 的 splitChunks 配置:

// webpack.config.js
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendor: {
        test: /[\/]node_modules[\/]/,
        name: 'vendors',
        chunks: 'all',
      }
    }
  }
}

这样一来,首页只加载核心逻辑,其他路由独立打包,首次加载体积从1.6MB降到780KB(gzip后)。

第五步:ECharts 按需引入

原先是:

import * as echarts from 'echarts'

改成了:

import * as echarts from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'

echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, CanvasRenderer])

只引入需要的模块,体积从600KB降到90KB左右,效果立竿见影。

还有几个顺手优化

  • 图片资源全部转成 WebP,通过 image-minimizer-webpack-plugin 压缩,平均再降80KB
  • 加了 preload 关键 CSS,减少 FOUC
  • 第三方脚本如统计代码异步加载,不阻塞主流程
  • 接口请求统一走 /api/proxy 代理,避免 CORS 预检请求过多

优化后:流畅多了

改完重新 build,再跑一遍 bundle-analyzer,整个图变得清爽多了,没有红色巨块,全是绿色小方块。

线上灰度发布后,监控数据显示:

  • 首屏加载时间从 5.2s 降到 1.1s(3G 网络模拟)
  • FCP(First Contentful Paint)从 4.8s 降到 980ms
  • LCP 从 5.5s 降到 1.3s
  • bundle 总大小从 3.8MB(未压缩)降到 1.1MB,gzip 后 420KB

用户反馈明显变少,客服那边说最近没人投诉卡了。老板看了数据还夸了两句,虽然我知道他根本看不懂这些数字……

踩坑提醒:这三点一定注意

  1. babel-plugin-import 不生效? 检查是否和其他插件冲突,比如 preset-env 的 useBuiltIns: usage,曾经因为这个配置导致 import 插件失效,折腾了半天发现是加载顺序问题。
  2. React.lazy 和 Suspense 在生产环境白屏? 记得加 fallback,而且不要在顶层直接用 lazy,建议封装一层 Loadable 组件做错误边界处理。
  3. dayjs 替换 moment 后时区不对? 注意 dayjs 默认不带时区插件,如果要用,得显式引入:import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(timezone)

性能数据对比

以下是优化前后关键指标对比:

指标 优化前 优化后 提升
JS Bundle Size (gzip) 1.6 MB 420 KB 73.7%
首屏加载时间 (3G) 5.2s 1.1s 78.8%
FCP 4.8s 980ms 79.6%
请求数量 38 21 44.7%

最后说两句

这次 bundle 分析优化,说实话不是什么高深技术,都是些老生常谈的手段:按需引入、code splitting、替换重型依赖。但真正落地时,每个环节都有坑,尤其是团队协作项目,有人随手 import _ from ‘lodash’,几天就回退到解放前。

所以我现在在 CI 流程里加了 bundle 体积告警,超过阈值直接 fail,倒逼大家注意。同时也写了份简单的《前端性能自查清单》,新人入职就得过一遍。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们是怎么处理 ECharts 或者地图 SDK 的?我也还在摸索。

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

暂无评论