webpack升级3-5,性能优化踩坑实战
一、冷启动一个大型老项目要多久?
在工作中遇到几个比较大型的老项目,启动非常慢,经常是2-3分钟左右。
再来看一个更夸张的:
对于启动这样的项目真的是折磨。
二、预备知识
(一)esbuild
为什么我要提esbuild,因为vite使用了esbuild构建,而且webpack也可以使用esbuild-loader进行提速。
ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。
为什么这么快?
- js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
- go是纯机器码,肯定要比JIT快
- 不使用 AST,优化了构建流程(也带来了一些缺点,一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin ,无法使用)
三、webpack性能分析
1、webpack-bundle-analyzer
使用bundle-analyzer分析工具分析包的大小:
就拿其中路由比较少的一个项目admin-crm-web来启动,也有80多个路由:
启动时间1分半,占用内存6.77G,所有的chunk加起来有100MB
就算手动选择某个路由,仍需10几秒左右的时间。
2、speed-measure-webpack-plugin
使用speed-measure-webpack-plugin进行分析,测量各个插件和loader所花费的时间:
三、优化方案
1、浏览器驱动路由切割编译
之前的项目如果要启动的话,我们的公司同事自己封装了插件,减少构建时候的打包路由,让我们进行了手动选择,这样做还是不够方便:
1、路由太多不方便选择
2、选择错了,需要关闭然后重启再次选择
想想vite是怎么做的,vite启动的时候,没有做任何的编译,等你访问页面的时候,通过浏览器的esm请求资源,vite然后根据你需要的资源进行按需编译,返回给浏览器。
为了改进使用体验,提示开发效率,我在想,能不能启动的时候不选择路由,当我浏览器访问页面的时候,再告诉webpack我需要打包这个页面,webpack然后再去打包当前页面呢?
我的思路是,在webpack开发时候,打包空路由,然后在页面中注入脚本,监听路由变化,发送请求到webpack,通过webpack-dev-server的before钩子监听请求,获取路由变化的参数,然后在动态写入路由。
下面是我的实现:
可以提升的点:
1、打包的时候页面一片空白,可以加入loading或者打包进度输出
2、需要一份.routes.ts文件,这份文件和你的的路由文件是相同的,只是过滤后的文件,可以用babel重写,在importDecleration中进行匹配,在内存中进行过滤
2、UglifyJsPlugin 开启parallel
利用多核处理器进行并行处理。原理就是使用nodejs启用了新的子进程。
不过后来我升级了webpack版本到5,使用了terser-plugin,也是一样的。
3、dll插件
dll和external的功能是一样的,不过是对本地的依赖进行预打包,然后再排除,如果依赖变化了,还需要再重新打包一次。
dll插件不仅可以生产环境用,构建时也能用,所以可以放开。所以我把已经生成好的dll文件放在了本地目录下,
并设置了
然后在页面引入
4、升级webpack
检查过期的包:
升级和webpack相关的包:
升级完以后再安装webpack-cli:
5、使用esbuild
不过esbuild-loader也有缺点:
(1)不支持装饰器
使用oneOf对单文件采用单一loader, 当returen false的时候即采用babel-loader + ts-loader形式打包文件;
判断输入文件是否有装饰器:
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
但是我们的项目一般都是打包成es5的,所以会存在冲突,所以可以在开发环境,设定ts-loader的configfile
8、exclude改为include
这样能将 loader 应用于最少数量的必要模块
改为
9、尽量少地使用工具
每个额外的 loader/plugin 都有其启动时间。
例如一些插件,在生产环境才需要,开发环境可以不使用例如:BannerPlugin、ExtractTextPlugin、UglifyJsPlugin等
10、Devtool
需要注意的是不同的 devtool 设置,会导致性能差异。
- "eval" 具有最好的性能,但并不能帮助你转译代码。
- 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
- 使用 eval-source-map 变体配置进行增量编译。
在大多数情况下,最佳选择是 eval-cheap-module-source-map
11、开发环境避免额外的优化步骤
Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
12、开发环境输出结果不携带路径信息
Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:
13、开发环境ts-loader的优化
你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。
14、happypack或者thread-loader
webpack3可以使用happypack,webpack5使用thread-loader,不过也是有一些限制的:
- 这些 loader 不能生成新的文件。
- 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
- 这些 loader 无法获取 webpack 的配置。
每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
可以看到耗时的几个loader,所以进行配置:
又遇到了坑:
主要原因就是项目中js文件使用了ts的语法,导致我不得不也用了ts-loader将js文件也进行了转换,这样的做法是不严谨的,但是老项目也没有办法。
15、noParse
如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquery 、lodash。
16、externals
已经使用了dll排除了一些第三方依赖,但是每个具体的项目还有一些外部依赖也可以排除,在不重现打包dll文件的情况下,可以使用externals。\
17、优化 resolve.extensions 配置
在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:
- resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
- 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
- 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。
移除.web.js
18、开启缓存
例如babel-loader开启缓存,第二次构建时会读取缓存:
webpack自带的cache不用配置,默认开发环境开启。
HtmlWebpackPlugin、TerserPlugin、eslint-loader、babel-loader统统加上缓存
除了这些之外,还有cache-loader、HardSourceWebpackPlugin都是可以开启缓存的。我这里没做尝试,大家可以有兴趣试一试。
19、webpack编译两次
在使用webpack3的时候,经常会出现编译2次的情况,好像是低版本的html-webpack-plugin才有的问题,在我升级完以后,这个问题已经不存在了。
四、踩坑
坑1:
将dll的json中的meta替换为buildMeta即可。
坑2:
jsonpFunction 替换为chunkLoadingGlobal
坑3:
将loader替换为use即可。
坑4:
原因是因为自定义的webpack插件:
webpack的api变化,应该改写为
坑5:
将
改为:
坑6:
contentBase改为static
坑7:
disableHostCheck改为allowedHosts:'all'
坑8:
改为logging:'warn'
坑9:
webpack.optimize.CommonsChunkPlugin 升级到splitChunks
坑10:
将devserver的before换成onBeforeSetupMiddleware
坑11:
babel.rc文件中plugin不允许使用数组的配置了
坑12:
主要原因还是babel的预设都已经改为了@babel/preset-es2015这样的形式
改为安装新的预设包,并改写配置项:
坑13:
全局安装webpack-cli
坑14:
移除babel配置中的,并将@babel/preset-es2015和@babel/preset-stage-0换成@babel/preset-env
坑15:
将之前的写法改为
坑16:
改写为
坑17:
使用mini-css-extract-plugin代替extract-text-webpack-plugin即可。
改写为:
坑18:
对ossPlugin进行升级。
坑19:
安装 @babel/plugin-proposal-decorators插件即可。
坑20:
因为使用了babel-plugin-import,所以这个ts-import-plugin是多余的,可以去掉。
坑21:
这些报错主要是因为在js中使用了ts的功能,所以
js文件也应该用ts-loader
坑22:
类型断言只能在ts文件中使用
坑23:
zlib找不到,原因是webpack5以前是内置了nodejs的一些polyfills的,现在需要自己单独安装配置。
坑24:
还是因为装饰器的转换插件丢失了descriptor,
主要原因还是在我们的项目中,装饰器的使用的方法是
而官方的使用方法是:
在我们的项目中装饰的是class的propotery,而不是class method,所以最后根本没有走装饰器插件那里的转换。
这里只有2个解决办法,一个就是将项目中的代码改为和官方一样的写法,还有一个就是自己再把以前老的装饰器的插件对于class的propetry那块拿来重新一个自定义插件。
坑25:
uglifyjs-webpack-plugin改为terser-webpack-plugin
坑26:
安装配置assert
坑27:
安装配置stream-browserify
坑28:
加入ignoreOrder
坑29:
使用了esbuild之后,虽然速度提升了好几倍,
但是在webpack中配置的reslove的模块找不到引用了
因为这里项目中也是在babel中改变引用了路径的,要修复的话,必须要把crm-comps下面src的文件新建一个index.js暴露出来即可。
坑29:
主要是main设置的有问题,项目中是通过babel改变了引入的目录的,现在使用esbuild就会报错
改为:
五、优化效果
全部启动只花了不到10秒,还记得前面没有优化之前启动一个整个项目需要多久吗?1份10几秒,时间提升了90%!
内存使用减少了1.7G
转载自:https://juejin.cn/post/7062635710347477029