執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

位元組碼快取

使用 Bun 打包器中的位元組碼快取加速 JavaScript 執行

位元組碼快取是一種建置時最佳化,通過預先將 JavaScript 編譯為位元組碼來縮短啟動時間。例如,啟用位元組碼後編譯 TypeScript 的 tsc,啟動時間可提升 2 倍。

使用方法

基本用法(CommonJS)

使用 --bytecode 標誌啟用位元組碼快取。不使用 --format 時,輸出格式預設為 CommonJS:

terminal
bun build ./index.ts --target=bun --bytecode --outdir=./dist

建置會寫入兩個檔案:

  • dist/index.js - 打包後的 JavaScript(CommonJS)
  • dist/index.js.jsc - 位元組碼快取檔案

執行環境,Bun 會自動檢測並使用 .jsc 檔案:

terminal
bun ./dist/index.js  # 自動使用 index.js.jsc

生成獨立執行檔時

使用 --compile 建立執行檔時,Bun 會將位元組碼嵌入二進位制檔案中。ESM 和 CommonJS 均可使用:

terminal
# ESM(需要 --compile)
bun build ./cli.ts --compile --bytecode --format=esm --outfile=mycli

# CommonJS(使用或不使用 --compile 均可)
bun build ./cli.ts --compile --bytecode --outfile=mycli

生成的執行檔同時包含程式碼和位元組碼。

ESM 位元組碼

ESM 位元組碼需要使用 --compile,因為 Bun 會將模組後設資料(匯入/匯出資訊)嵌入編譯後的二進位制檔案中。藉助這些後設資料,JavaScript 引擎可以在執行環境完全跳過解析。

如果不使用 --compile,ESM 位元組碼仍需要解析原始碼以分析模組相依,這會違背位元組碼快取的目的。

限制位元組碼深度

預設情況下,Bun 會將打包中的每個函式編譯為位元組碼,包括巢狀在其他函式中的函式。使用 --bytecode-depth(非負整數)只編譯最外層的 N 層巢狀。超出此限制的函式會在首次呼叫時從原始碼編譯。

terminal
# 僅編譯每個模組的頂層程式碼,不編譯巢狀函式
bun build ./index.ts --target=bun --bytecode --bytecode-depth=0 --outdir=./dist

# 編譯頂層程式碼及其直接宣告的函式
bun build ./index.ts --target=bun --bytecode --bytecode-depth=1 --outdir=./dist

深度越小,.jsc 檔案越小。代價是深層巢狀函式會在執行環境解析,而不是提前解析。Bun.build 也提供相同的選項 bytecodeDepth。

與其他最佳化結合使用

將位元組碼與壓縮和源對映結合使用:

terminal
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli
  • --minify 在生成位元組碼前壓縮程式碼體積(程式碼更少,位元組碼更小)
  • --sourcemap 保持錯誤定位(錯誤仍指向原始原始碼)
  • --bytecode 消除解析開銷。

效能影響

效能提升與程式碼體量成正比:

應用大小典型啟動速度提升
小型 CLI(< 100 KB)快 1.5-2 倍
中大型應用(> 5 MB)快 2-4 倍

體量越大,受益越明顯,因為解析的程式碼更多。

何時使用位元組碼

非常適合:

CLI 工具

  • 經常呼叫(如 linter、格式化工具、git 鉤子)
  • 啟動時間即全部使用者體驗
  • 使用者能明顯感受到 90ms 與 45ms 啟動時間的差異
  • 例子:TypeScript 編譯器、Prettier、ESLint

建置工具和任務執行器

  • 在開發過程中執行數百次甚至數千次
  • 每次節省數毫秒,累計提升明顯
  • 改善開發者體驗
  • 例子:建置指令碼、測試執行器、程式碼生成器

獨立執行檔

  • 釋出給重視效能的使用者
  • 單檔案分發方便
  • 檔案體積不如啟動速度重要
  • 例子:通過 npm 或二進位制釋出的 CLI

不適合:

  • ❌ 小型指令碼
  • ❌ 只執行一次的程式碼
  • ❌ 開發建置
  • ❌ 受大小限制的環境

限制

僅支援 CommonJS

位元組碼快取目前僅支援 CommonJS 輸出格式。Bun 打包器會自動將大部分 ESM 程式碼轉換為 CommonJS,但 頂層 await 是例外:

JavaScript
// 會阻止位元組碼快取
const data = await fetch("https://api.example.com");
export default data;

