Bun:不仅是新的JavaScript运行时,并且重塑了JavaScript工具链

2023-11-09 18:56:01 浏览数 (3)

从2022年 Bun 的 Beta 版本发布,就早已爆火,仅一个月内,就在 GitHub 上获得了超过两万颗 Star,成为年度最火的前端项目。在今年的 9 月 8 号,由 Jarred Sumner 开发的 Bun v1.0 正式发布。本文会重点介绍 Bun 的特性和性能的测试,并且分析 Bun 为什么这么快。

Bun 为什么会出现?

JavaScript 成熟、发展迅速,并且有着充满活力和激情的开发者社区。然而,自14年前Node.js发布以来,JavaScript 的工具链变得越来越庞大和复杂。这是因为在发展过程中,各种工具被逐渐添加进来,但没有一个统一的集中规划,导致工具链缺乏整体性和效率,变得运行缓慢和复杂。

Bun 就是为了解决这一问题,Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试 JavaScript 和 TypeScript 代码。

Bun的目标很简单,就是要消除 JavaScript 工具链的缓慢和复杂性,但同时保留 JavaScript 本身的优点。Bun 希望让开发者继续使用喜欢的库和框架,并且无需放弃已经熟悉的规范和约定。为了实现这个目标,可能需要放弃一些在使用 Bun 之后变得不再必要的工具:Node.js:Bun 的一个可以直接替代的工具,因此不再需要以下工具:

  • nodenpx:Bun 的 bunx 命令比 npx 快5倍。
  • nodemon:Bun 内置了监听模式,无需使用 nodemon。
  • dotenv、cross-env:Bun 默认支持读取.env文件的配置
  • vite、webpack Bun 自带构建功能
  • ts-node、tsx Bun可以直接运行 TypeScript 和 tsx 文件
  • jest Bun是一个支持Jest的测试运行器,具有快照测试、模拟和代码覆盖率等功能,因此不再需要以下测试相关的工具

对比 Deno

在讨论 JavaScript 运行时的演变时,很难忽略 Deno。Node.js 的创建者 Ryan Dahl 将 Deno 作为一种新的运行时推出,旨在解决他在 Node.js 中发现的一些设计缺陷和遗留问题。

Deno 是 JavaScript 和 TypeScript 的安全运行时。它直接解决了 Node.js 的许多缺点。例如,Deno 原生支持 TypeScript,无需外部工具。与脚本默认具有广泛权限的 Node.js 不同,Deno 采用了安全优先的方法,要求开发人员为文件系统访问或网络连接等潜在敏感操作明确授予权限。

虽然 Deno 为 Node.js 提供了一个令人信服的替代方案,但它还没有达到 Node.js 被广泛采用的程度。因此,本文将重点对比 Bun 和成熟的 Node.js。

JavaScript 运行时

JavaScript 运行时是执行JavaScript代码所需的环境。它包括了解析和执行JavaScript代码的引擎,以及提供核心对象和功能的库,例如处理事件、定时器和HTTP请求等。运行时还包括调用堆栈、堆(用于分配内存)、和垃圾收集机制。

Node.js 和 Bun 都是运行时。Node.js 主要用 C 编写,而 Bun 则用一种名为 Zig 的低级通用编程语言编写,Zig 也在 Bun 发布之后2周涨了 1K 星星,可以说 Bun 也在给 Zig 代言。

Bun 为什么那么快

  • http server 使用了 uWebSockets ;
  • bun install 使用了比较好的 system call,例如 linux 上用的是 io_uring ,mac 上是 Jarred 在犄角旮旯找的接口;
  • 用 C / Zig 来实现 node 中用 js 拼接起来的重要内置库,其中复用了很多 WebCore 的代码;
  • 使用 JavaScriptCore。

uWebSockets

uWebSockets是一个高效的网络库,它被设计为提供低层级和高性能的网络通信功能。

低层级性:

uWebSockets 被描述为一个非常低层级的库,它去掉了许多高层级库(如 Express)提供的额外功能和抽象,以实现更高的性能

优化的实现:

uWebSockets 的实现被彻底优化,以提供对 HTTP 和 WebSockets 的高效处理。这种优化使得 uWebSockets 能够在最具挑战性的应用中提供简单、安全和标准兼容的网络。

直接内存访问和管理:

