webpack 的挑战者们

355 阅读21分钟

在前端构建领域,webpack 是绝对的霸主,但是最近新的前端构建工具如雨后春笋般涌现。

为什么要淘汰 webpack,取而代之?新的挑战者们又有什么优势和 webpack 一决雌雄,我们一起来研究一下。

webpack 的厉害之处

webpack 首先是一个打包器,其次依赖强大的插件系统,丰富的 loader 和 plugin,让其成为最强大的前端构建工具。

  1. 模块打包:Webpack将应用程序拆分成多个模块,并使用模块间的依赖关系构建依赖图。然后,它将这些模块打包成一个或多个输出文件,减少了网络请求的数量,提高了加载速度。
  2. 支持多种模块化规范:Webpack支持多种模块化规范,如CommonJS、AMD、ES Modules等,使开发者可以使用各种模块化的方式来组织和管理代码。
  3. 资源加载优化:Webpack可以处理多种类型的资源文件,包括JavaScript、CSS、图片等。它可以通过加载器(Loaders)和插件(Plugins)来处理这些资源,例如将ES6代码转换为ES5、压缩和合并CSS文件、优化图片大小等。
  4. 开发环境支持:Webpack提供了丰富的开发环境支持,包括自动刷新、热模块替换(Hot Module Replacement)、源代码映射(Source Mapping)等功能,可以提高开发效率和调试体验。
  5. 代码拆分和懒加载:Webpack支持将应用程序代码拆分成多个块(chunks),并实现按需加载。这可以减小初始加载的文件大小,提高页面的加载速度,并在需要时动态加载额外的代码块。
  6. 插件系统:Webpack具有强大的插件系统,开发者可以使用现有的插件或编写自己的插件来扩展Webpack的功能。插件可以用于各种用途,如优化、压缩、代码分割、资源管理等。

webpack 的问题

  1. 过重的插件体系:插件体系是 webpack 的核心,事实上,webpack 的大部分功能都是通过内部插件或者第三方插件来完成的。可以说,webpack 的生态就是建立在众多插件之上的。但插件体系也同样有很多问题。

    例如:以一个标准的 vue-cli 生成的脚手架项目为例,一共有 7 个第三方插件:

    "copy-webpack-plugin": "^4.0.1",
    "extract-text-webpack-plugin": "^3.0.0",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^2.30.1",
    "webpack-bundle-analyzer": "^2.9.0",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "uglifyjs-webpack-plugin": "^1.1.1",
    

    以及 7 个 webpack 内置插件:

    • HashedModuleIdsPlugin
    • ModuleConcatenationPlugin
    • CommonsChunkPlugin
    • DefinePlugin
    • HotModuleReplacementPlugin
    • NamedModulesPlugin
    • NoEmitOnErrorsPlugin

    总共 14 个插件,我们按照平均一个插件含有 2-3 个配置项(这已经是往低了算了)来计算,14 个插件就有 30 多项配置,这已经是一个现代 webpack 开发、构建使用的很基础的配置了,真实的项目只会比这个更多。

  2. 性能差:开发环境项目启动慢,生产环境项目打包慢,这也是新的挑战者们重点发力解决的问题。

下面通过打包器大而全的构建工具两个方向来介绍一下 webpack 的挑战者们。

打包器新秀

rollup

rollup-logo.svg
  1. webpack比rollup早出2年,诞生在esm标准出来前,commonjs出来后。

    • 当时的浏览器只能通过script标签加载模块

      • script标签加载代码是没有作用域的,只能在代码内 用iife的方式 实现作用域效果

        • 这就是webpack打包出来的代码 大结构都是iife的原因
        • 并且每个模块都要装到function里面,才能保证互相之间作用域不干扰。
        • 这就是为什么 webpack打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
    • 关于webpack的代码注入问题,是因为浏览器不支持cjs,所以webpack要去自己实现require和module.exports方法(才有很多注入)(webpack自己实现polyfill)

      • 这么多年了,甚至到现在2022年,浏览器为什么不支持cjs

      • cjs是同步的,运行时的,node环境用cjs,node本身运行在服务器,无需等待网络握手,所以同步处理是很快的

      • 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差

    • 后续出来esm后,webpack为了兼容以前发在npm上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个iife的结构和代码注入,导致现在看webpack打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到

  2. rollup诞生于esm标准出来后,就是针对esm设计的,也没有历史包袱,所以可以做到真正的“打包”(精简,无额外注入)。

    开发者写esm代码 -> rollup通过入口,递归识别esm模块 -> (可以支持配置输出多种格式的模块,如esm、cjs、umd、amd)最终打包成一个或多个bundle.js。

