likes
comments
collection
share

webpack升级3-5,性能优化踩坑实战

作者站长头像
站长
· 阅读数 22

一、冷启动一个大型老项目要多久?

在工作中遇到几个比较大型的老项目,启动非常慢,经常是2-3分钟左右。

再来看一个更夸张的:

webpack升级3-5,性能优化踩坑实战

对于启动这样的项目真的是折磨。

二、预备知识

(一)esbuild

为什么我要提esbuild,因为vite使用了esbuild构建,而且webpack也可以使用esbuild-loader进行提速。

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

为什么这么快?

  1. js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程(也带来了一些缺点,一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin ,无法使用)

三、webpack性能分析

1、webpack-bundle-analyzer

使用bundle-analyzer分析工具分析包的大小:

就拿其中路由比较少的一个项目admin-crm-web来启动,也有80多个路由:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

启动时间1分半,占用内存6.77G,所有的chunk加起来有100MB

就算手动选择某个路由,仍需10几秒左右的时间。

2、speed-measure-webpack-plugin

使用speed-measure-webpack-plugin进行分析,测量各个插件和loader所花费的时间:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

三、优化方案

1、浏览器驱动路由切割编译

webpack升级3-5,性能优化踩坑实战

之前的项目如果要启动的话,我们的公司同事自己封装了插件,减少构建时候的打包路由,让我们进行了手动选择,这样做还是不够方便:

1、路由太多不方便选择

2、选择错了,需要关闭然后重启再次选择

想想vite是怎么做的,vite启动的时候,没有做任何的编译,等你访问页面的时候,通过浏览器的esm请求资源,vite然后根据你需要的资源进行按需编译,返回给浏览器。

为了改进使用体验,提示开发效率,我在想,能不能启动的时候不选择路由,当我浏览器访问页面的时候,再告诉webpack我需要打包这个页面,webpack然后再去打包当前页面呢?

我的思路是,在webpack开发时候,打包空路由,然后在页面中注入脚本,监听路由变化,发送请求到webpack,通过webpack-dev-server的before钩子监听请求,获取路由变化的参数,然后在动态写入路由。

下面是我的实现:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

可以提升的点:

1、打包的时候页面一片空白,可以加入loading或者打包进度输出

2、需要一份.routes.ts文件,这份文件和你的的路由文件是相同的,只是过滤后的文件,可以用babel重写,在importDecleration中进行匹配,在内存中进行过滤

2、UglifyJsPlugin 开启parallel

利用多核处理器进行并行处理。原理就是使用nodejs启用了新的子进程。

不过后来我升级了webpack版本到5,使用了terser-plugin,也是一样的。

3、dll插件

dll和external的功能是一样的,不过是对本地的依赖进行预打包,然后再排除,如果依赖变化了,还需要再重新打包一次。

webpack升级3-5,性能优化踩坑实战

dll插件不仅可以生产环境用,构建时也能用,所以可以放开。所以我把已经生成好的dll文件放在了本地目录下,

并设置了

webpack升级3-5,性能优化踩坑实战

然后在页面引入

webpack升级3-5,性能优化踩坑实战

4、升级webpack

检查过期的包:

webpack升级3-5,性能优化踩坑实战

升级和webpack相关的包:

webpack升级3-5,性能优化踩坑实战

升级完以后再安装webpack-cli:

webpack升级3-5,性能优化踩坑实战

5、使用esbuild

不过esbuild-loader也有缺点:

(1)不支持装饰器

使用oneOf对单文件采用单一loader, 当returen false的时候即采用babel-loader + ts-loader形式打包文件;

webpack升级3-5,性能优化踩坑实战

判断输入文件是否有装饰器:

function hasDecorator(fileContent, offset = 0) {
  const atPosition = fileContent.indexOf('@', offset);

  if (atPosition === -1) {
    return false;
  }

  if (atPosition === 1) {
    return true;
  }

  if (["'", '"'].includes(fileContent.substr(atPosition - 1, 1))) {
    return hasDecorator(fileContent, atPosition + 1);
  }

  return true;
}

(2)不支持按需加载

开发环境使用esbuild-loader,antd全量引入即可