低层级库通常可以更直接地访问和管理内存,从而避免了额外的内存分配和垃圾收集开销,这可能是 uWebSockets 比其他高层级库更快的另一个原因。

事件驱动和异步处理:

uWebSockets 可能利用了事件驱动和异步处理技术来高效地处理大量并发连接,而不会产生很多阻塞或线程上下文切换的开销。

协议优化:

通过优化网络协议的实现,例如减少不必要的数据复制和缓冲,uWebSockets 可能实现了更低的延迟和更高的吞吐量。

JavaScriptCore

Node.js 使用谷歌为 Chrome 浏览器提供支持的 V8 引擎,而 Bun 则使用 JavaScriptCore (JSC),这是苹果公司为 Safari 开发的开源 JavaScript 引擎。

V8 和 JSC 有着不同的架构和优化策略。JSC 优先考虑的是更快的启动时间和更少的内存使用,执行时间稍慢。另一方面,V8 优先考虑快速执行,同时进行更多运行时优化,这可能会导致更多内存使用。这使得 Bun 的速度很快,启动速度比 Node.js 快达 4 倍。

输出 hello, world 测试

版本:

  • node: v18.18.2
  • bun: 1.0.6
  • deno: 1.37.2

hello.js

代码语言:javascript复制
console.log('hello, world');
代码语言:shell复制
hyperfine 'node hello.js' 'bun hello.js' 'deno run hello.js' --warmup 100 --runs 1000 
Benchmark 1: node hello.js
  Time (mean ± σ):      35.8 ms ±   4.1 ms    [User: 27.7 ms, System: 8.0 ms]
  Range (min … max):    30.1 ms …  57.7 ms    1000 runs
 
Benchmark 2: bun hello.js
  Time (mean ± σ):      11.1 ms ±   2.0 ms    [User: 5.6 ms, System: 6.8 ms]
  Range (min … max):     8.8 ms …  27.3 ms    1000 runs
 
Benchmark 3: deno run hello.js
  Time (mean ± σ):      29.3 ms ±   4.1 ms    [User: 19.2 ms, System: 10.9 ms]
  Range (min … max):    24.4 ms …  47.4 ms    1000 runs
 
Summary
  'bun hello.js' ran
    2.64 ± 0.60 times faster than 'deno run hello.js'
    3.23 ± 0.69 times faster than 'node hello.js'

100个数字快排测试

代码语言:shell复制
hyperfine 'node quickSort.js' 'bun quickSort.js' 'deno run quickSort.js' --warmup 100 --runs 1000

Summary
  'bun quickSort.js' ran
    2.28 ± 0.43 times faster than 'deno run quickSort.js'
    2.70 ± 0.50 times faster than 'node quickSort.js'

可以看出,Bun 的执行速度会比 Deno、nodejs 快2-3倍左右。

转换器

虽然 Node.js 是 JavaScript 的强大运行时,但它并不原生支持 TypeScript 文件。要在 Node.js 环境中执行 TypeScript,需要外部依赖。一种常见的方法是使用构建步骤将 TypeScript 转换为 JavaScript,然后运行生成的 JS 代码。

相比之下,Bun 提供了一种更精简的方法。它的运行时集成了 JavaScript 转换器。这样,你就可以直接运行 .js、.ts、.jsx 和 .tsx 文件。Bun 内置的转换器能将这些文件无缝转换为 JavaScript,无需额外步骤即可立即执行。

代码语言:shell复制
bun index.ts

在运行 TypeScript 文件时,速度上的差异会被放大,因为 Node.js 在运行前需要一个转译步骤。

bun_node_tsbun_node_ts

ESM 和 CommonJS 兼容

在 JavaScript 中,两个主要的模块系统是 CommonJS 和 ES 模块(ESM)。CommonJS 源自 Node.js,使用 require 和 module.exports 进行同步模块处理,ES6 中引入的 ESM 使用 import 和 export 语句,提供了一种更静态和异步的方法,并针对浏览器和现代构建工具进行了优化。

不少人从 CommonJS 到 ES 模块的过渡踩了很多坑。在引入 ESM 之后,Node.js 花了 5 年时间才在没有 --experimental-modules 标志的情况下支持它。无论如何,生态系统中仍然充满了 CommonJS 的包。

Bun 始终支持两种模块系统。无需担心文件扩展名、.js vs .cjs vs .mjs,也无需在 package.js 中包含 "type"(类型)或 "module"(模块): "模块"。你甚至可以在同一个文件中使用 import 和 require()。