rollup 在推出的当时(2015年)就是面向未来,面向 esm 的,坚决抛弃了 cjs,这是他的初心和理念。

但 rollup 不支持 hmr,加载其他类型的资源文件或者支持导入 CommonJS 模块,又或是编译 ES 新特性,这些额外的需求 Rollup 需要使用插件去完成。

所以rollup 比较适合打包js库,react、vue等的源代码库现在都是 rollup 打包的。

截屏2023-06-25 20.17.48.png

rollup 官网已经把 vite 写到了宣传栏里。

参考文章:rollup打包产物解析及原理(对比webpack)

esbuild

截屏2023-06-26 13.57.07.png

esbuild 最大的特点就是快,作为一个单纯的打包器,快的离谱。

为什么会这么快?官网也有解释

  1. 语言优势

    大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,但是在资源打包这种 CPU 密集场景下,Go 更具性能优势。

    虽然现代 JS 引擎与10年前相比有巨大的提升,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都需要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。

v2-bf83182170e5ba6f0f8c29eed344cf77_b.webp

  1. 多线程优势

    Go 天生具有多线程运行能力,而 JavaScript 本质上是一门单线程语言,到引入 WebWorker 规范之后才有可能在浏览器、Node 中实现多线程操作。

    Esbuild,它最核心的卖点就是性能,它的实现算法经过非常精心的设计,尽可能饱和地使用各个 CPU 核,特别是打包过程的解析、代码生成阶段已经实现完全并行处理。

v2-b3762e012f7bac9d1ed578921c592592_b.gif

v2-ba320e357a583103548a6d2aeb32da52_b.gif

  1. esbuild中的所有内容都是从头开始编写的。

    完全重写整套编译流程所需要用到的所有工具!这意味着它需要重写 js、ts、jsx、json 等资源文件的加载、解析、链接、代码生成逻辑。

    开发成本很高,风险很大,但收益也是巨大的,它可以一路贯彻原则,以性能为最高优先级定制编译的各个阶段,比如说:

    • 重写 ts 转译工具,完全抛弃 ts 类型检查,只做代码转换
    • 大多数打包工具把词法分析、语法分析、符号声明等步骤拆解为多个高内聚低耦合的处理单元,各个模块职责分明,可读性、可维护性较高。而 Esbuild 则坚持性能第一原则,不惜采用反直觉的设计模式,将多个处理算法混合在一起降低编译过程数据流转所带来的性能损耗
    • 一致的数据结构,以及衍生出的高效缓存策略
  2. 结构一致性

    Webpack 中使用 babel-loader 处理 JavaScript 代码时,可能需要经过多次数据转换:

    • Webpack 读入源码,此时为字符串形式
    • Babel 解析源码,转换为 AST 形式
    • Babel 将源码 AST 转换为低版本 AST
    • Babel 将低版本 AST generate 为低版本源码,字符串形式
    • Webpack 解析低版本源码
    • Webpack 将多个模块打包成最终产物

    源码需要经历 string => AST => AST => string => AST => string ,在字符串与 AST 之间反复横跳。

    而 Esbuild 重写大多数转译工具之后,能够在多个编译阶段共用相似的 AST 结构,尽可能减少字符串到 AST 的结构转换,提升内存使用效率。

在我看来,Esbuild 当下与未来都不能替代 Webpack,它不适合直接用于生产环境,而更适合作为一种偏底层的模块打包工具,需要在它的基础上二次封装。可喜的是,vite 在开发环境就使用 esbuild 进行文件编译。webpack 也有esbuild-loader使 esbuild 在 webpack 中使用。

SWC

logo.png

如果说 esbuild 是想取代 rollup 的话,那 swc 就是来革 babel 的命的。

SWC(代表 Speedy Web Compiler )是一个用 Rust 编写的超快速 TypeScript / JavaScript 编译器。它是一个同时支持 Rust 和 JavaScript 的库。

webpack与bable的性能瓶颈都在于JS语言,出现了go实现的esbuild与Rust实现的swc等工具,esbuild 想要开辟一个构建工具性能的新时代,创建一个易用的现代打包器,swc 的目标则是是替代babel,在官方文档的Comparison with babel这个单元,我们可以看到swc团队将相当一部分bable的插件重写了以增强swc的能力。

截屏2023-06-26 18.08.35.png

swc 也有更大的野心,还在开发中的 swpack 会是一个全新的构建工具。

swc 的开发者是个 97 年的韩国小伙子 kdy1dev

