webpack|webpack 性能调优报告

调优成果

优化前 优化后 优化比例
打包体积 32.01 MB 16.56 MB 48.27%
Gzip 体积 3.37 MB 1.82 MB 45.99%
文件数量 82 19 76.83%
资源压缩 服务器 br 压缩 客户端 br 压缩 1
压缩级别 6 11(最高) 1
构建速度 60s + 41.1s + 31.67%
播放页 FP 耗时 明显感知 无感知 1
(以上数据来源为本地测试,仅供参考)
优化目标
  • 降低播放页 FP 耗时
  • 减少静态资源请求数量
  • 提升开发体验
  • 提升构建产物质量
    (文章基于 webpack@4.46.0)
    问题分析先看项目当前的打包分析结果
    webpack|webpack 性能调优报告
    文章图片

以上是用 vue-cli 生成的构建统计报告,它会帮助分析包中包含的模块们的大小,简单列一下报告信息如下:
  1. 整体打包体积 32.01 MB
  2. ts.worker.js 是 monaco 编辑器的语言支持文件,主要提供 typescript 语法支持,体积 10.92 MB
  3. my-details.js 是播放页文件,网站核心资源文件,体积 6.81 MB
  4. chunk-vendors.js 第三方模块捆绑包,体积 4.14 MB
  5. html.workar.js 是 monaco 编辑器的 html 语法支持文件
  6. css.workar.js 是 monaco 编辑器的 css 语法支持文件
  7. json.worker.js 是 monaco 编辑器的 json 语法支持文件
  8. app.js 项目入口文件,体积 1.02 MB
  9. 小文件文件数量太多,加起来接近百个,导致 http 请求过多
经过分析总结,定位问题如下:
  1. monaco-editor 是最大的问题,体积占据半壁江山,严重影响加载速度,需要优化
  2. chunk-vendors.js 作为公共模块,构成项目必不可少的一些基础类库,升级频率都不高,但每个页面都需要它们,现在它体积过大,应该在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。其中包含了三个大家伙:elementUI、moment 和 lodash,更是需要单独做优化
  3. my-details.js 和 app.js 作为项目主要的资源文件,体积过大,需要优化
  4. 小文件数量太多,需要合并
解决思路 检查下有没有哪些产出是不必要的,在有限的时间空间和算力下,去除低效的重复(提出公共大模块),进行合理的冗余(小文件允许重复),达到时间和空间综合考量上的最优。
下面分步骤实现每一个优化项。
monaco-editor 优化 缩小体积
我们业务层面,只需要用到展示功能,其语言编辑功能完全用不到,因此可以把语言包全部过滤掉,这里需要用到 monaco-editor-webpack-plugin 插件,配置添加插件选项 languages 为支持的语言数组(具体语言查看官网)默认是支持所有语言的,配置此项应该只是去除一些语言的高级特性支持。
new MonacoWebpackPlugin({ languages: [], }),

再次打包后,体积缩小至 9.26 MB。
单独打包
monaco-editor 作为一个重量级组件,会分散很多小文件到各个地方,从而增加文件数量和体积,进而造成流量损失,通过 webpack 的 splitChunks 功能拆分 monaco-editor 为单独独立文件,充分利用浏览器缓存,减少多次引用时的消耗。
splitChunks: { chunks: 'all', minSize: 20000, maxAsyncRequests: 30, maxInitialRequests: 30, enforceSizeThreshold: 50000, maxSize: 0, cacheGroups: { monacoEditor: { chunks: 'async', name: 'chunk-monaco-editor', priority: 22, test: /[\/]node_modules[\/]monaco-editor[\/]/, enforce: true, reuseExistingChunk: true, }, }

按需加载
通过按需加载,减少核心内容渲染前的阻塞,转到需要的时候加载。
const monaco = await import('monaco-editor');

开启 prefetch
按需加载后,因为 monaco-editor 本身体量很大,因此在加载编辑器时会出现长时间无响应现象,采用 prefetch 预加载方案,在浏览器空闲时预先下载资源,到用的时候直接取,有效避免无响应情况。
const monaco = await import( /* webpackPrefetch: true; " */ 'monaco-editor');

moment.js 优化 尝试删除 moment.js 语言包后体积依然很大,最后采用和 moment.js api 完全兼容的 dayjs 替换,gzip压缩后仅仅 2kb
(因替换工作为全局,可能会出现和 moment.js 相关功能的 bug )
element-ui 优化 单独打包
理论上 UI 组件库也可以放入 chunk-vendors.js 中,但它实在是过大,可能比 libs 里所有的包加起来还要大不少,而且 UI 组件库的更新频率也相对比 chunk-vendors.js 要更高一点。Element-UI 组件库作为 UI 组件,应从 chunk-vendors.js 中分离出来,单独打包为 chunk-elementUI.js,如图所示打包后体积为 1.67 MB
webpack|webpack 性能调优报告
文章图片

按需引入
按照官方按需引入方式,只引入使用的组件,减少体积。
lodash.js 优化 按需引入
【webpack|webpack 性能调优报告】使用 webpack 插件 lodash-webpack-plugin 和 babel 插件实现按需打包 lodash
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); config.plugin('loadshReplace').use(new LodashModuleReplacementPlugin());