代码语言:javascript复制
import lodash from "lodash";
const _ = require("underscore");

Web APIs

网络请求的 API 是基于浏览器的 web 应用不可或缺的一部分,为网络交互提供了 fetch 和 WebSocket 等工具。虽然这些已成为浏览器标准,但 Node.js 等服务器端环境对它们的支持却不一致。

在 Node.js 的早期版本中,浏览器中常见的网络标准 API 并不支持本机。开发人员不得不依赖 node-fetch 等第三方软件包来实现这些功能。不过,从 Node.js v18 开始,对 fetch API 的实验性支持可以替代这些第三方包了,对于同构的应用来说非常利好。

而 Bun 为这些 Web 标准 API 提供内置支持。开发人员可以直接使用稳定的 fetch、Request、Response、WebSocket 和其他类似浏览器的 API,而无需额外的软件包。此外,Bun 对这些 Web API 的原生实现确保了它们比第三方替代品更快、更可靠。

热重载

在 Node.js 生态系统中,有几种实现热重载的方法。其中一个流行的工具是 nodemon,它可以硬重启整个过程,另外,从 Node.js v18 开始,还引入了一个试验性的 --watch 标志:

代码语言:shell复制
node --watch index.js

这两种方法的目的都是在代码发生变化时实时重新加载应用程序。不过,它们可能会有不同的行为,尤其是在某些环境或场景中。

例如,nodemon 会导致中断 HTTP 和 WebSocket 连接,而 --watch 作为实验性标记,可能无法提供全套功能,在 GitHub 上有很多相关的 issure 也没解决。

Bun 在热重载方面更进一步。使用 --hot 标志运行 Bun,就能启用热重载:

代码语言:shell复制
bun --hot index.ts

与可能需要重启整个进程的 Node.js 方法不同,Bun 会在不终止旧进程的情况下就地重新加载代码。这可确保 HTTP 和 WebSocket 连接不中断,并保留应用程序状态,从而提供更流畅的开发体验。

与 nodejs 的兼容

在过渡到新的运行时或环境时,兼容性往往是开发人员最关心的问题。Bun 将自己定位为 Node.js 的直接替代品,从而解决了这个问题。这意味着现有的 Node.js 应用程序和 npm 软件包无需任何修改即可与 Bun 无缝集成。确保这种兼容性的主要功能包括:

  • 支持内置 Node.js 模块,如 fs、path 和 net。
  • 识别 __dirname 和 process 等全局变量。
  • 遵循 Node.js 模块解析算法,包括熟悉的 node_modules 结构。

Bun 仍在不断发展。它专为增强开发工作流程而生,非常适合资源有限的环境,例如 serverless。Bun 背后的团队正在努力实现与 Node.js 的全面兼容以及与主流框架的更好集成。

Bun APIs

Bun 在确保与 Node.js 兼容的同时,并没有止步于此。Bun 为开发人员最需要的东西提供了高度优化的标准库 API。与为了向后兼容而存在的 Node.js API 不同,这些 Bun 原生 API 的设计目标是快速、易用。

需要注意的是,Bun 的文件读取依赖 io_uring,在有些低版本 linux 内核中,可能无法使用,在 https://github.com/ZJONSSON/node-unzipper/issues/104 这里可以看到现在还没有完全解决,我在云开发机就遇到了这个错误:

代码语言:shell复制
EBADF: Bad file descriptor
   path: "test.txt"
 syscall: "open"
   errno: -9

使用 Bun.file(),可以在特定路径下轻松加载文件:

代码语言:javascript复制
// Bun (index.ts)
const file = Bun.file("test.txt");
const contents = await file.text();

// Node.js
const fsPromises = require('fs').promises;

async function readFile(filePath) {
  const data = await fsPromises.readFile(filePath, 'utf8');
}

readFile('test.txt');

在 mac M1 上测试,Bun 的文件读取比 nodejs 快3倍左右

代码语言:shell复制
hyperfine 'node node_file.js' 'bun bun_file.js' --warmup 100 --runs 1000
Benchmark 1: node node_file.js
  Time (mean ± σ):      29.5 ms ±   1.6 ms    [User: 23.2 ms, System: 5.1 ms]
  Range (min … max):    27.3 ms …  42.4 ms    1000 runs

