webpack5优化实战记录

149 阅读8分钟

先说成果

优化后产物体积减少了58.10%,无缓存编译时间缩短了75.99%,有缓存编译时间缩短了70%-95%。优化手段和思路web页面也能用。

(因为数据有波动,都是测10次取平均。其中用缓存减少95%属于第一次编译后没改动代码就进行的编译,减少70%为开发中改动少量代码一般会有的提升)

注意

阅读本文需要有对webpack的基本了解,相关代码网上搜搜就有,便不赘述,由于是实战后的总结,主要讲清思路,而不是我实际优化的顺序,所以读者姥爷可能看到体积时间不是线性变化的问题。

背景

本人因公司项目中几个环境要管理几套账密一个个复制粘贴过于麻烦写了个Chrome插件,不用脚手架的情况下用webpack对代码进行了处理,但是随着代码增多,引入的插件和库增多,原本9秒能跑完的编译,最终也变成了30多秒,产物也从100多kb变成了1.24M。于是痛定思痛,开始了对编译过程和产物的优化。所以以下内容都是围绕减少编译时间减小产物体积进行的优化。

准备工作:配置拆分

库:webpack-merge
原本我寻思导出一个函数就行,根据参数的production来判断环境就完事儿。但是随着配置的增多,每个地方都得写production?'生产环境配置':'开发环境配置'不仅麻烦,且降低了可读性,不利于后续维护,所以将webpack.config.js配置拆分成webpack.common.jswebpack.dev.jswebpack.prod.js后,使用webpack-merge在dev和prod的文件中,将common的配置合并,并且完成基础的配置,那就完成了初步的优化。
dev中主要配的是HMR,也就是热更新,还有source map,用于优化开发体验,不是本文重点,后续不对本文件改动。
对common的改动主要是加快编译效率。对于dev和prod都有用。
给prod中配上mode: productiondevtool: false,webpack就会有默认的压缩、去除source map等操作,当然这肯定是不够的,后续我们将持续优化这个文件,压缩相关的操作主要放在这个文件。

整体思路

  1. 减少输入
  2. 优化流程
  3. 提升效率
  4. 优化产物

思路1:减少总工作量[时间-8.12% 体积-6.62%]

插件webpack-bundle-analyzer
众所周知,干活儿的快慢取决于效率和总工作量,所以第一步就是分析有没有哪些库或者组件是可以替换的,这里使用bundle analyzer来查看,但是一般来说刚装上是看不出什么的,所以可以看后边的思路6:去除重复 + 合理分包,在合理分包后,我们才知道到底哪个包比较大。从占有的面积来看是antd的Upload文件和react-dom比较大,所以这里我自己手写了一个Upload组件,因为用不上那么多功能,没必要白占那么多体积。
时间:减少了8.12%
体积:减小了6.62%
总结:使用较轻的库/组件替代重型库/组件

思路2:外包工作量

webpack配置项:externals
众所周知,把能外包的活外包出去,活才能干得又快又好4862782B.png。使用externals将react、react-dom以及antd排除出去,这样最终产物里边是没有这些东西的,然后在HTML中插入对应的script,从而利用CDN加载。体积从532kB减少到了95kB,编译时间从7390.3ms减少到了3900多毫秒。这一步没有具体测,因为实战遇到了CSP(内容安全策略)的报错,我也没找到chrome插件(版本3)的对于这一块的具体解决方案。但是放在web页面这套是可行的,不会遇到CSP报错,读者姥爷可以参考一下。

思路3:减少loader工作量[时间-58.51% 体积-15.32%]

插件speed-measure-webpack-plugin
webpack配置项:loader的include、exclude
对于webpack初学者来说,把loader配上就完事儿了,能跑就行。对于教程里提及的加上include我一直左耳进右耳出,但是不使用缓存时,时间上最大的优化就是这一块。
使用speed-measure-webpack-plugin可以看到每个插件和loader的耗时,我的代码中babel loader的耗时最长,花了18s,所以在我给我的所有loader配上了include后,一下从30782.6ms减少到12772ms的效果直接让我震惊了,原来这段时间它一直在处理所有的\.js(x?)$文件,就算不是src下的文件也处理了...
因为少了babel loader的处理,体积顺便减少了15.32%。
时间:减少了58.51%
体积:减少了15.32%
注意MiniCssExtractPluginspeed-measure-webpack-plugin有冲突,除了降低版本外,可以通过以下方式规避问题

