如何利用esbuild-loader让你的webpack项目也有🚀一般的速度?🔥🔥

1,078 阅读8分钟

前言

本篇文章主要记录在使用esbuild-loader代替babel-loader去优化公司巨石umi3项目时的一些报错解决方案。

话不多说,先看优化前后对比图:

开发环境冷启动对比:

image.png 生产环境打包对比:

image.png 可以看到开发环境冷启动效率提高了125%,生产环境打包效率提高了110%,效率提升还是非常大的,接下来就来讲讲umi3项目是如何做优化的。

先整体看一下优化方案:

  1. 将umi默认使用的自定义babel-loader替换为esbuild-loader。
  2. umi在生产模式下压缩js文件默认使用terser-webpack-plugin插件,压缩css文件默认使用optimize-css-assets-webpack-plugin,优化后js文件使用esbuild-loader提供的压缩器进行压缩,css文件的压缩也可以使用esbuild-loader自带的css压缩器进行压缩(3.~以上版本的esbuild-loader才压缩css文件,低版本的不提供)。
  3. 在开发模式下不将css文件拆分为单独的文件。
  4. 删除umi自定义的编译node_modules包的loader。
  5. soucemap选择。
  6. 后缀扩展名查找优化。

其中将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)的时间消耗。

image.png

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进行优化。

image.png

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,具体规则如下:

image.png 翻译过来就是:

  1. js文件使用js loader但是文件内不能出现jsx语法
  2. jsx和tsx文件可以用jsx loader
  3. ts文件使用ts loader但是文件不能出现jsx语法
  4. tsx文件可以使用tsx loader

现在我们来启动项目:

1727680521904.png 可以看到会有大量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呢?答案是不能,原因官网也有写:

image.png

image.png 现在让我们来启动一下项目看看:

image.png

1.React未定义报错

由上图我们可以看到效率已经提高很多,剩下的优化基本上都是小打小闹了,现在我们打开项目看看有没有问题:

image.png 查看源码后得知esbuild-loader将react中的jsx元素都编译为了React.createElement()的形式但是却没有导入React导致的错误。

原名:原因:React17+后已经不强制在组件顶部导入import React from 'react',而是babel的预设@babel/preset-react处理了这种自动导入的情况,{ "presets": [ [ "@babel/presetreact", { "runtime": "automatic" } ] ] },而且umi自定义的babel也有自定义预设来处理自动导入的情况,如下图所示:

image.png 但是当替换为esbuild-loader之后,esbuild-loader并没有处理自动导入的能力,下边是esbuild-loader仓库中的一个issues里边作者回复的:

image.png 意思就是:esbuild-loader只关注代码转换,并不关注文件中的依赖解析,可以使用webpack自带的ProvidePlugin插件来解决这个问题。

ProvidePlugin 是 Webpack 提供的一个插件,它可以自动加载模块,而不需要显式地在每个文件中 import 或 require 这些模块。这对于一些全局使用的库(例如 ReactjQuery_$ 等)特别有用

image.png 现在打开项目后就不会报错了,可以正常打开。

2.antd组件样式没有编译进项目内

打开项目后,可以正常启动,但是页面中antd组件的样式并没有应用,通过浏览器控制台得知antd组件的css样式并没有被打包。

image.png

原因: 在没有使用esbuild-loader之前,umi使用的自定义babel-loader,其中有一个自定义babel预设配置如下图所示:

image.png 这段配置表示按需加载antd组件和样式其原理和babel-plugin-import插件相同,但是现在使用esbuild-loader编译文件,虽然组件中还是使用import { ×× } from ‘antd’来引入组件,这是antd组件库默认就支持的按需导入组件,但是需要将antd的css样式文件全局导入,但是使用esbuild-loader之后并没有提供按需导入css文件的插件也没有全局导入样式文件,所以导致antd组件样式无法应用,因此需要在全局样式文件中导入antd的css样式:@import "~antd/dist/antd.css";现在再来看一下样式有没有被应用:

image.png

antd自定义主题样式没有生效

引入该css文件之后,打开页面发现还是有问题,一些按钮等组件的背景颜色显示错误,全局查找测试环境按钮背景颜色发现该颜色是在umi配置文件中的theme属性中进行配置的:

image.png 查看umi文档该属性的作用是配置主题,实际上是配 less 变量。又去看了一下antd组件的官网,发现官网也有说明

image.png 原理就是使用 less 提供的 modifyVars 的方式进行覆盖变量,而且我们看umi自定义的less-loader也能看到theme中的配置都放到了modifyVars中,但是由于我们全局引入的是css文件所以不会起作用,只要把css文件改为less文件就可以解决了。

2.生产模式下压缩代码插件的更改

在生产模式下,构建过程中根据进度条可以看出在terser阶段耗费时间过长,可以考虑在生产模式下将terser-webpack-plugin以及optimize-css-assets-webpack-plugin插件替换为esbuild-loader提供的压缩插件 image.png 如果是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

image.png

4. 删除umi自定义的编译node_modules包的loader。

umi3默认是编译node_modules包中的依赖的,但是绝大部分node_modules包都是已经编译后的,所以我们可以设置不编译node_modules包的依赖,有两种方式:

1.使用nodeModulesTransform配置项

image.png 2.直接将umi3默认的自定义编译node_modules包中依赖的loader删除

image.png

image.png

// 删除加载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配置

image.png 而我们的文件大部分都是以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过程中,遇到其它的错误也欢迎大家来讨论!