swc、esbuild和vite前端构建工具浅析

12,050 阅读8分钟

背景

过去几年里,前端又推出了一堆新的构建工具🔧,例如像以轻量快速著称的snowpack(目前已经不维护了,并推荐使用Vite)、编译速度超越Babel几十倍的SWC、打包和压缩资源速度惊人的esbuild,以及如今呼声最高,被誉为前端下一代构建工具的Vite。这么多的工具的推出,让以前一家独大的webpack突然失去的声音,甚至前端构建领域掀起了一场去webpack的浪潮🌊。今天就让我们简单了解下这些构建工具的代表SWCesbuildVite

SWC

简介

受限于JS的语言本身效率的问题,近几年前端领域出现了不少工具被Rust重写,其中就包括编译JS/TS文件速度比Babel快不少的SWC,其所对标的工具就是Babel

image.png

SWC全称为Speed Web Compiler,其是基于Rust实现的工具,目前被很多前端知名项目(Next.js、Parcel和Deno)所使用。

核心功能库

@swc/cli: CLI 命令行工具,可通过命令行编译文件。

@swc/core: 编译转码核心的API的集合。

swc-loader: 该模块允许您将 SWC 与 webpack 一起使用。

@swc/wasm-web: 该模块允许您使用 WebAssembly 在浏览器内同步转换代码。

@swc/jest: 该模块可以让jest的tranform速度更快。

而这些功能库几乎都能在Babel找到对应的库,例如@babel/cli@babel/core、以及babel-loader等。也更加印证了SWC的竞争对手就是Babel。

功能介绍

编译JS文件

通过将一个es6语法的JS文件,编译为es5的语法来比较两款工具编译能力:

Babel的编译结果: image.png

SWC的编译结果: image.png 最终SWC只花了0.12s,Babel花了1.08s,SWC的编译速度约为Babel的近9倍🚀。

在webpack中使用

在webpack中SWC也可以和babel掰掰手腕,SWC提供了swc-loader,其实也跟babel-loader的作用差不多。

在没有无缓存的情况下比较babel-loader与swc-loader在webpack中的编译情况:

babel-loader的编译耗时: image.png

swc-loader的编译耗时:

image.png

最终swc-loader只花了4.89s就完成了文件编译工作,而babel-loader花了12.56s,可见即使是在webpack中,swc的编译效率依旧很高。

swcpack

swc的打包能力还在建设中,目前只能在spack.config.js文件中进行一些简单的配置,预计在V2版本会SWC的bundle能力会有较大的提升。

// spack.config.js
const { config } = require('@swc/core/spack')

module.exports = config({
    entry: {
        'web': __dirname + '/src/index.ts',
    },
    output: {
        path: __dirname + '/lib'
    },
    module: {},
});

配置文件

swc有自己的配置文件.swcrc,与babel的配置文件不同的是swc的配置文件基本做到开箱即用,不需要进行对插件或预设进行二次安装。

{
  "jsc": {
    "parser": { 
      // 语法,支持ecmascript和typescript 
      "syntax": "ecmascript", 
      // 是否解析jsx,对应插件 @babel/plugin-transform-react-jsx 
      "jsx": false, 
      // 动态加载 等同于 @babel/plugin-syntax-dynamic-import 
      "dynamicImport": true, 
      // 装饰器 等同于 @babel/plugin-syntax-decorators 
      "decorators": false, 
      //顶层await 等同于@babel/plugin-syntax-top-level-await 
      "topLevelAwait": false, 
      ... 
      // 支持多种编译插件的配置
      },
    // 编译目标 
    "target": "es5", 
    // 等同babel-preset-env的松散配置 
    "loose": false, 
    // 输出代码可能依赖于辅助函数来支持目标环境。
    "externalHelpers": false
  }, 
  // 压缩代码 
  "minify": false
}

详细配置见官网配置

小结

优势:

  • 编译速度快
  • 迁移成本低,基本可以从babel无痛迁移并能覆盖基本的使用场景。

不足:

  • 生态相比于babel来说不够完善,用户的覆盖面也不高,某些场景可能会有试错成本。
  • 若需要深入开发的话,需要学习Rust,有较高的学习成本。
  • SWC虽然有bundle能力,但是bundle能力还不太完善,目前其在工程化领域更像是Compiler(编译工具)。

esbuild

简介

image.png

esbuild基于Golang开发的一款打包构建工具,相比传统的打包构建工具,主打性能优势,在构建速度上可以快 10~100 倍。

下图为esbuild和其他的构建工具用默认配置打包10个three.js库所花费时间的对比,我们能看见esbuild比webpack5的构建速度快了很多倍。 image.png

为什么能这么快?