(3)只能打包成es6

webpack升级3-5,性能优化踩坑实战

但是我们的项目一般都是打包成es5的,所以会存在冲突,所以可以在开发环境,设定ts-loader的configfile

webpack升级3-5,性能优化踩坑实战

8、exclude改为include

这样能将 loader 应用于最少数量的必要模块

webpack升级3-5,性能优化踩坑实战

改为

webpack升级3-5,性能优化踩坑实战

9、尽量少地使用工具

每个额外的 loader/plugin 都有其启动时间。

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

例如一些插件,在生产环境才需要,开发环境可以不使用例如:BannerPlugin、ExtractTextPlugin、UglifyJsPlugin等

10、Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • "eval" 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

11、开发环境避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:

webpack升级3-5,性能优化踩坑实战

12、开发环境输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:

webpack升级3-5,性能优化踩坑实战

13、开发环境ts-loader的优化

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。

webpack升级3-5,性能优化踩坑实战

14、happypack或者thread-loader

webpack3可以使用happypack,webpack5使用thread-loader,不过也是有一些限制的:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。

请仅在耗时的操作中使用此 loader!

webpack升级3-5,性能优化踩坑实战

可以看到耗时的几个loader,所以进行配置:

webpack升级3-5,性能优化踩坑实战

又遇到了坑:

webpack升级3-5,性能优化踩坑实战

主要原因就是项目中js文件使用了ts的语法,导致我不得不也用了ts-loader将js文件也进行了转换,这样的做法是不严谨的,但是老项目也没有办法。

15、noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquery 、lodash。

webpack升级3-5,性能优化踩坑实战

16、externals

已经使用了dll排除了一些第三方依赖,但是每个具体的项目还有一些外部依赖也可以排除,在不重现打包dll文件的情况下,可以使用externals。\

webpack升级3-5,性能优化踩坑实战

17、优化 resolve.extensions 配置

在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:

  • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

webpack升级3-5,性能优化踩坑实战

移除.web.js

18、开启缓存

例如babel-loader开启缓存,第二次构建时会读取缓存:

webpack升级3-5,性能优化踩坑实战

webpack自带的cache不用配置,默认开发环境开启。

HtmlWebpackPlugin、TerserPlugin、eslint-loader、babel-loader统统加上缓存

除了这些之外,还有cache-loader、HardSourceWebpackPlugin都是可以开启缓存的。我这里没做尝试,大家可以有兴趣试一试。

19、webpack编译两次

webpack升级3-5,性能优化踩坑实战

在使用webpack3的时候,经常会出现编译2次的情况,好像是低版本的html-webpack-plugin才有的问题,在我升级完以后,这个问题已经不存在了。

四、踩坑

坑1:

webpack升级3-5,性能优化踩坑实战

将dll的json中的meta替换为buildMeta即可。

坑2:

webpack升级3-5,性能优化踩坑实战

jsonpFunction 替换为chunkLoadingGlobal

坑3:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

将loader替换为use即可。

坑4:

webpack升级3-5,性能优化踩坑实战

原因是因为自定义的webpack插件:

webpack升级3-5,性能优化踩坑实战

webpack的api变化,应该改写为

webpack升级3-5,性能优化踩坑实战

坑5:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

改为:

webpack升级3-5,性能优化踩坑实战

坑6:

webpack升级3-5,性能优化踩坑实战

contentBase改为static

坑7:

webpack升级3-5,性能优化踩坑实战

disableHostCheck改为allowedHosts:'all'

坑8:

webpack升级3-5,性能优化踩坑实战

改为logging:'warn'

坑9:

webpack升级3-5,性能优化踩坑实战

webpack.optimize.CommonsChunkPlugin 升级到splitChunks

webpack升级3-5,性能优化踩坑实战

坑10:

webpack升级3-5,性能优化踩坑实战

将devserver的before换成onBeforeSetupMiddleware

webpack升级3-5,性能优化踩坑实战

坑11:

webpack升级3-5,性能优化踩坑实战

babel.rc文件中plugin不允许使用数组的配置了

webpack升级3-5,性能优化踩坑实战

坑12:

webpack升级3-5,性能优化踩坑实战

主要原因还是babel的预设都已经改为了@babel/preset-es2015这样的形式

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

改为安装新的预设包,并改写配置项:

webpack升级3-5,性能优化踩坑实战

坑13:

webpack升级3-5,性能优化踩坑实战

全局安装webpack-cli

坑14:

webpack升级3-5,性能优化踩坑实战

移除babel配置中的,并将@babel/preset-es2015和@babel/preset-stage-0换成@babel/preset-env

webpack升级3-5,性能优化踩坑实战

坑15:

webpack升级3-5,性能优化踩坑实战

将之前的写法改为

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

坑16:

webpack升级3-5,性能优化踩坑实战

改写为

webpack升级3-5,性能优化踩坑实战

坑17:

webpack升级3-5,性能优化踩坑实战

使用mini-css-extract-plugin代替extract-text-webpack-plugin即可。

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

改写为:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

坑18:

webpack升级3-5,性能优化踩坑实战

对ossPlugin进行升级。

坑19:

webpack升级3-5,性能优化踩坑实战

安装 @babel/plugin-proposal-decorators插件即可。

webpack升级3-5,性能优化踩坑实战

坑20:

webpack升级3-5,性能优化踩坑实战

因为使用了babel-plugin-import,所以这个ts-import-plugin是多余的,可以去掉。

webpack升级3-5,性能优化踩坑实战

坑21:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

这些报错主要是因为在js中使用了ts的功能,所以

webpack升级3-5,性能优化踩坑实战

js文件也应该用ts-loader

坑22:

webpack升级3-5,性能优化踩坑实战

类型断言只能在ts文件中使用

坑23:

webpack升级3-5,性能优化踩坑实战

zlib找不到,原因是webpack5以前是内置了nodejs的一些polyfills的,现在需要自己单独安装配置。

webpack升级3-5,性能优化踩坑实战

坑24:

webpack升级3-5,性能优化踩坑实战

还是因为装饰器的转换插件丢失了descriptor,

主要原因还是在我们的项目中,装饰器的使用的方法是

webpack升级3-5,性能优化踩坑实战

而官方的使用方法是:

webpack升级3-5,性能优化踩坑实战

在我们的项目中装饰的是class的propotery,而不是class method,所以最后根本没有走装饰器插件那里的转换。

webpack升级3-5,性能优化踩坑实战

这里只有2个解决办法,一个就是将项目中的代码改为和官方一样的写法,还有一个就是自己再把以前老的装饰器的插件对于class的propetry那块拿来重新一个自定义插件。

坑25:

webpack升级3-5,性能优化踩坑实战

uglifyjs-webpack-plugin改为terser-webpack-plugin

webpack升级3-5,性能优化踩坑实战

坑26:

webpack升级3-5,性能优化踩坑实战

安装配置assert

webpack升级3-5,性能优化踩坑实战

坑27:

webpack升级3-5,性能优化踩坑实战

安装配置stream-browserify

webpack升级3-5,性能优化踩坑实战

坑28:

webpack升级3-5,性能优化踩坑实战

加入ignoreOrder

webpack升级3-5,性能优化踩坑实战

坑29:

webpack升级3-5,性能优化踩坑实战

使用了esbuild之后,虽然速度提升了好几倍,

webpack升级3-5,性能优化踩坑实战

但是在webpack中配置的reslove的模块找不到引用了

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

因为这里项目中也是在babel中改变引用了路径的,要修复的话,必须要把crm-comps下面src的文件新建一个index.js暴露出来即可。

坑29:

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

主要是main设置的有问题,项目中是通过babel改变了引入的目录的,现在使用esbuild就会报错

webpack升级3-5,性能优化踩坑实战

改为:

webpack升级3-5,性能优化踩坑实战

五、优化效果

webpack升级3-5,性能优化踩坑实战

全部启动只花了不到10秒,还记得前面没有优化之前启动一个整个项目需要多久吗?1份10几秒,时间提升了90%!

webpack升级3-5,性能优化踩坑实战

webpack升级3-5,性能优化踩坑实战

内存使用减少了1.7G