原因:頂層 await 需要非同步模組評估,這無法用 CommonJS 表示。模組圖非同步化,CommonJS 包裝函式模型失效。

解決方案:將非同步初始化放入函式:

JavaScript
async function init() {
  const data = await fetch("https://api.example.com");
  return data;
}

export default init;

現在模組匯出的是函式,由呼叫方根據需要使用 await。

版本相容性

位元組碼不跨 Bun 版本通用。位元組碼格式綁定於 JavaScriptCore 內部表示,不同版本會改變。

更新 Bun 後必須重新生成位元組碼:

terminal
# 更新 Bun 後
bun build --bytecode ./index.ts --outdir=./dist

如果位元組碼與當前 Bun 版本不匹配,Bun 會忽略它,並回退到解析 JavaScript 原始碼。

最佳實踐:將位元組碼生成納入 CI/CD 建置流程,不要將 .jsc 檔案提交到 Git。更新 Bun 時重新生成位元組碼。

仍需原始碼檔案

位元組碼不會替代 JavaScript。你必須同時部署兩個檔案:

  • .js 檔案(打包後的原始碼)
  • .jsc 檔案(位元組碼快取)

執行環境流程:

  1. Bun 載入 .js 檔案,發現 @bytecode 指令,檢查 .jsc 檔案
  2. Bun 載入 .jsc
  3. Bun 驗證位元組碼雜湊與原始碼匹配
  4. 驗證通過則使用位元組碼
  5. 否則回退為解析原始碼

位元組碼不是混淆手段

位元組碼不會隱藏你的原始碼。它是效能最佳化,而非安全措施。

生產部署

Docker

在 Dockerfile 中整合位元組碼生成:

Dockerfile
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun build --bytecode --minify --sourcemap \
  --target=bun \
  --compile \
  ./src/server.ts --outfile=./dist/server

FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /app/dist/server /app/server
CMD ["./server"]

位元組碼檔案與架構無關。

CI/CD

在建置流水線中生成位元組碼:

workflow.yml
# GitHub Actions
- name: Build with bytecode
  run: |
    bun install
    bun build --bytecode --minify \
      --outdir=./dist \
      --target=bun \
      ./src/index.ts

除錯

驗證位元組碼是否生效

檢查 .jsc 檔案是否存在:

terminal
ls -lh dist/
Text
-rw-r--r--  1 user  staff   245K  index.js
-rw-r--r--  1 user  staff   1.1M  index.js.jsc

.jsc 檔案通常比 .js 大 2-8 倍。

若要記錄 Bun 是否使用位元組碼,請在環境中設定 BUN_JSC_verboseDiskCache=1。

快取命中時,Bun 會記錄:

Text
[Disk Cache] Cache hit for sourceCode

快取未命中時:

Text
[Disk Cache] Cache miss for sourceCode

出現多條快取未命中日誌是正常的:Bun 不會對其內建模組中的 JavaScript 進行位元組碼快取。

常見問題

位元組碼被靜默忽略:通常由 Bun 版本更新引起。快取版本不匹配,因此 Bun 會拒絕使用位元組碼。重新生成即可解決。

檔案太大:這是預期的。建議:

  • 使用 --minify 在生成位元組碼前減小程式碼體積
  • 壓縮 .jsc 檔案以便通過網路傳輸(gzip/brotli)
  • 評估啟動速度的提升是否值得檔案體積增加

頂層 await:不支援,請用非同步初始化函式替代。

什麼是位元組碼?

  1. 解析:引擎讀取 JavaScript 原始碼,並將其轉換為抽象語法樹(AST)

  2. 位元組碼編譯:引擎將 AST 編譯為位元組碼——一種更低階、執行速度更快的表示形式

  3. 執行:引擎的直譯器或 JIT 編譯器執行位元組碼

  4. 解析:讀取程式碼,生成抽象語法樹(AST)

  5. 位元組碼編譯:將 AST 編譯為位元組碼,一種更低階但執行更快的中間表示

  6. 執行:引擎通過直譯器或 JIT 編譯器執行位元組碼

以上步驟會在每次執行程式碼時執行。一個每天執行 100 次的 CLI 工具會被解析 100 次;一個無伺服器函式會在每次冷啟動時進行解析。

每次執行你的程式碼都會執行以上步驟。如果你的 CLI 每天執行 100 次,則程式碼被解析 100 次。如果是無伺服器函式發生頻繁冷啟動,解析在每次冷啟動時都會進行。