截屏2023-06-26 18.59.58.png

核心功能库

@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。

最新动态

2022 年 10 月 26 日,Vercel 公司正式宣布推出新的打包工具Turbopack,Turbopack用 swc 替代了 babel。相关链接

2022 年12 月,vite4 发布,也支持开发过程中使用 SWC 的新 React 插件。在 vite4 的发布公告中说:

SWC 现在是 Babel 的成熟替代品,特别是在 React 项目的背景下。SWC 的 React Fast Refresh 实现比 Babel 快很多,对于一些项目来说,它现在是一个更好的选择。从 Vite 4 开始,有两个插件可用于 React 项目,他们各自都有不同的取舍和权衡。我们认为目前这两种方法都值得支持,未来我们会继续探索对这两个插件的改进。

@vitejs/plugin-react

@vitejs/plugin-react 是一个使用 esbuild 和 Babel 的插件,能够以占用空间小的软件包和灵活使用 babel transform pipeline 实现快速的 HMR。

@vitejs/plugin-react-swc (新)

@vitejs/plugin-react-swc 是一个新的插件,在构建过程中使用 esbuild,但在开发过程中用 SWC 取代 Babel。对于不需要非标准 React 扩展的大项目,冷启动和 HMR 的速度会明显加快。

大而全的构建工具

Parcel:主打一个零配置

截屏2023-06-27 10.54.59.png

Parcel在js打包工具中属于相对后来者(根据npm版本上传显示最早上传于 2017.8,webpack是2013年左右,rollup是2015.5)。

特点和优势

  1. 零配置:Parcel是一个零配置工具,无需手动配置就可以进行打包。它能够根据项目的内容自动推断所需的配置,减少了项目配置的复杂性,使得构建过程更加简单和快速。
  2. 快速构建:Parcel采用了并行化的构建策略,能够同时处理多个文件,从而加快构建速度。它还利用了缓存机制,只重新构建发生更改的文件,提高了增量构建的效率。
  3. 内置支持多种文件类型:Parcel支持许多常见的文件类型,包括HTML、CSS、JavaScript、TypeScript、图片、字体等。你可以在项目中直接导入这些文件,Parcel会自动处理它们的依赖关系并进行正确的打包。
  4. 智能的资源管理:Parcel会自动分析项目中的依赖关系,并生成适当的资源映射关系。它可以优化资源的加载顺序和处理方式,以提高页面的加载性能。
  5. 开发服务器:Parcel提供了一个内置的开发服务器,能够在开发过程中实时预览和调试项目。它支持热模块替换(HMR)功能,使得在修改代码后可以立即查看更改的效果。
  6. 插件系统:虽然Parcel具有零配置的特性,但它也提供了插件系统,允许你根据需要扩展和定制构建过程。你可以使用插件来处理特定类型的文件、优化输出、添加自定义功能等。

最新进展

我看了2023 年 5 月 26 日 parcel V2.9.0 的更新日志,parcel 也赶上了构建工具 rust 化的热潮:

  1. 在 v2.9.0 的版本中,用 SWC 替换掉了之前默认的 Terser,使打包速度更快。SWC 处理JavaScript,JSX和TypeScript的转译。比之前快了 10-20 倍。
  2. Parcel 基于 rust 开发了自己的 css 转换和打包器,比之前的 javascipt 版本快了 100 倍!⚡️ Lightning CSS
  3. sourceMap 映射操作也自己开发了 rust 版本的库。Parcel's source-map library
  4. 同时依靠多核和缓存保证快速的开发服务器启动体验。
  5. Lazy dev builds。在开发过程中,Parcel可以推迟构建文件,直到它们在浏览器中被请求。这意味着你只需要等待你实际工作的页面被构建起来。如果你的项目有很多入口或代码分割点,这可以极大地减少开发服务器的启动时间。

parcel 在国内用的似乎比较少,相比 vite 的火热程度也差不少,但是仔细研究一下发现是个很全面的也很方便上手的构建工具。

snowpack

snowpack已经宣布不维护了,现在还提到它,是因为 vite 的文档中提到了。

snowpack 的作者Fred K.Schott在 2019 年写了一篇文章 A Future Without Webpack,这是 snowpack 的由来。

Snowpack 1.0 是针对一个简单任务而设计的:安装 npm 软件包以直接在浏览器中运行。它背后的理念是,JavaScript 包是在开发过程中唯一 需要 使用打包器(bundler)的东西;只要能去掉这个要求,不再需要打包器,我们就能加快所有人的 Web 开发速度。

