Skip to main content
字节码缓存是一种构建时优化,通过将你的 JavaScript 预编译为字节码,从而显著提升应用启动速度。例如,在启用字节码的情况下编译 TypeScript 的 tsc,启动时间提升可达 2 倍

使用方法

基础用法

使用 --bytecode 标志启用字节码缓存:
terminal
bun build ./index.ts --target=bun --bytecode --outdir=./dist
这会生成两个文件:
  • dist/index.js - 你的打包好的 JavaScript
  • dist/index.jsc - 字节码缓存文件
运行时,Bun 会自动检测并使用 .jsc 文件:
terminal
bun ./dist/index.js  # 自动使用 index.jsc

生成独立可执行文件时

使用 --compile 创建可执行文件时,字节码会嵌入到二进制文件:
terminal
bun build ./cli.ts --compile --bytecode --outfile=mycli
生成的可执行文件同时包含代码和字节码,在单文件中提供极致性能。

与其他优化结合

字节码支持与代码压缩和源码映射同时使用:
terminal
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli
  • --minify 在生成字节码前压缩代码体积(代码更少,字节码更小)
  • --sourcemap 保持错误定位(错误仍指向原始源码)
  • --bytecode 消除解析开销

性能影响

性能提升与代码体量成正比:
应用大小典型启动速度提升
小型 CLI (< 100 KB)快 1.5-2 倍
中大型应用 (> 5 MB)快 2.5-4 倍
体量越大,受益越明显,因为解析的代码更多。

何时使用字节码

非常适合:

CLI 工具

  • 经常调用(如 linter、格式化工具、git 钩子)
  • 启动时间即全部用户体验
  • 用户能明显感受到 90ms 与 45ms 启动时间的差异
  • 例子:TypeScript 编译器、Prettier、ESLint

构建工具和任务运行器

  • 在开发过程中执行数百次甚至数千次
  • 每次节省数毫秒,累计提升明显
  • 改善开发者体验
  • 例子:构建脚本、测试运行器、代码生成器

独立可执行文件

  • 发布给重视性能的用户
  • 单文件分发方便
  • 文件体积不如启动速度重要
  • 例子:通过 npm 或二进制发布的 CLI

不适合:

  • 小型脚本
  • 只执行一次的代码
  • 开发环境构建
  • 对体积有限制的环境
  • 包含顶层 await 的代码(不支持)

限制

仅支持 CommonJS

字节码缓存目前仅支持 CommonJS 输出格式。Bun 打包器会自动将大部分 ESM 代码转换为 CommonJS,但 顶层 await 是例外:
// 会阻止字节码缓存
const data = await fetch("https://api.example.com");
export default data;
原因:顶层 await 需要异步模块评估,这无法用 CommonJS 表示。模块图异步化,CommonJS 包装函数模型失效。 解决方案:将异步初始化放入函数:
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 不匹配,会被自动忽略,回退为解析 JavaScript 源码。应用依然能运行,只是失去性能优化。 最佳实践:将字节码生成纳入 CI/CD 构建流程,不要将 .jsc 文件提交到 Git。更新 Bun 时重新生成字节码。

仍需源码文件

  • .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 \
  --outdir=./dist \
  --compile \
  ./src/server.ts --outfile=./dist/server

FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /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/
-rw-r--r--  1 user  staff   245K  index.js
-rw-r--r--  1 user  staff   1.1M  index.jsc
.jsc 文件通常比 .js 大 2-8 倍。 要记录字节码使用情况,设置环境变量 BUN_JSC_verboseDiskCache=1 成功时日志示例:
[Disk cache] cache hit for sourceCode
若缓存未命中,日志示例:
[Disk cache] cache miss for sourceCode
多次缓存未命中正常,因为 Bun 目前不缓存内置模块的 JavaScript 字节码。

常见问题

字节码被静默忽略:通常是 Bun 版本更新导致的缓存版本不匹配,重新生成可解决。 文件太大:这是预期的。建议:
  • 先用 --minify 减小代码体积再生成字节码
  • 网络传输时压缩 .jsc 文件(gzip/brotli)
  • 评估启动性能提升是否值得文件增大代价
顶层 await:不支持,请用异步初始化函数替代。

什么是字节码?

运行 JavaScript 时,JavaScript 引擎并不直接执行源代码,而是经过以下步骤:
  1. 解析:读取代码,生成抽象语法树(AST)
  2. 字节码编译:将 AST 编译为字节码,一种更低级但执行更快的中间表示
  3. 执行:引擎通过解释器或 JIT 编译器执行字节码
字节码介于源代码和机器码之间,可理解为虚拟机的汇编语言。每个字节码指令对应一条操作,如“加载变量”、“两个数字相加”或“调用函数”。 每次运行你的代码都会执行以上步骤。如果你的 CLI 每天运行 100 次,则代码被解析 100 次。如果是 serverless 函数发生频繁冷启动,解析在每次冷启动时都会进行。 字节码缓存将步骤 1 和 2 移到构建时。运行时直接加载预编译的字节码,加快启动。

为什么懒解析让字节码更有效

现代 JavaScript 引擎有种巧妙的优化叫懒解析,不会一开始解析所有代码,只有函数首次调用时才解析:
// 没有字节码缓存时:
function rarely_used() {
  // 这段 500 行的函数仅在调用时被解析
}

function main() {
  console.log("Starting app");
  // rarely_used() 从未调用,故无解析
}
解析开销不仅是启动时的成本,而是在应用运行期间根据不同路径陆续产生。字节码缓存则提前编译所有函数,包括本应懒解析的,那些解析工作一次性在构建时完成。

字节码格式

.jsc 文件内部结构

