前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

webpack 打包结果依赖分析,找出产物里的体积大头

首页2018-11-20 17:40:08Front-End
Webpack优化打包分析性能优化

产物 3MB,首屏白屏两秒多,领导问你「大在哪」。这时候如果只能回答「可能是第三方库比较多」,那基本没法往下推。构建优化和性能优化都一样,先量清楚,再动手,凭感觉加插件多半是白忙。

这篇讲怎么把 webpack 的打包结果画成一张能点进去的图,谁占了多少一目了然。工具就一个 webpack-bundle-analyzer,配置十行以内,但真正值钱的是怎么读这张图,以及看完之后该改什么。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • stat、parsed、gzip 三种体积口径的区别,以及该盯哪一个
  • webpack-bundle-analyzer 的安装、按需启用和各配置项的含义
  • 这张矩形树图怎么读,常见的几个体积黑洞长什么样
  • 插件的原理,它的数据是从哪来的
  • 官方在线分析工具 webpack/analyse 的用法和适用场景
  • 看完图之后可以落地的几种改法

# 一、先弄清楚三种体积口径

打开报告你会看到右上角有个下拉框,三个选项 stat、parsed、gzip。很多人第一次看会被数字吓一跳,其实是口径没选对。

stat 是模块的原始体积,也就是 webpack 从磁盘读进来那份源码的大小,没经过任何处理。这个数一般最大,参考价值最低。

parsed 是经过 loader 转译、Tree shaking、压缩之后,真正写进产物文件的体积。这才是用户下载的那份 JS 的大小,也是日常最该盯的那个。

gzip 是 parsed 再过一遍 gzip 压缩的结果。如果你的服务器或者 CDN 开了 gzip(大多数都开了),用户实际消耗的流量是这个数。JS 文本的 gzip 压缩率通常在三分之一左右,所以这个数会比 parsed 小不少。

先说结论,日常排查看 parsed,跟别人汇报或者定优化目标用 gzip。看 stat 唯一的用处是判断某个库有没有被 Tree shaking 摇掉,stat 大但 parsed 小,说明摇掉了。

# 二、使用 webpack-bundle-analyzer 插件分析

# 2.1 安装插件

包地址在 https://www.npmjs.com/package/webpack-bundle-analyzer 。

npm install webpack-bundle-analyzer --save-dev
@前端进阶之旅: 代码已经复制到剪贴板

它是个开发依赖,装到 devDependencies 就够了,不要装进 dependencies。

# 2.2 按环境变量开关,不要常驻

这个插件每次构建都要跑一遍统计并起一个服务,日常构建根本不需要。所以正确的接法是按需开启,靠一个环境变量控制。

先在 package.json 里加一条命令,传入环境变量 ANALYZE,配置里再用 process.env.ANALYZE 去读:

"scripts": {
  "build": "node scripts/build.js",
  "analyze": "cross-env ANALYZE=1 npm run build"
}
@前端进阶之旅: 代码已经复制到剪贴板

用 cross-env 是为了兼容 Windows。直接写 ANALYZE=1 npm run build 在 Windows 的 cmd 下是跑不通的,团队里有人用 Windows 就一定会踩这个。

然后在 build/webpack.prod.config.js 的 plugins 后面加上判断:

plugins.concat(process.env.ANALYZE?[
    new BundleAnalyzerPlugin({
    //  可以是`server`,`static`或`disabled`。
    //  在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
    //  在`static`模式下,会生成带有报告的单个HTML文件。
    //  在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
    analyzerMode: 'server',
    //  将在`server`模式下使用的主机启动HTTP服务器。
    analyzerHost: '127.0.0.1',
    //  将在`server`模式下使用的端口启动HTTP服务器。
    analyzerPort: 8888,
    //  路径捆绑,将在`static`模式下生成的报告文件。
    //  相对于捆绑输出目录。
    reportFilename: 'report.html',
    //  模块大小默认显示在报告中。
    //  应该是`stat`,`parsed`或者`gzip`中的一个。
    //  有关更多信息,请参见「定义」一节。
    defaultSizes: 'parsed',
    //  在默认浏览器中自动打开报告
    openAnalyzer: true,
    //  如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
    generateStatsFile: false,
    //  如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
    //  相对于捆绑输出目录。
    statsFilename: 'stats.json',
    //  stats.toJson() 方法的选项。
    //  例如,您可以使用 source: false 选项排除统计文件中模块的来源。
    //  在这里查看更多选项:https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
    statsOptions: null,
    logLevel: 'info' // 日志级别。可以是'info','warn','error'或'silent'。
  	})
  ]:[])
@前端进阶之旅: 代码已经复制到剪贴板

配置项里真正要挑的只有 analyzerMode 这一个。

server 模式会起一个本地服务,浏览器里能交互,适合本地排查。static 模式生成一个单独的 report.html,适合在 CI 里跑完之后当产物存下来,或者发给同事看。disabled 配合 generateStatsFile: true 只吐一份 stats.json,不出界面,适合接自己的分析脚本或者做体积门禁。

defaultSizes: 'parsed' 建议保留,理由上一节说过了。

# 2.3 启动