位元組碼快取將步驟 1 和 2 移到建置時。執行環境直接載入預編譯的位元組碼,加快啟動。

現代 JavaScript 引擎使用一種稱為延遲解析的最佳化。它們不會預先解析所有程式碼,而是在每個函式首次呼叫時才進行解析:

JavaScript
// 沒有位元組碼快取時:
function rarely_used() {
  // 這段 500 行的函式僅在呼叫時被解析
}

function main() {
  console.log("Starting app");
  // rarely_used() 從未呼叫,故無解析
}

延遲解析意味著解析開銷不只是啟動時的開銷。隨著應用執行不同的程式碼路徑,整個執行期間都會發生解析。使用位元組碼快取後,Bun 會預編譯所有函式,包括引擎原本會延遲解析的函式。

位元組碼格式

.jsc 檔案內部結構

.jsc 檔案包含一個序列化的位元組碼結構。

頭部部分(每次載入驗證):

  • 快取版本:與 JavaScriptCore 框架版本關聯的雜湊,確保位元組碼只在對應版本的 Bun 中執行。
  • 程式碼塊型別標籤:指示這是程式、模組、eval 還是函式程式碼塊。

SourceCodeKey(驗證位元組碼對應原始碼):

  • 原始碼雜湊:原始 JavaScript 原始碼的雜湊值。Bun 在使用位元組碼前會驗證該雜湊是否匹配。
  • 原始碼長度:原始碼的確切長度,用於額外驗證。
  • 編譯標誌:編譯上下文,例如嚴格模式、指令碼與模組,以及 eval 上下文型別。相同原始碼使用不同標誌編譯會生成不同的位元組碼。

位元組碼指令:

  • 指令流:位元組碼操作碼,即 JavaScript 的編譯表示,以可變長度的指令序列儲存。
  • 後設資料表:每個操作碼都有相關後設資料,例如效能分析計數器、型別提示和執行次數(即使這些資料尚未填充)。
  • 跳轉目標:為控制流(if/else、迴圈、switch 語句)預先計算的地址。
  • Switch 表:用於 switch 語句的最佳化查詢表。

常量與識別符號:

  • 常量池:程式碼中的所有字面量值——數字、字串、布林值、null、undefined。這些值以 JavaScript 值(JSValues)的形式儲存,因此執行環境無需從原始碼解析。
  • 識別符號表:程式碼中使用的所有變數名和函式名,以去重後的字串形式儲存。
  • 原始碼錶示標記:指示常量應如何表示的標誌(例如整數、雙精度浮點數、大整數等)。

函式後設資料(每個函式對應):

  • 暫存器分配:函式需要多少個暫存器(區域性變數)——thisRegister、scopeRegister、numVars、numCalleeLocals、numParameters。
  • 程式碼特性:函式特徵的位掩碼:它是否為建構函式?箭頭函式?是否使用 super?是否有尾呼叫?這些特性會影響引擎執行函式的方式。
  • 詞法作用域特性:嚴格模式及其他詞法上下文。
  • 解析模式:解析函式時使用的模式(普通、async、生成器、async 生成器)。

巢狀結構:

  • 函式宣告和表示式:每個內嵌函式含獨立位元組碼塊。檔案包含 100 個函式即有 100 個巢狀位元組碼塊。
  • 異常處理:try/catch/finally 邊界及處理地址經過預計算。
  • 表示式資訊:位元組碼位置映射回原始碼位置,方便報錯及除錯。

位元組碼不包含什麼

位元組碼不會嵌入你的原始碼。相反:

  • JavaScript 原始碼儲存在 .js 檔案中
  • 位元組碼只儲存原始碼的雜湊和長度
  • 載入時 Bun 驗證位元組碼與當前原始碼匹配

這就是為什麼你需要同時部署 .js 和 .jsc 檔案:沒有對應的 .js 檔案,.jsc 檔案就毫無用處。

權衡:檔案大小

位元組碼檔案通常比原始碼大 2-8 倍。

為什麼位元組碼更大?

位元組碼指令冗長 一行壓縮後的 JS 程式碼可能對應數十條位元組碼指令,例如:

JavaScript
const sum = arr.reduce((a, b) => a + b, 0);

產生的位元組碼包括:

  • 載入 arr 變數
  • 獲取 reduce 屬性
  • 建立箭頭函式(它本身也有位元組碼)
  • 載入初始值 0
  • 按正確的引數數量設定呼叫
  • 執行呼叫
  • 將結果儲存到 sum

