Webpack 是目前最为流行的构建工具。在开发中几乎每次修改代码都需要要让Webpack重新打包。这个时候效率就成为了一个非常关键的指标。假设每次都能省几秒钟,一天上百次就可以提前十几分钟下班。
下面就分析一下2022年在Webpack5.0下如何让构建效率提高十倍以上。
相关代码请联系: 微信【xiaran310574】
原理分析
要提高Webpack的构建效率,首先要从原理上理解Webpack的构建过程是什么。
Webpack可以认为是一个静态模块打包器。简单的说,由于前端工程庞大,必须使用模块化的方式组织代码。但是运行发布的时候又必须将模块整合起来。这个整合的过程就是Webpack 构建过程。
这个过程有点像装配作业,将很多不同种类的零件,整合在一起。
Webpack的扮演的角色就是流水线上的机械臂。整个作业的过程就像是由无数小任务组成的大任务。
根据这个形象的比喻你就可以很形象的理解构建工作了。也的可以很形象的思考如何提高构件效率了。快速完成一堆任务无非是四种方法。
- 不重复作业: 缓存技术
- 并行同步作业: 并行处理、多线程、多进程
- 高效作业:使用高效编辑器SWC、ESBuild
- 精简作业:减少步骤、缩减范围
不重复作业 - 缓存
顾名思义,不重复作业就是说不必要做的事情不做。比如,在开发时有几百个模块。每次只变更其中一个模块。这个时候重新构建将所有的模块都解析一次就是一种重复作业。这种方式主要是提高二次构建性能。也就是说第一次干完的事,后面就不需要全干了。
最好的解决方案是将不变的模块的解析结果都缓存起来,这样做的活少了自然速度也就快了。
在 webpack 中模块解析工作是叫个 一组Loader 完成的。
比如:CSS 文件会经过 style-loader和css-loader。
假设你想在处理前多一个检查是否利用缓存的操作可以在前面加一个 cache-loader就可以了。
但是到了 Webpack5 时代这个过程被进一步简化只需要简单的配置 cache 属性就可以了。
// webpack.config.js
module.exports = {
cache: {
type: "filesystem",
},
}
下面对比一下前后的差距。
大家可以明显看到,使用前后编译速度提高了13倍。
可以说效果立竿见影。
并行同步作业 - 多进程处理
这个优化思路也很好理解,一堆事情要做,一定是并行处理速度远高于串行处理速度。
对于基于node的webpack来讲受限于单线程驱动,默认情况下只能使用cpu的一个核心进行工作。对于多核cpu已经成为标配的时代算是一种极大的浪费。最好的方法就是开启并行处理方式。
目前开启并行的方式分为:
- HappyPack: 多进程方式加载Loader,停止更新。
- Thread-loader: 多进程资源加载,官方方案
- Parallel-Webpack: 多进程方式运行Webpack构建实例
- 支持多进程的Loader和Plugin:比如:TerserWebpackPlugin的多进程模式
首先先讲一下前两个方案, HappyPack和Thread-loader。他们两个思路非常类似。由于Thread-loader这种官方方案的推出,HappyPack已经宣布停更。所以以后只能用Thread方案了。
Parallel-Webpack的应用范围是多入口场景。相当于开启了多个webpack。
最后支持多进程的Loader的这种方式那就要看具体的Loader是否支持了。支持开启选项就可以了。
下面测试一下Thread-Loader的效果
你会发现,在本测试项目中,使用了Thread-loader的时候反而会降低运行效率,也就是反向优化。造成这个的原因可能是模块数量不够。由于创建进程也是有时间开销的,而如果并行处理节省的时间还不足以抵消创建进程的时间开销。那这样的优化就是反向优化。
实际上,我对Thread-loader的优化效率、以及各个场景下效果都做了系统的测试。
包括:
- 多进程与并发数、核心数
- 不同规模项目,CSS、Typescript
这个我后面会专门发一篇文章给大家做详尽的对比。结论是受限制于项目规模和Loader的兼容性问题提升效果并不明显。
高效作业 - ESBuild编译器
下面聊一下高效作业的问题,其实就是使用更高性能的语言。由于JS不适合计算密集型作业。所以这部分最好能够找到更高效的执行环境。目前比较成熟的方案是使用高效编译器。比如SWC、ESBuild。
首先先介绍一下这两个大将。
- ESBuild是基于Go语言开发的JavaScript Bundler, 由Figma前CTO Evan Wallace开发, 并且也被Vite用于开发环境的依赖解析和Transform.
- SWC则是基于Rust的JavaScript Compiler(其生态中也包含打包工具spack), 目前为Next.JS/Parcel/Deno等前端圈知名项目使用.
也就是说他们都是用了更为高性能的语言。所以性能提升非常明显。
这个时候就可以考虑使用高效编译器来提高性能。
这个效果其实就相当于汽车加氮气加速。
虽然不是 🚀 但是用了火箭的技术。
目前比较好的方案有两个:
- 使用ESBuild 或SWC 负责 Typescript 的编译
- 使用ESBuild 或 SWC 复杂压缩丑化
下面来一波性能对比。
我们就以压缩丑化为例,可以通过实质 minify 压缩器来使用不同的压缩引擎。
最后结果
- 默认: 3608ms
- SWC: 1889ms
- ESBuild: 1746ms
实际上只是将压缩阶段的工作交给了swc 如果是全部编译工作性能提升还会更加明显。这部分提升了50%以上的效果。
精简作业
这部分的主要目的就是尽量减少工作量。比如:减少不必要的编译步骤和对象。
主要策略包括:
- 开发阶段禁止产物优化
- 非必要不产生SourceMap
- 约束执行范围 - exclude
- NoParse 功能跳过编译
开发阶段禁止产物优化
产物优化的目的是为了提高执行效率,主要包括
- minimize 压缩
- splitChunks 分包
- concatenateModules 模块连接
- usedExports Tree-shaking 功能
这部分显然在开发阶段并不重要,至少相对于构建效率来讲。所以应该关闭。
module.exports = (base) => {
base.optimization = {
removeAvailableModules: false,
removeEmptyChunks: false, //
splitChunks: false, // 代码分包
minimize: false, //代码压缩
concatenateModules: false,
usedExports: false, // Treeshaking
};
};
非必要不产生SourceMap
Sourcemap的目的是为了能够准确定位线上错误,但是如果你的系统还没有异常监控系统。那Sourcemap就没啥用了。这部分也是可以省略的。这部分也可以
约束执行范围 - exclude
他的目的是为了约束模块resolve搜索范围。由于Loader转换过程需要将代码转换为AST语法树,这一步非常耗费资源,最好将转换的范围约束到最小范围下。一般会将 node_module 排除出去。
module.exports = (base) => {
base.module.rules.map((v) => {
v.exclude = /node_modules/;
});
return base;
};
效果对比
排除node_modules大概比原来节省了10%左右的编译时间。
NoParse 功能跳过编译
功能是跳过编译,以Vue3为例,Vue3已经提供了打包完成的 esm 文件。这个时候没必要重复进行依赖打包。所以可以通过 no-parse跳过。
const path = require("path");
module.exports = (base) => {
base.module.noParse = /(^vue$)|(^pinia$)|(^vue-router$)/;
return base;
};
这里面将vue全家桶全部no-parse掉。
可以看到NoParse前后速度提高了0.6s,大概 20%的提升。还是很明显的。
除此之外还有更多的细节可以处理。
最后总结
Webpack作为最为普及的打包工具,大家每天都会无数次的使用。他的执行效率直接影响了开发效率和编程体验。优化构建效率是一个非常重要的工作。里面有很多细致的工作需要做。结合实际的项目和场景他的优化方式也不太相同。我整理了一张思维脑图。需要测试项目代码的也可以联系然叔。
www.processon.com/mindmap/631…
相关程序代码请联系
微信【xiaran310574】