npm run analyze
@前端进阶之旅: 代码已经复制到剪贴板

在对应地址后面加上端口 8888,就能得到一个可视化的模块体积界面。

webpack-bundle-analyzer 生成的模块体积矩形树图

图里每个矩形代表一个模块或者一个目录,面积和体积成正比,可以一层层点进去。鼠标悬停会同时显示三种口径的数值。

# 三、这张图该怎么读

图打开了不代表问题就找到了。我一般按这个顺序看。

先看最外层有几个大块,确认分包策略生不生效。如果整张图就是一个巨大的 main.js,说明 splitChunks 压根没配或者没起作用,那这一步就该先去补分包,别急着抠单个库。

然后找面积最大的那几个 node_modules 子块。这里最常见的几个坑,几乎每个项目都能撞上一两个。

moment 是重灾区。它默认会把全部 locale 语言包打进去,一个几十 KB 的库能撑到两百多 KB,而你大概率只用中文。解决办法是用 IgnorePlugin 把 locale 目录忽略掉,或者干脆换成 day.js,API 基本兼容而体积只有几 KB。

lodash 整包引入是第二常见的。写 import _ from 'lodash' 会把整个库打进来,改成 import debounce from 'lodash/debounce' 或者上 babel-plugin-lodash 就能只留用到的那几个函数。

组件库没走按需加载排第三。antd、element-ui 这类库整包引入动辄几百 KB,配上对应的按需引入插件之后通常能砍掉一大半。

还有一类比较隐蔽,同一个库出现了两个不同版本。图里会看到两个同名的块分别躺在不同的路径下,通常是某个依赖锁定了旧版本导致的。npm ls <包名> 能看出依赖树里谁引了哪个版本,用 resolutions 或者升级依赖去收敛。

最后看一眼有没有本该异步加载的东西被打进了首屏包。富文本编辑器、图表库、Excel 导出这类只在特定页面用的东西,出现在初始 chunk 里就说明按需加载没生效,多半是 import() 写成了静态 import。

我自己的感受是,第一次跑这个工具基本都能找出至少两处可以砍的地方,而且改起来都不难。真正难的是坚持每次发版前跑一遍,不然过两个月又会长回去。

# 四、插件的原理

这个插件的做法其实不复杂。webpack 构建结束时会产出一份 stats 对象,里面记录了每个 chunk 包含哪些模块、每个模块多大、依赖关系是什么。插件从 stats.json 里取出 chunks 数据,算好每个模块的三种体积,最后用 Canvas 把矩形树图画出来。具体代码在 analyzer.js 的 getViewerData 方法里。

知道这一点有个实际好处,stats.json 是标准产物,你可以自己写脚本读它,做一些插件没提供的事,比如在 CI 里比对两次构建的体积差异,超过阈值就让流水线失败。

# 五、使用官方提供的在线工具

除了插件,webpack 官方还提供了一个在线分析页面。先生成一份 stats 文件:

webpack --profile --json > stats.json
@前端进阶之旅: 代码已经复制到剪贴板

然后把这份 stats.json 上传到 http://webpack.github.io/analyse 就能看到分析结果。

webpack 官方在线分析工具的界面

顺带说一句,原文这条命令里的 --proile 是个笔误,正确写法是 --profile。

这个在线工具和 webpack-bundle-analyzer 的侧重点不同。analyzer 强在体积,一眼看出谁大;官方这个强在结构,能看到模块之间的依赖关系、chunk 的父子关系、警告和错误的详细信息。想搞清楚「这个模块到底是被谁引进来的」,用它更合适。

它的缺点也很明显,界面比较老,大项目的 stats.json 动辄几十 MB,上传和渲染都慢。而且 stats 文件里包含你的模块路径信息,公司项目往上传之前想清楚。这块我自己用得不多,多数时候 analyzer 就够了。

# 六、看完图之后可以做什么

分析只是第一步,落地的改法大致分三类。

第一类是换或者裁。moment 换 day.js,lodash 改按需,组件库配按需引入插件,IgnorePlugin 干掉用不上的语言包目录。这类改动收益最直接,风险也最低。

第二类是拆。用 optimization.splitChunks 把第三方库和业务代码分开,把超稳定的核心库(React 这类)再单独分一组,配合 [contenthash] 做长期缓存。首屏用不到的路由和重组件改成 import() 动态加载。这部分的完整配置我写在 Webpack 5 构建性能优化实战 里了。

第三类是压。生产构建确认开了压缩(webpack 4 之后 mode: 'production' 默认就开),服务端或 CDN 打开 gzip 或者 brotli。这一步通常已经做了,但值得确认一遍,我见过配置里手动关掉了 minimize 却没人发现的情况。

fe
  • 一、先弄清楚三种体积口径
  • 二、使用 webpack-bundle-analyzer 插件分析
    • 2.1 安装插件
    • 2.2 按环境变量开关,不要常驻
    • 2.3 启动
  • 三、这张图该怎么读
  • 四、插件的原理
  • 五、使用官方提供的在线工具
  • 六、看完图之后可以做什么
  • 总结
  • 参考

← Hexo接入DaoVoice实现在线聊天功能Hexo博客接入gitalk评论系统 →