.jsc 文件包含序列化的字节码结构。理解其内容能解释性能优势与文件增大间的权衡。 头部部分(每次加载验证):
  • 缓存版本:与 JavaScriptCore 框架版本关联的哈希,确保字节码只在对应版本的 Bun 中运行。
  • 代码块类型标签:指示这是程序、模块、eval 还是函数代码块。
SourceCodeKey(验证字节码对应源码):
  • 源码哈希:原始 JavaScript 代码的哈希,Bun 会校验匹配后才使用字节码。
  • 源码长度:源码精确长度,用于进一步验证。
  • 编译标记:如严格模式、脚本或模块类型、eval 类型等。不同标记下同一源码生成不同字节码。
字节码指令
  • 指令流:实际的字节码操作码,表示 JavaScript 编译后代码,是可变长度序列。
  • 元数据表:每条操作码相关的元信息,如性能计数器、类型提示、执行次数(即使未填充)。
  • 跳转目标:针对控制流(if/else、循环、switch)预计算的地址。
  • Switch 表:针对 switch 语句的优化查找表。
常量与标识符
  • 常量池:代码中所有字面量,包括数字、字符串、布尔值、null、undefined。以 JavaScript 值(JSValue)存储,运行时无需解析源码来加载。
  • 标识符表:代码使用过的所有变量与函数名,采用去重字符串存储。
  • 源码表示标记:指示常量如何存储(整数、双精度、BigInt 等)。
函数元数据(每个函数对应):
  • 寄存器分配:函数所需寄存器数,如 thisRegisterscopeRegisternumVarsnumCalleeLocalsnumParameters
  • 代码特性:函数特性位掩码,如是否构造函数、箭头函数、使用 super、尾调用等,影响执行行为。
  • 词法作用域特性:严格模式及其他词法上下文。
  • 解析模式:函数被解析时的模式(普通、异步、生成器、异步生成器)。
嵌套结构
  • 函数声明和表达式:每个内嵌函数含独立字节码块。文件包含 100 个函数即有 100 个嵌套字节码块。
  • 异常处理:try/catch/finally 边界及处理地址经过预计算。
  • 表达式信息:字节码位置映射回源码位置,方便报错及调试。

字节码不包含什么

重要的是,字节码不包含你的源码文本,而是:
  • JavaScript 源码保存在 .js 文件中
  • 字节码只存储源码的哈希和长度
  • 加载时 Bun 验证字节码与当前源码匹配
因此需要同时发布 .js.jsc 文件,缺一不可。

权衡:文件大小

字节码文件显著大于源码——一般比源码大 2-8 倍。

为什么字节码更大?

字节码指令冗长
一行压缩后的 JS 代码可能对应数十条字节码指令,例如:
const sum = arr.reduce((a, b) => a + b, 0);
产生的字节码包括:
  • 加载 arr 变量
  • reduce 属性
  • 创建箭头函数(其本身含字节码)
  • 加载初始值 0
  • 设定调用参数数量
  • 执行调用
  • 结果赋值给 sum
每步都是独立操作指令附带元信息。 常量池存储所有字面量
例如字符串 "hello" 出现 100 次,常量池只存储一次,但标识符表和引用增加额外空间。
每函数元数据
即使是一行小函数,也有完整的元数据:
  • 寄存器分配信息
  • 代码特性掩码
  • 解析模式
  • 异常处理
  • 调试用表达式信息
文件中有 1000 个小函数即有 1000 份这类数据。 性能分析结构
即使未填充,也有用来记录性能数据的结构:
  • 值类型分析槽
  • 数组访问分析槽
  • 二元算术分析槽
  • 一元算术分析槽
都占用空间。 预计算控制流
跳转目标、switch 表、异常边界预先计算存储,加快执行也增加大小。

减少体积的策略

压缩
字节码对 gzip / brotli 压缩非常友好,可减小 60-70%。
先压缩代码
先用 --minify 可带来:
  • 较短的标识符 → 标识符表更小
  • 消除死代码 → 减少字节码
  • 常量折叠 → 常量池变小
权衡考虑
通常你用 2-4 倍更大的文件换 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. 不匹配时字节码静默拒绝
  4. 回退解析 .js 源码
应用仍然运行,只是性能优化失效。 降级设计
字节码缓存对异常“开放失败”:出错时自动回退解析,无异常抛出,保证代码运行。

未链接与已链接字节码

JavaScriptCore 区分“未链接”和“已链接”字节码,这使字节码缓存成为可能。

未链接字节码(缓存内容)

.jsc 文件保存的是未链接字节码。包含:
  • 编译的字节码指令
  • 代码结构信息
  • 常量与标识符
  • 控制流信息
不包含:
  • 指向运行时对象的指针
  • JIT 编译的机器码
  • 运行时性能分析数据
  • 调用链接信息(函数调用关系)
未链接字节码是不可变且可共享的,同一代码多次运行共用一份。

已链接字节码(运行时)

运行时,Bun 对字节码完成“链接”过程,生成运行时结构,附加:
  • 调用链接信息:优化函数调用路径
  • 性能分析数据:统计指令执行次数、类型流、数组模式等
  • JIT 编译状态:基线 JIT 或优化 JIT(DFG/FTL)的代码
  • 运行时对象:JavaScript 对象、原型、作用域指针等
每次运行产生新链接副本,允许:
  1. 缓存昂贵的解析编译(未链接字节码)
  2. 收集运行时分析数据
  3. 启用基于分析的 JIT 优化
字节码缓存将编译工作从运行时移至构建时,对于频繁启动的应用,可将启动时间减半,但换来更大的磁盘文件。 在生产 CLI 和无服务器部署中,结合 --bytecode --minify --sourcemap 可获得性能和调试性的平衡。

结束