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 字节码仍然需要解析源代码以分析模块依赖关系,这就违背了字节码缓存的目的。
与其他优化结合使用
将字节码与压缩和 source map 结合使用:terminal
--minify在生成字节码之前减少代码体积(更少的代码 -> 更少的字节码)--sourcemap保留错误报告(错误仍然指向原始源代码)--bytecode消除解析开销
性能影响
性能提升随你的代码库规模而扩展:
越大的应用受益越多,因为它们有更多代码需要解析。
何时使用字节码
非常适合:
CLI 工具
- 频繁调用(linter、格式化工具、Git hooks)
- 启动时间就是整个用户体验
- 用户能察觉 90ms 和 45ms 启动的差异
- 示例:TypeScript 编译器、Prettier、ESLint
构建工具和任务运行器
- 开发期间运行成百上千次
- 每次节省的毫秒数快速累积
- 改善开发者体验
- 示例:构建脚本、测试运行器、代码生成器
独立可执行文件
- 分发给关心性能的用户
- 单文件分发很方便
- 文件大小不如启动时间重要
- 示例:通过 npm 或作为二进制分发的 CLI
跳过它,当:
- ❌ 小型脚本
- ❌ 只运行一次的代码
- ❌ 开发构建
- ❌ 对大小敏感的环境
限制
版本兼容性
字节码在不同 Bun 版本之间不可移植。字节码格式与 JavaScriptCore 的内部表示绑定,该表示在不同版本之间会变化。 当你更新 Bun 时,必须重新生成字节码:terminal
.jsc 文件提交到 git。每次更新 Bun 时重新生成它们。
仍然需要源代码
字节码不会替代你的 JavaScript。你必须同时部署两个文件:.js文件(打包后的源代码).jsc文件(字节码缓存)
- Bun 加载
.js文件,看到@bytecode编译指示,检查.jsc文件 - Bun 加载
.jsc文件 - Bun 验证字节码哈希与源代码匹配
- 如果有效,Bun 使用字节码
- 如果无效,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 引擎不会直接执行你的源代码。相反,它会经历几个步骤:- 解析:引擎读取你的 JavaScript 源代码并将其转换为抽象语法树(AST)
- 字节码编译:AST 被编译为字节码——一个较低层次的表示,执行速度更快
- 执行:字节码由引擎的解释器或 JIT 编译器执行
为什么延迟解析让它变得更好
现代 JavaScript 引擎使用一种称为延迟解析的优化。它们不会一次性解析所有代码——相反,函数只在首次被调用时才被解析:字节码格式
.jsc 文件内部
.jsc 文件包含序列化的字节码结构。
头部部分(每次加载时验证):
- 缓存版本:与 JavaScriptCore 框架版本绑定的哈希值。这确保一个 Bun 版本生成的字节码只能在该精确版本上运行。
- 代码块类型标签:标识这是 Program、Module、Eval 还是 Function 代码块。
- 源代码哈希:原始 JavaScript 源代码的哈希值。Bun 在使用字节码前验证其匹配。
- 源代码长度:源代码的确切长度,用于额外验证。
- 编译标志:编译上下文,如严格模式、脚本 vs 模块、eval 上下文类型。相同的源代码使用不同的标志编译会产生不同的字节码。
- 指令流:字节码操作码——你的 JavaScript 的编译表示,以变长指令序列存储。
- 元数据表:每个操作码关联的元数据,如性能计数器、类型提示和执行计数(即使尚未填充)。
- 跳转目标:控制流的预计算地址(if/else、循环、switch 语句)。
- Switch 表:switch 语句的优化查找表。
- 常量池:代码中的所有字面值——数字、字符串、布尔值、null、undefined。它们存储为 JavaScript 值(JSValues),这样就不需要在运行时从源代码解析。
- 标识符表:代码中使用的所有变量和函数名。存储为去重后的字符串。
- 源代码表示标记:指示常量应如何表示的标志(整数、双精度浮点数、大整数等)。
- 寄存器分配:函数需要的寄存器(局部变量)数量——
thisRegister、scopeRegister、numVars、numCalleeLocals、numParameters。 - 代码特性:函数特征的位掩码:它是构造函数吗?箭头函数?是否使用
super?是否有尾调用?这些影响函数的执行方式。 - 词法作用域特性:严格模式和其他词法上下文。
- 解析模式:函数被解析的模式(普通、async、generator、async generator)。
- 函数声明和表达式:每个嵌套函数递归获得自己的字节码块。一个有 100 个函数的文件有 100 个独立的字节码块,全部嵌套在结构中。
- 异常处理器:try/catch/finally 块及其预计算的边界和处理程序地址。
- 表达式信息:将字节码位置映射回源代码位置,用于错误报告和调试。
字节码中不包含什么
字节码不嵌入你的源代码。相反:- JavaScript 源代码单独存储(在
.js文件中) - 字节码只存储源代码的哈希值和长度
- 加载时,Bun 验证字节码与当前源代码匹配
.js 和 .jsc 文件:没有对应的 .js 文件,.jsc 文件是无用的。
权衡:文件大小
字节码文件通常比源代码大 2-8 倍。为什么字节码要大得多?
字节码指令很冗长: 一行压缩后的 JavaScript 可能编译成数十条字节码指令。例如:- 加载
arr变量 - 获取
reduce属性 - 创建箭头函数(其本身也有字节码)
- 加载初始值
0 - 使用正确数量的参数设置调用
- 实际执行调用
- 将结果存储到
sum中
"hello" 一百次,常量池也只存储一次,但标识符表和常量引用会增加开销。
每个函数的元数据:
每个函数——即使是一行的小函数——都有自己完整的元数据:
- 寄存器分配信息
- 代码特性位掩码
- 解析模式
- 异常处理器
- 用于调试的表达式信息
- 值分析槽(跟踪每个操作流经的类型)
- 数组分析槽(跟踪数组访问模式)
- 二元算术分析槽(跟踪数学运算中的数字类型)
- 一元算术分析槽
缓解策略
压缩: 字节码用 gzip/brotli 压缩效果很好(60-70% 压缩率)。重复的结构和元数据能被高效压缩。 先压缩: 在生成字节码之前使用--minify 有助于:
- 更短的标识符 → 更小的标识符表
- 死代码消除 → 生成更少的字节码
- 常量折叠 → 池中更少的常量
版本控制与可移植性
跨架构可移植性:✅
字节码是架构无关的。你可以:- 在 macOS ARM64 上构建,部署到 Linux x64
- 在 Linux x64 上构建,部署到 AWS Lambda ARM64
- 在 Windows x64 上构建,部署到 macOS ARM64
跨版本可移植性:❌
字节码在不同 Bun 版本之间不稳定。原因如下: 字节码格式变化: JavaScriptCore 的字节码格式在各个版本之间会变化。新的操作码被添加,旧的被移除或改变,元数据结构也会变化。 版本验证:.jsc 文件头中的缓存版本是 JavaScriptCore 框架的哈希值。当 Bun 加载字节码时:
- 它从
.jsc文件中提取缓存版本 - 它计算当前的 JavaScriptCore 版本
- 如果它们不匹配,字节码会被静默拒绝
- Bun 回退到解析
.js源代码
未链接 vs 链接字节码
JavaScriptCore 区分”未链接”和”链接”字节码。这种分离使得字节码缓存成为可能:未链接字节码(被缓存的)
保存在.jsc 文件中的字节码是未链接字节码。它包含:
- 编译后的字节码指令
- 代码的结构信息
- 常量和标识符
- 控制流信息
- 指向实际运行时对象的指针
- JIT 编译后的机器码
- 之前运行时的性能分析数据
- 调用链接信息(哪个函数调用了哪个)
链接字节码(运行时执行)
当 Bun 运行字节码时,它会”链接”它——创建一个运行时包装器,添加:- 调用链接信息:随着代码运行,引擎会了解哪些函数互相调用,并优化这些调用点。
- 性能分析数据:引擎跟踪每条指令执行了多少次、流经代码的值类型、数组访问模式等。
- JIT 编译状态:对热点代码的 baseline JIT 或优化 JIT(DFG/FTL)编译版本的引用。
- 运行时对象:指向实际 JavaScript 对象、原型、作用域等的指针。
- 缓存昂贵的工作(解析和编译为未链接字节码)
- 仍然收集运行时性能分析数据以指导优化
- 仍然应用 JIT 优化基于实际的执行模式
--bytecode --minify --sourcemap 的组合为你提供了最佳的启动时间,同时保持错误映射到原始源代码。