前言
本篇文章主要记录在使用esbuild-loader代替babel-loader去优化公司巨石umi3项目时的一些报错解决方案。
话不多说,先看优化前后对比图:
开发环境冷启动对比:
生产环境打包对比:
可以看到开发环境冷启动效率提高了125%,生产环境打包效率提高了110%,效率提升还是非常大的,接下来就来讲讲umi3项目是如何做优化的。
先整体看一下优化方案:
将umi默认使用的自定义babel-loader替换为esbuild-loader。umi在生产模式下压缩js文件默认使用terser-webpack-plugin插件,压缩css文件默认使用optimize-css-assets-webpack-plugin,优化后js文件使用esbuild-loader提供的压缩器进行压缩,css文件的压缩也可以使用esbuild-loader自带的css压缩器进行压缩(3.~以上版本的esbuild-loader才压缩css文件,低版本的不提供)。- 在开发模式下不将css文件拆分为单独的文件。
- 删除umi自定义的编译node_modules包的loader。
- soucemap选择。
- 后缀扩展名查找优化。
其中将umi默认使用的自定义babel-loader替换为esbuild-loader在开发环境和生产环境都有效,并且是优化效率提升最高的手段。
什么是esbuild-loader
esbuild-loader 是一个为 webpack 设计的 loader,它基于 esbuild 这个极速的 JavaScript 打包器和压缩器。在 webpack 的构建流程中,loader 用于对模块的源代码进行转换。传统的 loader,如 babel-loader 或 ts-loader,在处理大量文件时可能会变得相当慢。而 esbuild-loader 利用 esbuild 的高性能,提供了更快的构建速度。
npm install esbuild-loader -D
module.exports = {
module: {
rules: [
+ // Use esbuild to compile JavaScript & TypeScript
+ {
+ // Match `.js`, `.jsx`, `.ts` or `.tsx` files
+ test: /.[jt]sx?$/,
+ loader: 'esbuild-loader',
+ options: {
+ // loader: 'js | jsx | ts | tsx' 如果不配置该属性,esbuild-loader会根据文件后缀自动匹配使用哪些loader,也可以指定该属性精准匹配文件后缀
+ target: 'es2015' // 转换后的目标代码
+ }
+ },
],
},
}
speed-measure-webpack-plugin插件
我们想要优化一个项目的编译效率,首先得知道编译过程中得哪一部分耗时时间久,效率慢,我们就对哪一部门进行专门的优化,而speed-measure-webpack-plugin(SMP)插件就正好可以满足我们的这个需求,它是一个专为Webpack设计的插件,主要用来测量和展示Webpack构建过程中各个部分(包括plugin和loader)的时间消耗。
modules with no loaders消耗时间长是因为项目过大依赖的node_modules包也非常多,导致webpack在构建依赖图以及生成最终文件时都涉及到大量文件IO操作,导致时间过长,后续替换babel-loader后这段时间也会被优化
从图中我们可以看出,编译过程中主要耗时deps文件夹中的loader耗时过长,但是由于umi项目中的loader以及plugin大部分都是自定义的,所以没有显示原有的loader名称,我们可以在umi的配置文件中的chainWebpack属性中打印一下umi整体的webpack配置:
chainWebpack: (config) => {
console.log(config.toString());
}
不仅仅是babel-laoder,还有一些像css-loader也是放在deps这个包中,因此我们可以着手dep包中的loader进行优化。
1.将umi的自定义babel-loader替换为esbuild-loader
umi是通过chainWebpack属性来操作底层的webpack配置的,因此修改配置时我们使用webpack-chain。
chainWebpack: (config) => {
// 1.首先删除原有的babel-loader配置
config.module.rules.delete('js');
// 添加esbuild-loader
config.module.rule('esbuild').
test(/\.(js|mjs|jsx|ts|tsx)$/).
exclude.add(/node_modules/).end().
use('esbuild-loader').loader('esbuild-loader').tap(options => {
return {
target: 'es2015'
};
});
}
可以看到,我们只返回了target属性,没有返回loader属性,那么esbuild-loader会根据文件后缀自动匹配要使用哪个loader,具体规则如下:
翻译过来就是:
- js文件使用js loader但是文件内
不能出现jsx语法 - jsx和tsx文件可以用jsx loader
- ts文件使用ts loader但是文件
不能出现jsx语法 - tsx文件可以使用tsx loader
现在我们来启动项目:
可以看到会有大量js文件报错说当前未启用JSX语法扩展,进该文件看了才知道,这些报错的js文件内都包含了jsx语法,而由于我们是根据文件后缀自动匹配loader,js文件只能使用js loader,不识别jsx语法所以会报错。知道原因后就好办了,把配置分开精确指定loader而不用自动匹配
chainWebpack: (config) => {
// 1.首先删除原有的babel-loader配置
config.module.rules.delete('js');
// 添加esbuild-loader
config.module.rule('esbuild').
test(/\.(js|mjs|jsx)$/).
exclude.add(/node_modules/).end().
use('esbuild-loader').loader('esbuild-loader').tap(options => {
return {
target: 'es2015',
loader: 'jsx'
};
});
}
config.module.rule('esbuild').
test(/\.(ts|tsx)$/).
exclude.add(/node_modules/).end().
use('esbuild-loader').loader('esbuild-loader').tap(options => {
return {
target: 'es2015'
};
});
}
我们把js|jsx文件都使用jsx loader进行转换,因为jsx是js的扩展所以jsx loader可以对这两种文件进行转译,而ts和tsx文件还是采用自动匹配loader的方式,ts用ts loader,tsx用tsx loader。那么ts和tsx文件能不能指定tsx loader呢?答案是不能,原因官网也有写:
现在让我们来启动一下项目看看:
1.React未定义报错
由上图我们可以看到效率已经提高很多,剩下的优化基本上都是小打小闹了,现在我们打开项目看看有没有问题:
查看源码后得知esbuild-loader将react中的jsx元素都编译为了React.createElement()的形式但是却没有导入React导致的错误。
原名:原因:React17+后已经不强制在组件顶部导入import React from 'react',而是babel的预设@babel/preset-react处理了这种自动导入的情况,{ "presets": [ [ "@babel/presetreact", { "runtime": "automatic" } ] ] },而且umi自定义的babel也有自定义预设来处理自动导入的情况,如下图所示:
但是当替换为esbuild-loader之后,esbuild-loader并没有处理自动导入的能力,下边是esbuild-loader仓库中的一个issues里边作者回复的:
意思就是:esbuild-loader只关注代码转换,并不关注文件中的依赖解析,可以使用webpack自带的ProvidePlugin插件来解决这个问题。
ProvidePlugin是 Webpack 提供的一个插件,它可以自动加载模块,而不需要显式地在每个文件中import或require这些模块。这对于一些全局使用的库(例如React、jQuery、_$等)特别有用
现在打开项目后就不会报错了,可以正常打开。
2.antd组件样式没有编译进项目内
打开项目后,可以正常启动,但是页面中antd组件的样式并没有应用,通过浏览器控制台得知antd组件的css样式并没有被打包。
原因: 在没有使用esbuild-loader之前,umi使用的自定义babel-loader,其中有一个自定义babel预设配置如下图所示:
这段配置表示按需加载antd组件和样式其原理和babel-plugin-import插件相同,但是现在使用esbuild-loader编译文件,虽然组件中还是使用import { ×× } from ‘antd’来引入组件,这是antd组件库默认就支持的按需导入组件,但是需要将antd的css样式文件全局导入,但是使用esbuild-loader之后并没有提供按需导入css文件的插件也没有全局导入样式文件,所以导致antd组件样式无法应用,因此需要在全局样式文件中导入antd的css样式:
@import "~antd/dist/antd.css";现在再来看一下样式有没有被应用:
antd自定义主题样式没有生效
引入该css文件之后,打开页面发现还是有问题,一些按钮等组件的背景颜色显示错误,全局查找测试环境按钮背景颜色发现该颜色是在umi配置文件中的theme属性中进行配置的:
查看umi文档该属性的作用是配置主题,实际上是配 less 变量。又去看了一下antd组件的官网,发现官网也有说明
原理就是使用 less 提供的 modifyVars 的方式进行覆盖变量,而且我们看umi自定义的less-loader也能看到theme中的配置都放到了modifyVars中,但是由于我们全局引入的是css文件所以不会起作用,只要把css文件改为less文件就可以解决了。
2.生产模式下压缩代码插件的更改
在生产模式下,构建过程中根据进度条可以看出在terser阶段耗费时间过长,可以考虑在生产模式下将terser-webpack-plugin以及optimize-css-assets-webpack-plugin插件替换为esbuild-loader提供的压缩插件
如果是esbuild-loader3.~以上版本,提供了js和css的压缩器。
import { EsbuildPlugin } from 'esbuild-loader';
if(isProduct){
config.optimization.minimizers.clear(); // 清除js的压缩插件terser-webpack-plugin
config.plugins.delete('optimize-css'); // css的压缩插件optimize-css-assets-webpack-plugin,使用esbuild3.×版本
// 使用esbuild压缩代码
config.optimization.minimizer('esbuild').use(ESBuildPlugin, [{target:
'es2015', css: true}]); // 3.×版本的压缩写法
}
如果是esbuild-loader2.~以上版本,只提供了js压缩器,css压缩还需要使用optimize-css-assets-webpack-plugin插件。
import { ESBuildPlugin, ESBuildMinifyPlugin } from 'esbuild-loader';
if(isProduct){
config.optimization.minimizers.clear(); // 清除js的压缩插件terser-webpack-plugin
// 使用esbuild压缩代码
config.optimization.minimizer('esbuild').use(ESBuildMinifyPlugin, [{target: 'es2015'}]);
}
改变压缩器后时间大概能提升15~20s,效果还是可以的。
3.在开发模式下不将css文件拆分为单独的文件
在开发模式下将css文件放到style标签内,而不是抽离出单独的文件,需要将mini-css-extract-plugin替换为style-loader。
4. 删除umi自定义的编译node_modules包的loader。
umi3默认是编译node_modules包中的依赖的,但是绝大部分node_modules包都是已经编译后的,所以我们可以设置不编译node_modules包的依赖,有两种方式:
1.使用nodeModulesTransform配置项
2.直接将umi3默认的自定义编译node_modules包中依赖的loader删除
// 删除加载node_modules包的loader
config.module.rules.delete('js-in-node_modules');
config.module.rules.delete('ts-in-node_modules');
5.souceMap选择
umi3项目中开发模式下devtool为cheap-module-source-map,生产模式下devtool为false,如果不关注开发调试是否方便,也可以将开发模式下也设置为false,编译速度也能提升2~3s但是不建议。
6.后缀扩展名查找优化
先看下umi3默认resolve.extensions配置
而我们的文件大部分都是以js和jsx结尾,因此将这些常用的后缀名优先级提前,也可以帮助我们减少查找文件的时间,提高构建速度。
const initExtensions = Array.from(config.resolve.extensions.values());
const sortExtensions = ['.jsx', '.js', '.ts', '.tsx', '.mjs'];
initExtensions.forEach(e => {
if (!sortExtensions.includes(e)) {
sortExtensions.push(e);
}
});
config.resolve.extensions.clear();
sortExtensions.forEach(e => {
config.resolve.extensions.add(e);
});
其余的比较常规的优化比如使用thread-loader、cdn等等大家可以根据自己的项目来进行优化,这里就不再讲解了。
如果文章有什么错误,希望大家可以多多指正! 或者在使用esbuild-loader过程中,遇到其它的错误也欢迎大家来讨论!