每步都是獨立操作指令附帶元資訊。

常量池儲存所有內容: 每個字串字面量、數字、屬性名——所有內容都會儲存在常量池中。即使原始碼中有 100 個 "hello",常量池也只儲存一次。識別符號表和常量引用仍會增加開銷。

每函式後設資料 即使是一行小函式,也有完整的後設資料:

  • 暫存器分配資訊
  • 程式碼特性掩碼
  • 解析模式
  • 異常處理
  • 除錯用表示式資訊

檔案中有 1000 個小函式即有 1000 份這類資料。

效能分析結構 即使未填充,也有用來記錄效能資料的結構:

  • 值型別分析槽
  • 陣列訪問分析槽
  • 二元算術分析槽
  • 一元算術分析槽

都佔用空間。

預計算控制流 跳轉目標、switch 表、異常邊界預先計算儲存,加快執行也增加大小。

減少體積的策略

壓縮: 位元組碼通過 gzip/brotli 壓縮效果很好(壓縮率 60-70%)。重複的結構和後設資料能夠高效壓縮。

先壓縮程式碼 先用 --minify 可帶來:

  • 較短的識別符號 → 識別符號表更小
  • 消除死程式碼 → 減少位元組碼
  • 常量摺疊 → 常量池變小

權衡: 你需要在檔案增大 2-8 倍與啟動速度加快 2-4 倍之間權衡。對於 CLI,這通常很值得。對於佔用幾兆位元組磁碟空間無關緊要的長時間執行伺服器,這就更不成問題了。

版本與可移植性

跨架構可移植:✅

位元組碼與架構無關。可以:

  • macOS ARM64 建置,部署到 Linux x64
  • Linux x64 建置,部署到 AWS Lambda ARM64
  • Windows x64 建置,部署到 macOS ARM64

位元組碼是抽象指令,由執行環境 JIT 編譯針對架構最佳化。

跨版本可移植:❌

位元組碼不相容不同 Bun 版本,原因:

位元組碼格式變化: JavaScriptCore 的位元組碼格式會隨版本變化。新的操作碼會被新增,舊的操作碼會被移除或修改,後設資料結構也會發生變化。

版本驗證 .jsc 檔案頭部包含快取版本雜湊。載入時:

  1. 從 .jsc 檔案中提取快取版本
  2. 計算當前 JavaScriptCore 版本
  3. 如果兩者不匹配,Bun 會靜默拒絕使用位元組碼
  4. Bun 會回退到解析 .js 原始碼

優雅降級: 這種設計意味著位元組碼快取採用“失敗時放行”策略。如果出現任何問題(版本不匹配、檔案損壞、檔案缺失),程式碼仍會正常執行。你可能會看到啟動速度變慢,但不會遇到錯誤。

未連結與已連結位元組碼

JavaScriptCore 區分“未連結”和“已連結”位元組碼。這種分離機制使位元組碼快取成為可能:

未連結位元組碼(快取內容)

.jsc 檔案儲存的是未連結位元組碼。包含:

  • 編譯的位元組碼指令
  • 程式碼結構資訊
  • 常量與識別符號
  • 控制流資訊

不包含:

  • 指向執行環境物件的指標
  • JIT 編譯的機器碼
  • 執行環境效能分析資料
  • 呼叫連結資訊(函式呼叫關係)

未連結位元組碼是不可變且可共享的,同一程式碼多次執行共用一份。

已連結位元組碼(執行環境)

執行環境,Bun 對位元組碼完成“連結”過程,生成執行環境結構,附加:

  • 呼叫連結資訊:最佳化函式呼叫路徑
  • 效能分析資料:統計指令執行次數、型別流、陣列模式等
  • JIT 編譯狀態:基線 JIT 或最佳化 JIT(DFG/FTL)的程式碼
  • 執行環境物件:JavaScript 物件、原型、作用域指標等

Bun 每次執行程式碼時都會建立這個已連結表示。這種分離機制可以:

  1. 快取成本高昂的解析編譯結果(未連結位元組碼)
  2. 收集執行環境分析資料
  3. 啟用基於分析的 JIT 最佳化

對於生產環境中的 CLI 和無伺服器部署,結合使用 --bytecode --minify --sourcemap 可以在將錯誤映射回原始原始碼的同時,提供最佳的啟動速度。