根据官网esbuild的FAQ给出解释,简要总结下esbuild相比于传统构建工具有以下优势:

  • 语言优势。esbuild是基于go语言,传统的JS开发的构建工具并不适合资源打包这种 CPU 密集场景下,go更具性能优势。
  • 多线程能力。go具有多线程运行能力,而JS本质上就是一门单线程语言。由于go的多个线程是可以共享内存的,所以可以将解析、编译和生成的工作并行化。
  • 从零开始。从一开始就考虑性能,不使用第三方依赖,从始至终是使用的是一致的数据结构从而避免昂贵的数据转换。
  • 内存的有效利用。webpack的工作机制在经过不同的工具链的时候,都会进行(string => AST => string => ... => string)string到AST的不断转换,这样实际上会占用更多的内存并降低速度。而esbuild从头到尾尽可能的共用一份AST,从而降低内存的占用,提升编译速度。

主要功能

核心API

esbuild对外提供了两个核心API——tranform和build,主要功能如下:

  • 支持将js、ts、jsx、tsx、css等一系列文件的转译。
  • 支持文件监听和devServer。
  • 支持sourcemap。
  • 支持code-splitting。
  • 支持tree-shaking和文件压缩。
  • ...

详细的可见官方文档

esbuild-loader

也许我们单纯去使用esbuild去编译打包我们的项目还是比较麻烦,esbuild所提供的esbuild-loader帮我们解决了这个难题。在webpack中我们可以通过esbuild-loader体验到惊人的JS/TS文件编译的速度和高效的压缩能力。

接下来对比下在webpack环境下esbuild-loaderTerserPlugin的压缩效率:

TerserPlugin的压缩耗时: image.png

esbuild-loader的压缩耗时: image.png

两者压缩之后的JS产物的大小几乎没有太大差别,但是TerserPlugin所花费的时间是esbuild-loader的10多倍。

小结

优势:

  • 构建速度非常快
  • 压缩能力也非常强,可支持JS和CSS的压缩。

不足:

  • 其tranform的API不能将产物编译到es5及以下,产物无法兼容低版本的浏览器。
  • 直接使用esbuild进行打包具有一定的使用成本,并且不能完全覆盖使用场景。
  • 在代码分割和 CSS 处理方面功能还有待完善。

Vite

简介

image.png webpack的冷启动热更新速度一直被人诟病,也由此被戏称没有一个程序员能拒绝在启动一个webpack项目的时候冲一杯咖啡☕️。而一众基于浏览器原生ESM支持实现的no-bundle构建工具的问世,让前端开发者又看到了新的希望,其中Vite又属于是no-bundle构建工具阵营中的集大成者。

特点

  • 快速的冷启动:Vite基于ESM的支持实现了no-bundle服务,并且通过esbuild完成对依赖预构建工作。
  • 毫秒级的热更新:不同于webpack的HMR(热更替),Vite基于原生ESM实现一套HMR方案,其可以精确锁定HMR的更新边界,无需重新构建;并利用浏览器缓存策略提升请求速度。
  • 开箱即用:Vite就像CRA一样内置了对大部分文件的支持,并拥有一套,把用户的使用成本降到最低。
  • 强大的插件生态:Vite继承了Rollup优秀的插件接口设计,意味着Vite用户可以利用Rollup强大的生态系统进行功能的扩展。

预构建的作用

  • 转换文件格式:由于一些第三方依赖并没有ESM版本,而为了能在Vite上运行他们,则需要将其他格式转化为ESM的格式并缓存入 node_modules/.vite(默认路径)。
  • 减少HTTP请求:Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。例如像lodash-es这种库里面许多单独的文件相互导入,当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求。

小结

优势: 前面我们介绍Vite特点的时候就已经介绍了Vite的优势了,这边就不再赘述了。

不足:

  • 项目启动或使用懒加载时页面加载时间过长

    no-bundle是一个双刃剑,虽然减少了开发环节构建时间,但是大量未经打包的ESM模块可能导致出现大量HTTP请求,从而导致页面加载会变得很慢。这也是为什么Vite在生产环境还是选择rollup进行打包,而不是直接用原生ESM模块。

  • 预构建影响devServer性能

    由于首次预构建在devServer启动之前,若页面依赖较多预构建就会长时间阻塞devServer,从而导致页面加载时间过长;当依赖发生变化,导致页面重新加载。

  • 生态相比于webpack还有一定差距,不过差距正在不断缩小。

总结

未来肯定会有更多的人不断的开始使用这些非JS开发的前端工具链,因为JS本身语言的种种性能问题,随着前端越来越注重性能和体验,像SWCesbuild这类工具会越来越多。并且随着浏览器兼容性越来越好,no-bundle可能将是未来的一个趋势,未来会有更多的开发者完善相关的生态,而像Vite这类no-bundle工具和传统的bundle类构建工具终会有一次“时代交接”。

附录