但是 snowpack 有很多问题,不是一个开箱即用的可以用在生产环境中的工具。

Fred在宣布 snowpack 不维护之后,也推荐了 vite,并表示之后也会将自己的项目Astro从Snowpack迁移到Vite上。

Vite 跟 Snowpack 的关系

尤雨溪在知乎上回复过这个问题:

时常能看到有人说是 Snowpack 先搞出 no-bundle 开发的,甚至有人暗示 Vite 是抄了 Snowpack,这里也说说明白。

Snowpack 的前身叫 pika-web,1.x 改名叫 Snowpack。1.x 的 Snowpack 本质上只是一个封装过的 Rollup,核心是把 npm 依赖转换成 esm,目的是能够在原生 ESM 的场景下用 npm 的包。Snowpack 1.x 在服务器这端没有任何逻辑。 比如你要用 TS,你得自己起一个 tsc --watch 进程;你要用babel,你得自己不停地调用 babel-cli。

Vite 的前身是 github.com/vuejs/vue-d… - 这个概念从一开始就是在服务器端对原生 ESM 请求进行按需编译。 Vite 0.x 开始开发的时候是 2020 年 4 月(Commits · vitejs/vite),这个时候的开发目标已经是基于 ESM 实现 HMR 热更新。 同时期的 Snowpack 还在 1.7.x(Commits · snowpackjs/snowpack),不仅没有服务端的按需编译,也没有热更新。这个时候 Snowpack 2.0 还在开发中,其作者还在研究 ESM HMR 怎么搞的时候 Vite 已经发布可用的版本了,我们还探讨过通用的 API 标准(github.com/snowpackjs/…)。

所以这里明确一下,无论是 “基于原生 ESM 按需编译“ 还是 ”基于原生 ESM 实现 HMR“,都是 Vite 先提出并实现的。Vite 中确实有一块借鉴了 Snowpack 1.x,就是把依赖预打包从而让 cjs 的依赖也能在原生 ESM 下被使用。

顺道一提,Snowpack 团队现在新做的静态生成框架 Astro,本来基于 Snowpack,现在已经转投 Vite 了。

vite

vite 是什么?在我看来,vite 是现在最有希望推翻 webpack 统治的新的前端构建工具。

vite 的策略很聪明,它集合了很多目前成熟且先进的工具,从 ESM no-bundle 这个好主意出发,一步一步,稳扎稳打。

在开发环境端,支持 ES moudle 加载,把 commonJS 打包成为 esm,使只导出 commonJS 的 npm 包可以在浏览器里运行,例如 React。 同时,对于类似lodash-es这种导出 600 多个es文件的包,vite 会在检测到这种包的时候把它们打包在一起,这样可以避免众多的 http 请求阻塞浏览器,而这些操作的快速处理依赖于 esbuild 的强大性能。

在生产环境,用 rollup 打包,得益于 Rollup 优秀的插件接口设计和一部分 Vite 独有的额外选项。这意味着 Vite 用户可以利用 Rollup 插件的强大生态系统,同时根据需要也能够扩展开发服务器和 SSR 功能。

一个视频

本视频是 Vue 以及 Vite 作者 尤雨溪 在 2021 年 2 月 12 日在 Twitch 上做客 GitHub Open Source Friday 节目的直播视频。

介绍这个视频的文章

Turbopack

Turbopack 是 Webpack 的作者 Tobias Koppers 使用 Rust 语言开发一个前端模块化的工具,按作者构想 Turbopack 的目标是取代 Webpack。官方宣称 TurboPack 的速度比 Vite 快 10 倍,比 Webpack 快 700 倍。目前 Turbopack 仍然处于 AIpha 阶段,离正式运用到生产环境还有不少时间。

Turbopack 实现机制是由 Rust 的高效率和 Turbopack 主打的增量打包引擎共同实现的。 Rust 是系统级的编程语言,以高效著称,对标 c/c++,但是更安全,不容易发生内存泄露。 Turbopack 主打的增量打包引擎,可以把已经打包好的代码进行缓存,后面只打包新加入的代码,这个缓存级别可以达到单个函数级。

功能与特点

  1. 增量计算和函数级别的缓存

我们用一个简单的例子来说明增量计算和缓存,如下代码是一个页面的代码,代码包含了 Header 和 Footer 两个组件:

import Header from '../components/header'
import Footer from '../components/footer'
export default function Home() {
  return (
    <div>
      <Header />
      <h1>Home</h1> 
      <Footer />
    </div>
  )
}