Benchmark 2: bun bun_file.js
  Time (mean ± σ):       9.7 ms ±   1.1 ms    [User: 5.3 ms, System: 4.4 ms]
  Range (min … max):     8.0 ms …  18.5 ms    1000 runs

Summary
  bun bun_file.js ran
    3.05 ± 0.38 times faster than node node_file.js

包管理器,对比 npm 和 pnpm

Bun 不仅仅是一个运行时;它还是一个高级工具包,其中包括一个功能强大的软件包管理器。如果你曾在依赖安装过程中焦躁不安,那么 Bun 可以缓解你的焦虑。即使你不把 Bun 用作运行时,它内置的软件包管理器也能加快你的开发工作流程。

Bun 的使用方式和 npm 几乎一致,但是安装速度比 npm 快了好几个数量级。它通过利用全局模块缓存,消除了从 npm 注册表的冗余下载,从而实现了这一目标。此外,Bun 还采用了适用于各操作系统的最快系统调用,以确保最佳性能。

使用 vite 创建一个空的vue项目模板:npm create vite@latest ,然后再安装一些常用的依赖:

代码语言:json复制
{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.5.1",
    "core-js": "^3.33.0",
    "lodash": "^4.17.21",
    "vue": "^3.3.4",
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5",
    "vue-tsc": "^1.8.5"
  }
}

在每次测试前,删除 node_modules,保证公平,测试结果如下:

代码语言:shell复制
hyperfine --warmup 5 --runs 20 --prepare 'rm -rf node_modules' 'npm install' 'bun install' 'pnpm install'
Benchmark 1: npm install
  Time (mean ± σ):      3.539 s ±  0.121 s    [User: 4.246 s, System: 0.780 s]
  Range (min … max):    3.294 s …  3.734 s    20 runs
 
Benchmark 2: bun install
  Time (mean ± σ):      46.5 ms ±   0.6 ms    [User: 9.2 ms, System: 42.2 ms]
  Range (min … max):    45.3 ms …  47.2 ms    20 runs
 
Benchmark 3: pnpm install
  Time (mean ± σ):      1.437 s ±  0.074 s    [User: 1.500 s, System: 0.241 s]
  Range (min … max):    1.300 s …  1.577 s    20 runs
 
Summary
  'bun install' ran
   30.88 ± 1.63 times faster than 'pnpm install'
   76.06 ± 2.78 times faster than 'npm install'

bun CLI 包含一个与 Node.js 兼容的软件包管理器,旨在以更快的速度取代 npm、yarn 和 pnpm。

此外,bun run <command> 仅需 7 毫秒,而 npm run <command> 则需要 176 毫秒。多年来,Node.js 的 npm 一直是 JavaScript 软件包管理的标准,从测试结果来看,Bun 确实是速度比 npm 快很多倍,看起来官方的文档也没有骗人。

构建

在 Node.js 生态系统中,构建通常由第三方工具而非 Node.js 本身来处理。Node.js 中最流行的构建工具包括 Webpack、Rollup 和 Vite,它们提供了代码拆分、Tree shaking 和热模块替换等功能。

另一方面,Bun 本身也是一个构建程序。它旨在为各种平台构建 JavaScript 和 TypeScript 代码,包括浏览器中的前端应用程序(React 或 Next.js 应用程序)和 Node.js。

要构建 Bun,只需使用一个简单的命令即可:

代码语言:shell复制
bun build ./index.ts --outdir ./build

该命令构建 index.ts 文件,并在 ./build 目录中输出结果。构建过程快得惊人,Bun 的速度是 esbuild 的 1.75 倍,大大超过 Parcel、tsup 和 Webpack 等其他构建程序。

bun 构建速度比 tsup 都要快 20 多倍。

代码语言:shell复制
 hyperfine --warmup 10 --runs 100 --prepare 'rm -rf dist' 'npx tsup src/index.ts --outDir dist' 'bun build src/index.ts --outdir ./dist'
 
Summary
  'bun build src/index.ts --outdir ./dist' ran
   21.40 ± 2.36 times faster than 'npx tsup src/index.ts --outDir dist'

Bun 的另外一个好用的特性是引入了 JavaScript 宏。这些宏允许在构建过程中执行 JavaScript 函数,并将结果直接内联到最终构建包中。