module.exports = { presets: ['@vue/cli-plugin-babel/preset'], plugins: [ 'lodash', ], };

优化后,lodash 基本上做到无感知存在
svgIcon优化 单独打包
svgIcon 组件库作为高频更新且引用超多的库,应单独分出为 chunk-svgIcon.js,如图所示打包后体积为 561 kb
webpack|webpack 性能调优报告
文章图片

删除 use-zh.svg
经过代码审查发现体积最大的 use-zh.svg 在项目中现在并未使用,所以删除
雪碧图
将所有 svg 合成雪碧图,减少请求次数,使用 svg-sprite-loader 实现
const svgRule = config.module.rule('svg'); // 清除已有的所有 loader,如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。 svgRule.uses.clear(); // 添加要替换的 loader svgRule .test(/.svg$/) .include.add(path.resolve(__dirname, 'src/components/svgIcon/svg')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]', }) .end();

压缩 svg (未执行)
因为压缩过后的 svg 图会存在去掉 fill 的情况,因此这一步骤并未执行
代码压缩 采用 terser 插件(webpack4 官方推荐)进行 js 和 css 代码压缩,同时去掉生产环境的注释和 console 信息
config.optimization.minimizer('terser').tap(options => { const compress = { warnings: false, drop_console: true, drop_debugger: true, pure_funcs: ['console.log'], }; const initCompress = options[0].terserOptions.compress; options[0].terserOptions.compress = { ...initCompress, ...compress }; return options; });

图片压缩(未执行) 使用 webpack 插件 image-webpack-loader 对所有图片进行压缩,但是该插件依赖于系统环境,在不同环境下可能出现安装失败,编译失败等情况,我们项目目前图片资源较少,所以暂时不用。
开启 br 压缩 当前 br 压缩是在 nginx 服务器进行且并未缓存资源,因此每次请求都需要对资源进行压缩之后发出,考虑服务器性能,压缩级别设置为 6。
现在改为客户端压缩,服务器在接收请求时直接把压缩文件发出,减少服务器压力。同时客户端压缩可以把压缩级别调整至最高的 11,整体资源大小会再次下降,使用 compression-webpack-plugin 实现。
const CompressionWebpackPlugin = require('compression-webpack-plugin'); if (!isDev) { plugins.push( new CompressionWebpackPlugin({ filename: '[path].br[query]', algorithm: 'brotliCompress', test: /.(js|css|json|txt|html|ico|svg)(?.*)?$/i, compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, }, threshold: 1024, minRatio: 0.99, //删除原始文件只保留压缩后的文件 deleteOriginalAssets: false, }), ); }

多线程执行 loader 开启 parallel 为 Babel 或 TypeScript 使用 thread-loader
parallel: require('os').cpus().length > 1

打包缓存 采用 HardSourceWebpackPlugin 插件为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。
可能带来的问题,修复办法在这 hard-source-webpack-plugin
下面链接用于解决 hash 丢失问题
https://github.com/mzgoddard/...
其他修改项
  • 提取环境变量 const` isDev = process.env.NODE_ENV === 'development'; `
  • 增加打包速度检测插件 SpeedMeasurePlugin
最终报告 报告图:
webpack|webpack 性能调优报告
文章图片

webpack|webpack 性能调优报告
文章图片

    推荐阅读