使用 esbuild-loader 和 swc-loader 优化 Vue CLI 5 项目构建时间

3,282 阅读6分钟

先上实验结果

构建框架耗时
vue cli 4 (babel-loader & TerserPlugin)7min
vue cli 5 (babel-loader & TerserPlugin)4min+
vue cli 5 (esbuild-loader & EsbuildPlugin)2min+
vue cli 5 (swc-loader & EsbuildPlugin)2min+

esbuild-loader 是什么

原文搬运:www.npmjs.com/package/esb…

Speed up your Webpack build with esbuild! 🔥

esbuild is a JavaScript bundler written in Go that supports blazing fast ESNext & TypeScript transpilation and JS minification.

总的来说有这样一些功能:

  • 替换 babel-loader
  • 替换 ts-loader
  • 替换 Terser or UglifyJs
  • 替换 DefinePlugin
  • 支持压缩 CSS

Vite 就是基于 esbuild 的。

swc-loader 是什么

SWC - Rust-based platform for the Web.

swc-loader allows you to use SWC with webpack.

Rspack 使用 builtin:swc-loader 对 TypeScript、JSX 以及最新的 JavaScript 语法进行转换。

Turbopack 原生使用 SWC 作为编译器。

都说用 GO 写的 esbuild 和用 Rust 写的 swc 比用 JavaScript 写的 babel 快很多,接下来验证下。

先用小项目验证

n年前的一个小项目,使用的是 webpack 3 自建脚手架。

将 webpack 3 切换到 vue cli 5 并使用 esbuild-loader,构建时长从2分钟优化到27秒。

  • webpack 3:2分钟
  • vue cli 5:1分钟
  • vue cli 5 & esbuild-loader:27秒

需要把转换 js 的 babel-loader 删掉,改为用 esbuild-loader。TerserPlugin 也删掉,使用 EsbuildPlugin 压缩代码。本项目不需要兼容一些低版本浏览器,支持 Chrome 90+ 就行,不需要完全编译为 ES5,可以减少构建耗时。

lintOnSave 记得设置为 false,生产构建时跑 eslint 也是很耗时的。

两种配置都可以:

vue.config.js

{
  lintOnSave: process.env.NODE_ENV !== 'production',
  configureWebpack: {
    optimization: {
      minimizer: [
        new EsbuildPlugin({
          target: 'chrome90',
          css: true
        })
      ]
    }
  },
  chainWebpack: config => {
    const rule = config.module.rule('js')
    rule.uses.clear()
    rule.use('esbuild-loader').loader('esbuild-loader').options({
      loader: 'js',
      target: 'chrome90'
    })

    config.optimization.minimizers.delete('terser')
  },
}

vue inspect > output.js 查看 minimizer 的配置。

{
  ...
  optimization: {
    ...
    minimizer: [
      {
        options: {
          target: 'chrome90',
          css: true
        }
      }
    ]
  },
  ...
}

更推荐第二种配置方式:

vue.config.js

const { EsbuildPlugin } = require('esbuild-loader')

{
  lintOnSave: process.env.NODE_ENV !== 'production',
  chainWebpack: config => {
    const rule = config.module.rule('js')
    rule.uses.clear()
    rule.use('esbuild-loader').loader('esbuild-loader').options({
      loader: 'js',
      target: 'chrome90'
    })

    config.optimization.minimizers.delete('terser')

    config.optimization
      .minimizer('esbuild')
      .use(EsbuildPlugin, [{ target: 'chrome90', css: true }])
  },
}
{
  ...
  optimization: {
    ...
    minimizer: [
      /* config.optimization.minimizer('esbuild') */
      new EsbuildPlugin(
        {
          target: 'chrome90',
          css: true
        }
      )
    ]
  },
  ...
}

配置就两点:用 esbuild-loader 替换 babel-loader;去掉 terser,换成 EsbuildPlugin。

构建效果如前所示:

  • webpack 3:2分钟
  • vue cli 5:1分钟
  • vue cli 5 & esbuild-loader:27秒

优化效果还是很明显的。

应用到更大更复杂的项目

这个项目是用 vue cli 4 创建的,直接使用 esbuild-loader 遇到很多坑,没能解决。

考虑到前一个轻量级项目的成功经验,遂先升级到 vue cli 5,当然 vue cli 4 升级 vue cli 5 的过程依然踩了一些坑,不过都不难处理。

先看看升级前项目使用 vue cli 4 的打包情况:

FileSizeGzipped
dist/js/chunk-vendors.c3af1a2b.js4535.40 KiB1210.86 KiB
dist/ts.worker.js3560.75 KiB806.96 KiB
dist/js/chunk-4bce028b.5428fd26.js3144.10 KiB968.15 KiB
dist/js/chunk-b4acb204.5c187046.js2170.96 KiB642.01 KiB
dist/js/chunk-6a6dc5a4.b6240583.js2165.08 KiB644.84 KiB
dist/js/chunk-30ef5c8c.3ed9597e.js2007.50 KiB597.89 KiB

Time: 7 minutes

耗时 7 分钟左右,打包后的大文件又大又多。

再看看升级 vue cli 5 后的打包情况:

FileSizeGzipped
dist/js/chunk-vendors.80fdd77f.js4436.97 KiB1158.05 KiB
dist/ts.worker.js3556.88 KiB805.04 KiB
dist/js/81.43d383ad.js1332.49 KiB421.39 KiB
dist/js/chunk-common.f1e7960d.js1245.42 KiB352.42 KiB

Time: 256044ms

可以看到大文件明显变少了,耗时也缩短到了4分多钟。

chunk-vendors 还是很大的,有必要做分包处理。

使用 splitChunks 优化大文件

vue ui 打开 Analyzer 分析包文件,看看 chunk-vendors 里哪些包可以分出来。

image.png

vue inspect > output.js 查看 splitChunks 的默认配置。

{
  optimization: {
    realContentHash: false,
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },
    minimizer: [
      /* config.optimization.minimizer('terser') */
      new TerserPlugin(
        {
          ...
        }
      )
    ]
  },
}

monaco-editorelement-ui@antv/** 这几个比较大的包分拆。

vue.config.js

{
  chainWebpack: config => {
    config.optimization.splitChunks({
      cacheGroups: {
        monaco: {
          name: 'chunk-monaco',
          test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        element: {
          name: 'chunk-element',
          test: /[\\/]node_modules[\\/]element-ui[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        antv: {
          name: 'chunk-antv',
          test: /[\\/]node_modules[\\/]@antv[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    })
  },
}

借鉴 使用splitChunks

chunks:

  • all: 不管文件是动态还是非动态载入,统一将文件分离。当页面首次载入会引入所有的包
  • async: 将异步加载的文件分离,首次一般不引入,到需要异步引入的组件才会引入。
  • initial:将异步和非异步的文件分离,如果一个文件被异步引入也被非异步引入,那它会被打包两次(注意和all区别),用于分离页面首次需要加载的包。

再来看看分拆后的打包效果:

FileSizeGzipped
dist/ts.worker.js3556.88 KiB805.04 KiB
dist/js/chunk-monaco.a6d2fb5c.js2269.46 KiB540.92 KiB
dist/js/81.43d383ad.js1332.49 KiB421.39 KiB
dist/js/chunk-common.3fe65876.js1245.42 KiB352.42 KiB
dist/js/chunk-vendors.bd8a1fb8.js975.05 KiB306.00 KiB
dist/js/chunk-element.01e0ff3f.js751.09 KiB190.42 KiB
dist/js/2467.0bc420b8.js631.27 KiB196.59 KiB
dist/js/363.25f4118e.js511.77 KiB138.14 KiB
dist/js/5517.4ad6dc76.js468.67 KiB136.35 KiB
dist/js/chunk-antv.a6d76427.js437.98 KiB121.40 KiB

Time: 240821ms

效果还是比较明显的,原来 4436KB 的 chunk-vendors 现在被拆分成了几个小一点的包,耗时都差不多,4分钟。

使用 esbuild-loader 进一步优化

首先安装依赖。

npm i -D esbuild-loader

配置:

vue.config.js

const { EsbuildPlugin } = require('esbuild-loader')
...
{
  chainWebpack: config => {
    const rule = config.module.rule('js')
    rule.uses.clear()
    rule.use('esbuild-loader').loader('esbuild-loader').options({
      loader: 'jsx',
      target: 'chrome80'
    })

    config.optimization.minimizers.delete('terser')

    config.optimization
      .minimizer('esbuild')
      .use(EsbuildPlugin, [{ target: 'chrome80', css: true }])

    config.optimization.splitChunks({
      cacheGroups: {
        monaco: {
          name: 'chunk-monaco',
          test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        element: {
          name: 'chunk-element',
          test: /[\\/]node_modules[\\/]element-ui[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        antv: {
          name: 'chunk-antv',
          test: /[\\/]node_modules[\\/]@antv[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    })
  },
}

此时用 vue inspect > output.js 看看新的 Webpack 配置。

{
  optimization: {
    realContentHash: false,
    splitChunks: {
      cacheGroups: {
        monaco: {
          name: 'chunk-monaco',
          test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        element: {
          name: 'chunk-element',
          test: /[\\/]node_modules[\\/]element-ui[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        antv: {
          name: 'chunk-antv',
          test: /[\\/]node_modules[\\/]@antv[\\/]/,
          priority: 1,
          chunks: 'initial'
        },
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },
    minimizer: [
      /* config.optimization.minimizer('esbuild') */
      new EsbuildPlugin(
        {
          target: 'chrome80',
          css: true
        }
      )
    ]
  },
