swc与esbuild-将你的构建提速翻倍

背景

最近一直在尝试对项目构建性能进一步优化, 除了webpack常用的一些优化方式,比如缓存优化、多进程优化等,也尝试在项目内使用swcesbuild,以进一步提高构建速度,本篇主要记录如何在项目中使用swcesbuild,及落地过程中碰到的一些问题,避免后续踩坑

项目内使用

使用swc

安装swc-loader

pnpm i -D @swc/core swc-loader @swc/helpers

使用swc-loader替换babel-loader

module: {
 rules: [
  {
    test: /\.m?js$/,
    exclude: /(node_modules)/,
    use: {
      // `.swcrc` can be used to configure swc
-      loader: "babel-loader",
+      loader: "swc-loader",
+    	 options: {
        	jsc: {
            parser: {
              syntax: "typescript",
              tsx: true,
              decorators: true,
            },
            transform: {
              legacyDecorator: true,
            },
            externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
            target: 'es5',
          },
          env: {
            targets: "last 3 major versions, > 0.1%", // 根据项目设置
            mode: "usage",
            coreJs: "3" // 根据项目选择
          },
          isModule: 'unknown'
       }
    }
  }
  ]
}

terser-webpack-plugin开启swc压缩

import TerserPlugin from 'terser-webpack-plugin';

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            unused: true,
            drop_console: true,
            drop_debugger: true
          },
          mangle: true,
        },
        minify: TerserPlugin.swcMinify,
      })
    ]
  }
}

使用esbuild

安装esbuild-loader

pnpm i -D esbuild-loader

使用esbuild-loder替换babel-loader

  module.exports = {
    module: {
      rules: [
-       {
-         test: /\.js$/,
-         use: 'babel-loader'
-       },
+       {
+         // Match js, jsx, ts & tsx files
+         test: /\.[jt]sx?$/,
+         loader: 'esbuild-loader',
+         options: {
+           // JavaScript version to compile to
+           target: 'es2015',
+           loader: 'tsx',
+         }
+       },

        ...
      ],
    },
  }

使用esbuild压缩

import { EsbuildPlugin } from 'esbuild-loader';

module.exports = {
  optimization: {
    minimizer: [
      new EsbuildPlugin({
        target: 'es2015',
        css: true, // 开启css压缩,当这里开启的时候,就不需要css-minimizer-webpack-plugin这样的css压缩插件
      })
    ]
  }
}

具体demo

选择方案

其实webpack构建过程中,相对耗时的两个部分,第一部分就是代码转换;第二部分就是代码压缩,所以无论是切换到swc还是切换到esbuild,还是一直使用的babelterser,我们最终的目标

  • 项目能够顺利构建
  • 构建产物代码能够正常运行
  • 构建时间尽可能的短
  • 构建产物尽可能的小

所以我们是可以去组合使用swcesbuildbabelterser,通过不同的组合去达成我们最终的目标

代码转换

代码转换的目的,目前主要有两个

  • ts转换成js
  • js高版本转换成js低版本,以保证兼容性

下面是几种转换工具的优缺点

  • babel: 基于Nodejs,现在使用最多的js代码转换工具
    • 优点: 功能最全,覆盖场景最广,最成熟,支持输出es5、es6+代码,支持动态polyfill
    • 缺点: 转换速度相对较慢
  • swc: 基于Rust,用来取代babel的js代码转换工具
    • 优点: 相对babel快20-70倍,另外支持文件打包,支持输出es5、es6+代码,支持动态polyfill
    • 缺点: 配套周边相对没有babel成熟,基于rust对上手难度大
  • esbuild: 基于Go,用来快速转换js的工具
    • 优点: 相对babel快10-100倍,另外支持文件打包,支持输出es6+代码,不支持动态polyfill
    • 缺点: 配套周边相对没有babel成熟,基于Go对上手难度大、不支持输出es5代码
  • tsc: 基于Nodejstypescript内置的转换器
    • 优点: 相对babel有ts类型检查,速度慢于babel,支持输出es5、es6+代码,不支持动态polyfill
    • 缺点: 速度慢,不支持取消类型检查

转换速度
esbuild > swc > babel > tsc

功能齐全
babel > tsc > swc > esbuild