当页面访问 /home 时 Header 和 Footer 会被标记为需要缓存 ,并被编译输出为 components_header.tsx.js 和 components_footer.tsx.js。而后更新 Footer 这个组件并保存,在 Webpack 中 Header 和 Footer 两个组件都会被重新编译,而仔细观察 Turbopack 输出的缓存文件,会发现只有 Footer 组件被重新编译,而 Header 组件则使用的是上一次的编译结果,如图所示 compoents_footer.tsx.js 的文件被刷新,而 header 则依旧是上一次的结果。

image.png

  1. 按需编译

本地开发时,Webpack 启动时要全量编译所有文件,这使得启动项目或者切换分支后需要花费大量的时间重新打包编译。而 Turbopack 则采用按需编译的方式,我们使用一个简单的例子来说明什么是按需编译,为什么 Turbopack 的启动速度如此之快,在项目的基础上添加一个 Login 的页面,如下所示:

import Header from '../components/header'
import Footer from '../components/footer'
export default function Login() {
  return (
    <div>
      <Header />
      <h1>Login</h1>
      <Footer />
    </div>
  )
}

启动项目,在不到 1s 时间后控制台提示已经启动成功,但是文件目录下却没有输出任何缓存文件,这是由于 Turbopack 的按需编译机制,所有组件在启动时都未被使用,所以没有任何的编译操作。在浏览器中访问项目的首页地址,此时观察输出缓存文件则发现 /home 页面及其依赖的组件才被编译:

图片

而我们添加的 Login 的页面并没有被编译和输出,我们再次访问 /login 页面,在看一次输出的缓存文件:

图片

在浏览器访问 /login 之后,该页面的以及所依赖的组件才会被编译,而这种按需编译的机制,减少程序的重复工作,提升开发人员的工作效率。此外还有一点,虽然 /home 和 /login 页面都依赖于 Footer 组件,当浏览器访问 /login 时只会更新当前页面的下的 Footer 组件,而 /home 页面下的 Footer 组件是不会被更新的。

  1. SWC 编译器

Turbopack 的速度如此之快有一个很大的原因是使用了 SWC 作为编译器。大部分 Webpack 的项目编译都是使用 Babel 编译和转换,由于 Babel 本身也是使用 Javascript 编写,转换效率并不理想,而 Turbopack 原生使用 SWC 作为编译器。SWC 是一款 Rust 编写的 Javascript 代码编译器,官方宣称其编译速度是 Babel 的 20 倍( Webpack 也可以使用SWC)。

  1. 本地持久化

根据作者的想法,未来编译结果不仅仅缓存在内存当中,还会本地持久化。本地持久化的意义是什么?在实际的生产环境中, 中大型的项目往往都需要打包 15 分钟甚至更久,编译结果持久化可以节省大量的打包时间。假设项目里有 50 个页面,本次迭代只修改了其中 10 个页面,Webpack 打包会全量重新打包 50 个页面,而 Turbopack 只需重新打包 10 个被修改的页面,未修改的 40 个页面直接从硬盘读取上一次打包结果,打包效率则得到非常大的提升。

目前的问题

  1. turbopack 还和 next 包在一起,作为 nextjs 的开发服务器,还不能单独使用,turbopack 文档也说还没有准备好为生产环境服务。
  2. Turbopack 非常灵活和可扩展,但没有计划与 webpack 实现 1:1 的兼容性。
  3. 目前很多功能还在开发中,与 vite 已经大面积生产应用相比,显然是落后了。

Rspack -- 基于 Rust 的高性能 Web 构建工具(字节出品)

文档地址

  1. 基于 rust,性能提升。
  2. 兼容 webpack 主要配置,适配了 webpack 的 loader 架构,可以无缝使用各种loader,如 babel-loader、less-loader、sass-loader、vue-loader 等等。

目前还是 0.2 版本,还很不成熟。

总结

看了上面如此多的打包器或者构建工具,可以看到前端构建领域是烽烟四起,都想挑战 webpack 的一统局面,而他们的路径也都差不多,有几条思路:

  1. 打包器抛弃 js 代码,用更底层的 go 和 rust 提升性能。
  2. 构建工具以 vite 为代表的利用 ESM 的浏览器支持做文章,提高开发体验。
  3. 以 parcel 和 turbopack 为代表把能用 rust 重写提高性能的步骤都用 rust 重写,辅以多线程并行和极致的缓存策略提高性能。

目前看起来 vite 是领先的,但其他的也在后面紧追不舍,而我们也不能只在旁边看着,是不是 rust 也应该学起来了!!