Bun

Bun 的新文字鎖定檔


Jarred Sumner · 2024 年 12 月 17 日

bun install 是一個快速且相容 npm 的套件管理器,可用於 Node.js 或 Bun。

團隊從 npm、pnpm 或 yarn 遷移到 bun install 後,最常分享的回饋意見是關於 Bun 的 bun.lockb 二進制鎖定檔格式。二進制鎖定檔在 Pull Request 中難以審閱。合併衝突變得更難解決。工具程式無法輕易讀取二進制鎖定檔。

為了幫助解決這個問題,我們之前增加了對 bun ./bun.lockb 的支援,以產生相容於 yarn.lock 的鎖定檔,但這還不夠。真實來源仍然是二進制鎖定檔。您必須在二進制鎖定檔上執行 bun 才能取得 yarn 的鎖定檔。這在 Github、工具程式或合併衝突方面效果不佳。

這就是為什麼在 Bun v1.1.39 中,我們推出了 bun.lock - 一種用於 bun install 的新型文字鎖定檔格式

bun install --save-text-lockfile

這個標記讓 Bun 儲存基於文字的 bun.lock 檔案,而不是儲存二進制的 bun.lockb 檔案。在 Bun v1.2 中,我們計劃將其設為預設。

bun.lock
{
  "lockfileVersion": 0,
  "workspaces": {
    "": {
      "dependencies": {
        "uWebSocket.js": "uNetworking/uWebSockets.js#v20.51.0",
      },
    },
  },
  "packages": {
    "uWebSocket.js": ["uWebSockets.js@github:uNetworking/uWebSockets.js#6609a88", {}, "uNetworking-uWebSockets.js-6609a88"],
  }
}

如果您第一次執行 bun install --save-text-lockfile 時,存在 bun.lockb 檔案或 package-lock.json 檔案,bun 將使用現有的鎖定檔來產生 bun.lock 檔案,並保留解析結果和元數據。

快取 bun install 速度提升 30%

有些專案一開始比其他替代方案更快,但隨著它們添加遺失的功能和修復錯誤,速度會變慢。 Bun 不是 那些專案之一。我們不接受效能倒退。

在 Bun v1.1.39 中,相較於 Bun v1.1.38 中使用二進制鎖定檔的快取 bun install,我們使用文字鎖定檔的快取 bun install 速度提升了 30%。

cached-no-op-install
no-node-modules-install
package.json
cached-no-op-install
# --warmup=10
Benchmark 1: bun install --cwd=./with-text # Text-based lockfile
  Time (mean ± σ):      45.8 ms ±   2.2 ms    [User: 17.4 ms, System: 34.7 ms]
  Range (min … max):    43.8 ms …  55.1 ms    60 runs

Benchmark 2: bun-1.1.38 install --cwd=./with-binary # Binary lockfile
  Time (mean ± σ):      60.4 ms ±   2.1 ms    [User: 14.8 ms, System: 52.1 ms]
  Range (min … max):    58.3 ms …  69.9 ms    44 runs

Benchmark 3: cd with-pnpm && pnpm install
  Time (mean ± σ):     709.5 ms ±   3.7 ms    [User: 914.5 ms, System: 318.7 ms]
  Range (min … max):   705.3 ms … 716.1 ms    10 runs

Benchmark 4: cd with-yarn && yarn install
  Time (mean ± σ):     243.1 ms ±   3.0 ms    [User: 415.9 ms, System: 24.2 ms]
  Range (min … max):   240.6 ms … 248.4 ms    12 runs

Benchmark 5: cd with-npm && npm install
  Time (mean ± σ):      1.525 s ±  0.174 s    [User: 1.459 s, System: 0.119 s]
  Range (min … max):    1.275 s …  1.709 s    10 runs

Summary
  bun install --cwd=./with-text # Text-based lockfile ran
    1.32 ± 0.08 times faster than bun-1.1.38 install --cwd=./with-binary # Binary lockfile
    5.31 ± 0.27 times faster than cd with-yarn && yarn install
   15.49 ± 0.76 times faster than cd with-pnpm && pnpm install
   33.28 ± 4.13 times faster than cd with-npm && npm install
no-node-modules-install
# --warmup=2 --prepare="rm -rf ./with-{text,binary,pnpm,yarn,npm}/node_modules"
Benchmark 1: bun install --cwd=./with-text --ignore-scripts # Text-based lockfile
  Time (mean ± σ):      1.590 s ±  0.029 s    [User: 0.018 s, System: 0.809 s]
  Range (min … max):    1.546 s …  1.651 s    10 runs

Benchmark 2: bun-1.1.38 install --cwd=./with-binary --ignore-scripts # Binary lockfile
  Time (mean ± σ):      1.749 s ±  0.024 s    [User: 0.015 s, System: 0.882 s]
  Range (min … max):    1.719 s …  1.788 s    10 runs

Benchmark 3: cd with-pnpm && pnpm install --ignore-scripts
  Time (mean ± σ):     11.303 s ±  0.142 s    [User: 4.093 s, System: 107.544 s]
  Range (min … max):   10.926 s … 11.442 s    10 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs.

Benchmark 4: cd with-yarn && yarn install --ignore-scripts
  Time (mean ± σ):      6.372 s ±  0.104 s    [User: 5.980 s, System: 17.191 s]
  Range (min … max):    6.286 s …  6.603 s    10 runs