...
      /* config.module.rule('js') */
      {
        test: /\.m?jsx?$/,
        exclude: [
          function () { /* omitted long function */ }
        ],
        use: [
          /* config.module.rule('js').use('esbuild-loader') */
          {
            loader: 'esbuild-loader',
            options: {
              loader: 'jsx',
              target: 'chrome80'
            }
          }
        ]
      },
}

使用 esbuild-loader 的打包情况:

FileSizeGzipped
dist/ts.worker.js3614.37 KiB841.85 KiB
dist/js/chunk-monaco.d6e97f94.js2323.88 KiB580.06 KiB
dist/js/3248.10a0d27c.js1386.29 KiB435.81 KiB
dist/js/chunk-common.a7f44eae.js1286.91 KiB358.54 KiB
dist/js/chunk-vendors.95f02ea3.js1081.33 KiB328.69 KiB
dist/js/chunk-element.6a0335ac.js755.60 KiB198.92 KiB
dist/js/4634.497be067.js647.22 KiB192.28 KiB
dist/js/6121.228f6aad.js632.93 KiB199.96 KiB
dist/js/7959.7bc34ce1.js521.49 KiB142.09 KiB
dist/js/chunk-antv.4f544fdc.js440.22 KiB127.06 KiB

Time: 133415ms

构建包大小差别不大,构建耗时从4分钟优化到了2分钟多一点。

使用 swc-loader

使用 swc-loader 替代 babel-loader,因 swc-loader 的 minify 尝试无效果,依然使用 esbuild-loader 的 EsbuildPlugin 来替代 TerserPlugin。

安装依赖:

npm i -D @swc/core swc-loader

修改 vue.config.js

{
  chainWebpack: config => {
    const rule = config.module.rule('js')
    rule.uses.clear()
    rule.use('swc-loader').loader('swc-loader')

    config.optimization.minimizers.delete('terser')

    config.optimization
      .minimizer('esbuild')
      .use(EsbuildPlugin, [{ target: 'chrome80', css: true }])

    ...
  },
}

额外添加配置文件 .swcrc

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": true,
      "dynamicImport": true
    },
    "minify": {
      "compress": true,
      "mangle": true
    }
  },
  "env": {
    "targets": {
      "chrome": "80"
    },
    "corejs": "3"
  },
  "minify": true
}

使用 swc-loader 的打包情况:

FileSizeGzipped
dist/ts.worker.js3614.21 KiB841.85 KiB
dist/js/chunk-monaco.d6fd694c.js2309.22 KiB579.10 KiB
dist/js/5391.e7a92584.js1372.97 KiB432.64 KiB
dist/js/chunk-common.85c44fdd.js1283.95 KiB357.94 KiB
dist/js/chunk-vendors.cf9dba32.js1075.11 KiB327.63 KiB
dist/js/chunk-element.615e6aba.js755.34 KiB198.92 KiB
dist/js/4326.7a6b4053.js645.09 KiB192.02 KiB
dist/js/4914.7598439b.js633.89 KiB200.15 KiB
dist/js/8313.48877e43.js521.19 KiB142.04 KiB
dist/js/chunk-antv.6b751d47.js430.46 KiB126.56 KiB

Time: 135782ms

构建时长和 esbuild-loader 差不多,也是2分钟多一点。

总结

构建框架耗时
vue cli 4 (babel-loader & TerserPlugin)7min
vue cli 5 (babel-loader & TerserPlugin)4min+
vue cli 5 (esbuild-loader & EsbuildPlugin)2min+
vue cli 5 (swc-loader & EsbuildPlugin)2min+

可以看出使用 esbuild-loader 和 swc-loader 都能明显加快构建速度,相比 babel-loader,耗时差不多缩短一半。