看这个例子,在构建过程中,Bun 的 JavaScript 宏被用来获取用户名。宏不是在运行时调用 API,而是在构建时获取数据,并将结果直接内联到最终的输出中:

代码语言:javascript复制
// users.ts
export async function getUsername() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await response.json();
  return user.name;
}

// index.ts
import { getUsername } from "./users.ts" with { type: "macro" };
const username = await getUsername();

// build/index.js
var user = await "Leanne Graham";
console.log(user);

在一些流行的开源库中,可以看到有些也已经替换到 Bun 了,比如 lodash 。

不支持类型声明

需要注意的是,Bun 目前还不支持生成 dts 文件,还是需要tsc之类的工具:

代码语言:json复制
"scripts": {
  "build": "bun build ... && bun run build:declaration",
  "build:declaration": "tsc --emitDeclarationOnly --project tsconfig.types.json"
}

参考 https://github.com/oven-sh/bun/issues/5141

插件

就目前的 awesome-bun 列表来看,构建所用的插件几乎没有,如果要从旧的项目迁移过来,不少插件需要自己重新实现。

但估计不久之后,会有越来越多人参与插件的贡献了,从 pr 可以看到,还是有不少没有归纳的:

bun-plugins-prbun-plugins-pr

单元测试

除了运行时、软件包管理器和构建程序之外,Bun 还是一个测试运行器。

传统上,Node.js 开发人员一直依赖 Jest 或者 Vitest 来进行单元测试,而 Bun 则引入了一个内置测试运行器,保证了速度、兼容性和一系列满足现代开发工作流的功能。

Bun 的测试运行器 bun:test 设计为与 Jest 完全兼容,确保了熟悉 Jest 的开发人员可以轻松过渡到 Bun。

代码语言:javascript复制
import { test, expect } from "bun:test";

test("2   2", () => {
  expect(2   2).toBe(4);
});

使用 bun test 命令可以直接执行测试。此外,Bun 的运行时支持 TypeScript 和 JSX,无需额外的配置或插件。

Bun 对兼容性的保证还体现在对 Jest 全局导入的支持上。例如,从 @jest/globals 或 vitest 导入的内容将在内部重新映射到 bun:test。这意味着现有的测试套件无需修改代码即可在 Bun 上运行。

性能测试

Bun 的测试运行器不仅注重兼容性,还注重速度。在针对 Zod 测试套件的基准测试中,Bun 的速度比 Jest 快 13 倍,比 Vitest 快 8 倍。Bun 的匹配器以快速的本地代码实现,进一步凸显了速度优势。例如,Bun 中的 expect().toEqual() 比 Jest 快 100 倍,比 Vitest 快 10 倍。

无论是迁移现有测试还是启动新项目,Bun 都能提供符合现代开发需求的强大测试环境。

总结

Bun 因其令人惊讶的性能和易用性而迅速流行起来。Bun 还拥有许多 Node.js 没有的功能,例如内置ts转换器和测试运行器。

虽然 Bun 还处于早期阶段,但它所引发的热议已是不争的事实。目前,它已针对 MacOS 和 Linux 进行了优化,对 Windows 的支持也在进行中,有些功能还在开发中,也还有不少的 bug,1000 的bug单还没关闭。但作者单人输出能力很强,从 2021 年 4 月初始提交至今已有超过百万行的代码增删量,单人完成了 98% 以上的提交量,一周工作 90 小时,名副其实的卷王。

总而言之,如果想在真实的项目中使用,Bun 更适合作为 npm 和 jest 的平替。作为构建工具来说,生态还不是太成熟,迁移成本可能比较高。服务端渲染的话,需要先调研一下运行的环境,是否能完美支持。它更适合在一些相对来说更小型的实验性项目。

毫无疑问 Bun 给 JavaScript 生态带来了丰富的良性竞争,它应该也会在未来有一席之地,值得持续关注。

原文参考

  • https://dev.to/mattkrick/replacing-express-with-uwebsockets-48ph#:~:text=So, after 4 years, we,the paint because, well, speed
  • https://stackshare.io/stackups/uwebsockets-vs-ws#:~:text=ws vs uWebSockets: What are,implementation of HTTP and WebSockets
  • https://medium.com/@kristiyan.velkov/bun-vs-node-js-everything-you-need-to-know-7bc36d14f94d
  • https://bun.sh/blog/bun-v1.0

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