const index = webpackConfig.plugins.findIndex(plugin => plugin.constructor.name === 'MiniCssExtractPlugin');
const cssPlugin = webpackConfig.plugins[index];
const res = smp.warp(webpackConfig);
res.plugins[index] = cssPlugin;  
module.exports = res;

思路4:去除不必要的流程

优化流程主要是删除不必要的loader和plugins,比如ts-loader每次都要处理4.5s上下,我加了thread-loader打算通过多线程的方式进行优化,但是创建和销毁线程都需要额外的耗时,导致优化了和没优化一样,不如删掉。我代码中的CSS代码也不多,CssMinimizerWebpackPlugin压缩了也没压缩多少,不如删掉。

思路5:增加人手 + 删除冗余[时间-3.88% 体积-3.23%]

loaderthread-loader
插件TerserWebpackPlugin
webpack配置项:optimization的minimizer、minimize
增加人手主要讲的是启用多线程,但正如思路4:去除不必要的流程中说的一样,thread-loader作用不大就没必要用了。我主要优化的是terser的配置,将parallel设为2。(4、8也试过,和2差不多,具体由电脑CPU数量决定)
提升效率搞定后,就是优化产物了,注释对于生产环境来说属于冗余,所以可以修改terser的配置,删除注释,具体操作webpack官方文档有说,不再赘述。
时间:减少了3.88%
体积:减少了3.23%

思路6:去除重复 + 合理分包[时间-5.49% 体积-32.94%]

webpack配置项:optimization的runtimeChunk、splitChunks
webpack中的chunk分为initial、async和runtime,首先先将runtime chunk拆出来,一般在optimization中将runtimeChunk设为true就行,各自的runtime单独打包,不相干扰。因为我写的插件是多入口的,popup和content和background三套代码不会互相干扰,所以设为'single',三者共用同一份文件,减小了总体体积。这一步体积减小1.26%时间减少2.76%

另外,splitChunks的chunks默认配置是async,建议改成all,这样initial和async都会参与分包。这一步因为浏览器插件的manifest.json需要列清楚content相关的所有js文件,所以这一步我还写了一个插件,在emit钩子获取到compilation的chunks,然后根据_group的_entrypointChunk的name属性确定不同不同产物的入口chunk名称,然后通过files获取对应文件名,最后通过emitAsset将manifest.json输出。这一步体积减少33.25%时间减少0.55%

最后是将node_modules对应的cacheGroup的maxSize设置为了400 * 1024,体积增加1.58%,时间减少2.18%,如果是优化网页,还可以顺手在optimization加上moduleIds: 'deterministic',还有在output的filename中加上[contenthash],这样可以顺便优化下浏览器的缓存。

思路7:降低兼容性

在刚对项目进行TS改造时,体积一下从1.24M膨胀到7.8M,修改了tsconfig.json的target为esnext之后,体积才恢复1.24M。

思路8:使用缓存

webpack配置项:chche
经过上述步骤,编译一次的时间已经从30782.6ms降低到了7390.3ms,体积也从1.24M变成了532kB,总体上时间减少了75.99%,体积减少58.10%,一次编译的提升空间已经不大,所以我们可以利用缓存,优化二次编译的效率。

{
  cache: {
    type: 'filesystem',
  },
}

这三行配置在不改动代码,不加上述其他优化的情况下,二次编译可以减少94.7%的时间,在代码少量修改的情况下,一般可以减少70%-90%的时间,也就是5秒左右的耗时,已经满足开发的需要了。

无用优化

  1. 压缩html,一般使用vue、react的项目都没多少html代码
  2. DLL:掘金上时不时能看到有的文章说用DLL能优化,但实际操作下来,发现这玩意儿就是个缓存,就是先把不怎么变化的代码打包起来,提升开发效率,页面也能缓存。但是我试着用了下,给react、react-dom还有antd打包,直接给我打了个1.5M的包,好家伙,按需引入直接废掉了,他这是把整个库都打包了,所以提升不了开发时的打包效率,也减少不了生产环境的包的体积。至于缓存,直接利用内容不变content hash不变的原理,让浏览器缓存就行,所以DLL没有任何存在的必要。

结语

感谢看到这里的读者姥爷,点个关注收藏吧!感谢!