Benchmark 5: cd with-npm && npm install --ignore-scripts
  Time (mean ± σ):      8.309 s ±  0.081 s    [User: 8.598 s, System: 9.838 s]
  Range (min … max):    8.194 s …  8.418 s    10 runs

Summary
  bun install --cwd=./with-text --ignore-scripts # Text-based lockfile ran
    1.10 ± 0.02 times faster than bun-1.1.38 install --cwd=./with-binary --ignore-scripts # Binary lockfile
    4.01 ± 0.10 times faster than cd with-yarn && yarn install --ignore-scripts
    5.23 ± 0.11 times faster than cd with-npm && npm install --ignore-scripts
    7.11 ± 0.16 times faster than cd with-pnpm && pnpm install --ignore-scripts
package.json
{
  "name": "desktop",
  "type": "module",
  "module": "index.ts",
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5.6.2"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.32.1",
    "@babel/core": "^7.26.0",
    "@octokit/rest": "^21.0.2",
    "@sentry/bun": "^8.37.1",
    "date-fns": "^4.1.0",
    "debug": "^4.3.7",
    "express": "^4.21.1",
    "gatsby": "^5.14.0",
    "ink": "^5.0.1",
    "isbot": "^5.1.17",
    "next": "^15.1.0",
    "postgres": "^3.4.5",
    "puppeteer": "^23.10.4",
    "ts552": "npm:typescript@5.5.2",
    "ts562": "npm:typescript@5.6.2",
    "vite": "^5.4.9"
  }
}

是什麼讓 bun install 如此快速?

bun install 速度很快,因為我們非常努力使其快速。沒有像二進制鎖定檔格式這樣的「單一因素」使其快速。

Structure of Arrays

我們做了很多工作來避免 O(N^3) 記憶體分配。當您有許多依賴性和巢狀物件/結構要序列化(例如套件、它們的依賴項、它們的依賴項的依賴項和解析結果)時,您如何避免單獨分配每個物件/結構?您可以使用線性可序列化陣列的索引而不是指標/物件。

在 TypeScript 中,在套件管理器中儲存套件的緩慢但相對常見的方法看起來像這樣

slow.ts
interface SlowPackage {
  name: string;
  version: string;
  dependencies: Record<string, Dependency>;

  /// ... more fields ...
}

interface Workspace {
  packages: Record<string, Package>;
  root: Package;
}

快速(且過度簡化)的版本看起來像這樣

fast.ts
interface Package {
  /** Index into strings array */
  name: number;
  /** Index into strings array */
  version: number;
  /** Index into dependencies array */
  dependenciesStart: number;
  /** Length of dependencies array */
  dependenciesCount: number;
  /** Start offset into resolutions array */
  resolutionsStart: number;
  /** Length of resolutions array */
  resolutionsCount: number;
}

interface Workspace {
  packages: Package[];
  dependencies: Dependency[];
  resolutions: number[];
  strings: string[];
}

我們不是為陣列內部的每個元素使用陣列,而是為每種類型使用一個大陣列並將其附加到其中。這通常稱為 陣列結構

Small string optimizations

當您有許多通常很小的字串(例如套件名稱或版本)時,您可以將小字串儲存在用於引用它們的相同空間中,而不是單獨分配每個字串。在 JavaScript 等高階語言中,字串對您來說是抽象的,但在 Zig、C++ 或 Rust 中,「小字串優化」眾所周知 的。

在 Zig 中,我們的 `semver.String` 結構針對小字串進行了優化

pub const String = extern struct {
    pub const max_inline_len: usize = 8;
    /// This is three different types of string.
    /// 1. Empty string. If it's all zeroes, then it's an empty string.
    /// 2. If the final bit is set, then it's a string that is stored inline.
    /// 3. If the final bit is not set, then it's a string that is stored in an external buffer.
    bytes: [max_inline_len]u8 = [8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
};

Careful I/O

我們密切關注使用的系統呼叫。除非需要,否則我們避免開啟目錄和讀取檔案。我們使用非常特定,有時是不常見的平台特定系統呼叫,例如 `clonefile`、`sendfile`、`faccessat`、`memfd_create` 等,以避免不必要的工作。

我可以滔滔不絕地講述我們在 bun install 中所做的所有優化,但您明白了重點。這從來都不是二進制鎖定檔格式的問題。我們只是非常努力使其快速,並且所有這些工作也適用於文字鎖定檔格式。

非破壞性變更

我們計劃在 Bun v1.2.0 中將 `bun.lock` 設定為預設。同時,我們將繼續支援二進制 `bun.lockb` 格式,並且會持續一段時間。

在 Bun v1.2 之前,需要使用 bun install --save-text-lockfile 標記來產生文字鎖定檔。當 `bun.lock` 檔案存在時,`bun install` 將使用文字鎖定檔並忽略二進制鎖定檔。否則,它將產生二進制鎖定檔。

工具程式相容性

bun.lock 檔案是 JSONC 格式(類似於 tsconfig.json)

Visual Studio Code

VSCode 將為您突出顯示 bun.lock 檔案的語法,感謝 @remcohaszing

GitHub & git

GitHub 在差異中呈現 bun.lock,這在審閱程式碼時很重要。

GitHub 顯示基於文字的 bun.lock 檔案

先前,GitHub 不會呈現二進制 bun.lockb 檔案。

GitHub 顯示二進制 bun.lockb 檔案

Dependabot

在撰寫本文時,Dependabot #1 最受歡迎的功能請求 是支援 bun。基於文字的鎖定檔使 Dependabot 團隊更容易添加支援。

Dependabot 最受歡迎的功能請求