所以我们可以基于我们的浏览器兼容目标,及我们需要达成的目标来选择不同的转换方案

压缩方案

js代码压缩,主要有两个目的

  • 减少代码尺寸,以达到访问速度提升目的
  • 混淆代码,防止js源代码在一定程度上的泄漏

下面是js代码压缩工具优缺点

  • teser: 基于Nodejs,由uglify fork出来的新一代压缩工具
    • 优点: 功能齐全,支持tree-shaking,压缩比率最高
    • 缺点: 速度相对较慢
  • esbuild: 基于Go,支持js代码压缩
    • 优点: 速度快
    • 缺点: 压缩比率相对terser低,压缩时如果target设置成es5,js代码内不能出现es6+代码,不然会报错
  • swc: 基于Rust,支持js代码压缩
    • 优点: 速度快
    • 缺点: 压缩比率相对terser

目前三者关于压缩率与压缩时间,如下所示

  • 压缩率 terser > esbuild > swc
  • 压缩时间 esbuild > swc > terser

所以我们可以根据实际项目选择代码压缩方式,更多压缩数据可以参考minification-benchmarks

项目实测

同一台电脑,同一个项目, 项目技术栈 react + antd 的常规后台cms系统

swc与esbuild-将你的构建提速翻倍
注意:

  • 多进程用的是happypack
  • 上述数据都是在未开启webpack缓存的前提下进行

从数据上可以看出

  • swc+esbuild+es5+多进程组合效果最好,相对于babel+terser+es5组合提升49.4%
  • swc+esbuild+es6+多进程组合效果最好,相对于babel+terser+es5组合提升48.6%
  • esbuild只能全局引入polyfill,无法根据使用的api动态导入polyfill
  • 而输出target一致的前提下,swc+esbuild组合输出包大小仅比babel+terser组合大1.1%,大小完全可以忽略

注意以上数据仅作参考,具体效果,应当以自己项目为准

推荐组合

下面是对项目配置的一些推荐参数与组合方式,仅供参考

pc端项目推荐配置

选择全局polyfill

  • 外部客户使用,推荐esbuild+esbuild+es2015+多进程组合,browserlist: 设置为edge 14
  • 内部客户使用,推荐esbuild+esbuild+es2015+多进程组合,browserlist: 设置为last 2 Chrome versions

配置如下所示

const target = 'es2015';

module.exports = {
  module: {
    rules: [
     {
       test: /\.[jt]sx?$/,
       loader: 'esbuild-loader',
       options: {
         // JavaScript version to compile to
         target
       }
     },
    ],
  },
  optimization: {
    minimizer: [
      new ESBuildMinifyPlugin({
        target,
        css: true, // 开启css压缩,当这里开启的时候,就不需要css-minimizer-webpack-plugin这样的css压缩插件
      })
    ]
  }
}

选择按需polyfill

  • 外部客户使用,推荐swc+esbuild+es2015+多进程组合,browserlist: 设置为edge 14
  • 明源内部使用,推荐swc+esbuild+es2015+多进程组合,browserlist: 设置为last 2 Chrome versions
const target = 'es2015';

module.exports = {
  module: {
    rules: [
     {
       // Match js, jsx, ts & tsx files
       test: /\.[jt]sx?$/,
       loader: 'swc-loader',
       options: {
          jsc: {
            parser: {
              syntax: "typescript",
              tsx: true,
              decorators: true,
            },
            transform: {
              legacyDecorator: true,
            },
            externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
            target,
          },
          env: {
            targets: "edge 14",
            mode: "usage",
            coreJs: "3.22"
          },
          isModule: 'unknown'
       }
     },
    ],
  },
  optimization: {
    minimizer: [
      new ESBuildMinifyPlugin({
        target,
        css: true, // 开启css压缩,当这里开启的时候,就不需要css-minimizer-webpack-plugin这样的css压缩插件
      })
    ]
  }
}

移动端项目推荐配置

选择按需polyfill

  • 兼容性要求到低配机型,要求输出es5,推荐swc+esbuild+es5+多进程组合,browserlist: 设置为ios 9
  • 兼容性要求不高,直接输出es6,推荐swc+esbuild+es6+多进程组合,browserlist: 设置为ios 11
