tsc,启动时间可提升 2 倍。
使用方法
基本用法(CommonJS)
使用--bytecode 标志启用字节码缓存。不使用 --format 时,输出格式默认为 CommonJS:
terminal
dist/index.js- 打包后的 JavaScript(CommonJS)dist/index.js.jsc- 字节码缓存文件
.jsc 文件:
terminal
生成独立可执行文件时
使用--compile 创建可执行文件时,Bun 会将字节码嵌入二进制文件中。ESM 和 CommonJS 均可使用:
terminal
ESM 字节码
ESM 字节码需要使用--compile,因为 Bun 会将模块元数据(导入/导出信息)嵌入编译后的二进制文件中。借助这些元数据,JavaScript 引擎可以在运行时完全跳过解析。
如果不使用 --compile,ESM 字节码仍需要解析源代码以分析模块依赖,这会违背字节码缓存的目的。
与其他优化结合
将字节码与压缩和源映射结合使用:terminal
--minify在生成字节码前压缩代码体积(代码更少,字节码更小)--sourcemap保持错误定位(错误仍指向原始源码)--bytecode消除解析开销。
性能影响
性能提升与代码体量成正比:
体量越大,受益越明显,因为解析的代码更多。
何时使用字节码
非常适合:
CLI 工具
- 经常调用(如 linter、格式化工具、git 钩子)
- 启动时间即全部用户体验
- 用户能明显感受到 90ms 与 45ms 启动时间的差异
- 例子:TypeScript 编译器、Prettier、ESLint
构建工具和任务运行器
- 在开发过程中执行数百次甚至数千次
- 每次节省数毫秒,累计提升明显
- 改善开发者体验
- 例子:构建脚本、测试运行器、代码生成器
独立可执行文件
- 发布给重视性能的用户
- 单文件分发方便
- 文件体积不如启动速度重要
- 例子:通过 npm 或二进制发布的 CLI
不适合:
- ❌ 小型脚本
- ❌ 只运行一次的代码
- ❌ 开发构建
- ❌ 受大小限制的环境
限制
仅支持 CommonJS
字节码缓存目前仅支持 CommonJS 输出格式。Bun 打包器会自动将大部分 ESM 代码转换为 CommonJS,但 顶层 await 是例外:await。
版本兼容性
字节码不跨 Bun 版本通用。字节码格式绑定于 JavaScriptCore 内部表示,不同版本会改变。 更新 Bun 后必须重新生成字节码:terminal
.jsc 文件提交到 Git。更新 Bun 时重新生成字节码。
仍需源码文件
字节码不会替代 JavaScript。你必须同时部署两个文件:.js文件(打包后的源代码).jsc文件(字节码缓存)
- Bun 加载
.js文件,发现@bytecode指令,检查.jsc文件 - Bun 加载
.jsc - Bun 验证字节码哈希与源码匹配
- 验证通过则使用字节码
- 否则回退为解析源码
字节码不是混淆手段
字节码不会隐藏你的源代码。它是性能优化,而非安全措施。生产部署
Docker
在 Dockerfile 中集成字节码生成:Dockerfile
CI/CD
在构建流水线中生成字节码:workflow.yml
调试
验证字节码是否生效
检查.jsc 文件是否存在:
terminal
.jsc 文件通常比 .js 大 2-8 倍。
要记录是否使用了字节码,请在环境中设置 BUN_JSC_verboseDiskCache=1。
缓存命中时,Bun 会记录:
常见问题
字节码被静默忽略:通常是 Bun 版本更新导致的缓存版本不匹配,重新生成可解决。 文件太大:这是预期的。建议:- 使用
--minify在生成字节码前减小代码体积 - 压缩
.jsc文件以便通过网络传输(gzip/brotli) - 评估启动速度的提升是否值得文件体积增加
什么是字节码?
运行 JavaScript 时,JavaScript 引擎并不直接执行源代码,而是经过以下步骤:- 解析:读取代码,生成抽象语法树(AST)
- 字节码编译:将 AST 编译为字节码,一种更低级但执行更快的中间表示
- 执行:引擎通过解释器或 JIT 编译器执行字节码
字节码格式
.jsc 文件内部结构
.jsc 文件包含一个序列化的字节码结构。
头部部分(每次加载验证):
- 缓存版本:与 JavaScriptCore 框架版本关联的哈希,确保字节码只在对应版本的 Bun 中运行。
- 代码块类型标签:指示这是程序、模块、eval 还是函数代码块。
- 源码哈希:原始 JavaScript 源码的哈希值。Bun 在使用字节码前会验证该哈希是否匹配。
- 源码长度:源码的确切长度,用于额外验证。
- 编译标志:编译上下文,例如严格模式、脚本与模块,以及 eval 上下文类型。相同源码使用不同标志编译会生成不同的字节码。
- 指令流:字节码操作码,即 JavaScript 的编译表示,以可变长度的指令序列存储。
- 元数据表:每个操作码都有相关元数据,例如性能分析计数器、类型提示和执行次数(即使这些数据尚未填充)。
- 跳转目标:为控制流(if/else、循环、switch 语句)预先计算的地址。
- Switch 表:用于 switch 语句的优化查找表。
- 常量池:代码中的所有字面量值——数字、字符串、布尔值、null、undefined。这些值以 JavaScript 值(JSValues)的形式存储,因此运行时无需从源码解析。
- 标识符表:代码中使用的所有变量名和函数名,以去重后的字符串形式存储。
- 源码表示标记:指示常量应如何表示的标志(例如整数、双精度浮点数、大整数等)。
- 寄存器分配:函数所需寄存器数,如
thisRegister、scopeRegister、numVars、numCalleeLocals、numParameters。 - 代码特性:函数特性位掩码,如是否构造函数、箭头函数、使用
super、尾调用等,影响执行行为。 - 词法作用域特性:严格模式及其他词法上下文。
- 解析模式:函数被解析时的模式(普通、异步、生成器、异步生成器)。
- 函数声明和表达式:每个内嵌函数含独立字节码块。文件包含 100 个函数即有 100 个嵌套字节码块。
- 异常处理:try/catch/finally 边界及处理地址经过预计算。
- 表达式信息:字节码位置映射回源码位置,方便报错及调试。
字节码不包含什么
字节码不会嵌入你的源码。相反:- JavaScript 源码保存在
.js文件中 - 字节码只存储源码的哈希和长度
- 加载时 Bun 验证字节码与当前源码匹配
.js 和 .jsc 文件:没有对应的 .js 文件,.jsc 文件就毫无用处。
权衡:文件大小
Bytecode files are typically 2-8x larger than the source code.为什么字节码更大?
字节码指令冗长 一行压缩后的 JS 代码可能对应数十条字节码指令,例如:- 加载
arr变量 - 取
reduce属性 - 创建箭头函数(其本身含字节码)
- 加载初始值 0
- 设定调用参数数量
- 执行调用
- 结果赋值给
sum
"hello" 出现 100 次,常量池只存储一次,但标识符表和引用增加额外空间。
每函数元数据
即使是一行小函数,也有完整的元数据:
- 寄存器分配信息
- 代码特性掩码
- 解析模式
- 异常处理
- 调试用表达式信息
- 值类型分析槽
- 数组访问分析槽
- 二元算术分析槽
- 一元算术分析槽
减少体积的策略
压缩: 字节码通过 gzip/brotli 压缩效果很好(压缩率 60-70%)。重复的结构和元数据能够高效压缩。 先压缩代码 先用--minify 可带来:
- 较短的标识符 → 标识符表更小
- 消除死代码 → 减少字节码
- 常量折叠 → 常量池变小
版本与可移植性
跨架构可移植:✅
字节码与架构无关。可以:- macOS ARM64 构建,部署到 Linux x64
- Linux x64 构建,部署到 AWS Lambda ARM64
- Windows x64 构建,部署到 macOS ARM64
跨版本可移植:❌
字节码不兼容不同 Bun 版本,原因: 字节码格式变化: JavaScriptCore 的字节码格式会随版本变化。新的操作码会被添加,旧的操作码会被移除或修改,元数据结构也会发生变化。 版本验证.jsc 文件头部包含缓存版本哈希。加载时:
- 读取
.jsc中缓存版本 - 计算当前 JavaScriptCore 版本哈希
- 不匹配时字节码静默拒绝
- 回退解析
.js源码
未链接与已链接字节码
JavaScriptCore 区分“未链接”和“已链接”字节码。这种分离机制使字节码缓存成为可能:未链接字节码(缓存内容)
.jsc 文件保存的是未链接字节码。包含:
- 编译的字节码指令
- 代码结构信息
- 常量与标识符
- 控制流信息
- 指向运行时对象的指针
- JIT 编译的机器码
- 运行时性能分析数据
- 调用链接信息(函数调用关系)
已链接字节码(运行时)
运行时,Bun 对字节码完成“链接”过程,生成运行时结构,附加:- 调用链接信息:优化函数调用路径
- 性能分析数据:统计指令执行次数、类型流、数组模式等
- JIT 编译状态:基线 JIT 或优化 JIT(DFG/FTL)的代码
- 运行时对象:JavaScript 对象、原型、作用域指针等
- 缓存成本高昂的解析编译结果(未链接字节码)
- 收集运行时分析数据
- 启用基于分析的 JIT 优化
--bytecode --minify --sourcemap 可以在将错误映射回原始源代码的同时,提供最佳的启动速度。