const target = 'es5';

module.exports = {
  module: {
    rules: [
     {
       test: /\.[jt]sx?$/,
       loader: 'swc-loader',
       options: {
          jsc: {
            parser: {
              syntax: "typescript",
              tsx: true,
              decorators: true,
            },
            transform: {
              legacyDecorator: true,
            },
            externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
            target,
          },
          env: {
            targets: "IE 9",
            mode: "usage",
            coreJs: "3.22"
          },
          isModule: 'unknown'
       }
     },
    ],
  },
  optimization: {
    minimizer: [
      new ESBuildMinifyPlugin({
        target,
        css: true, // 开启css压缩,当这里开启的时候,就不需要css-minimizer-webpack-plugin这样的css压缩插件
      })
    ]
  }
}

可以根据公司的项目的实际使用情况,灵活进行选择

总结

从项目实践来看,可以得出如下结论

  • swcesbuild相比babelterser会快很多
  • 使用swc or esbuild的时候可能会碰到一些兼容性问题,需要自己排查解决
  • 从目前社区的使用情况,swc以后在项目中使用只会越来越普遍

FAQ

not implemented: automatic polyfill for scripts

原因是:@swc/core 版本过低,无法自动polyfill commonjs模块
解决方法:升级@swc/core到1.3.27+版本

TypeError: _type_of is not function

下面是没有压缩代码的抛错,如下图所示
swc与esbuild-将你的构建提速翻倍
如果是压缩之后的代码抛错,如下图所示
swc与esbuild-将你的构建提速翻倍
原因是:@swc/helpers common导出名称不对,对于commonjs模块,swc添加的helper写法是require('@swc/helpers')._type_of,而@swc/helpers内的导出是驼峰写法,如下所示
swc与esbuild-将你的构建提速翻倍
解决方法:添加webpack alias,然后在修改@swc/helpers的导出变量,具体如下所示

chain.resolve.alias.set('@swc/helpers$', path.join(__dirname, './swc/swc-helpers.js'));
const obj = require('@swc/helpers/lib/index');

function toLine(name) {
  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
}

const lineObj = Object.keys(obj).reduce((prev, key) => {
  prev[toLine(key)] = obj[key];
  return prev;
}, {});

module.exports = Object.assign({}, obj, lineObj);

swc转换装饰器抛错

swc与esbuild-将你的构建提速翻倍
原因:swc转换默认是没有开启支持装饰器语法的
解决:通过配置参数开启装饰器语法

jsc: {
  parser: {
    syntax: "typescript",
    tsx: true,
+    decorators: true,
  },
  transform: {
+    legacyDecorator: true,
  },
  externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
  target,
},

esbuild 压缩样式会导致部分样式在低版本浏览器上有差异

swc与esbuild-将你的构建提速翻倍
如上图所示,弹窗位置在低版本浏览器上,没有出现在页面中间位置
原因:esbuild压缩css会根据传入的targets转换部分css写法,比如color,position等
原始代码
swc与esbuild-将你的构建提速翻倍
esbuild压缩之后的代码
swc与esbuild-将你的构建提速翻倍
解决:esbuild压缩css的时候传入具体的浏览器版本

{
	optimization: {
    minimizer: [
      new ESBuildMinifyPlugin({
-        target: ['es2015'],
+        target: ['es2015', 'safari12'],
        css: true,
      })
    ]
  }
}

更多内容可以参考inset is not supported by many browsers

esbuild无法动态polyfill

原因是:esbuild本身没有提供这项能力
解决方法:手动在全局引入polyfill代码

Transforming class syntax to the configured target environment ("es5") is not supported yet

原因是: 如果是esbuild转换场景,不能输出es5代码,如果是压缩场景,则是有代码没有被转换成es5代码,导致esbuild压缩的时候报错
解决方法: 如果是esbuild转换场景,则输出es2015及以上;如果是压缩场景,则将没有转换成es5代码的包通过babel-loader or swc-loader 处理

原文链接:https://juejin.cn/post/7236670763272798266 作者:wks

(0)
上一篇 2023年5月25日 上午11:02
下一篇 2023年5月25日 上午11:12

相关推荐

发表回复

登录后才能评论