tsc,启动时间提升可达 2 倍。
使用方法
基础用法
使用--bytecode 标志启用字节码缓存:
terminal
dist/index.js- 你的打包好的 JavaScriptdist/index.jsc- 字节码缓存文件
.jsc 文件:
terminal
生成独立可执行文件时
使用--compile 创建可执行文件时,字节码会嵌入到二进制文件:
terminal
与其他优化结合
字节码支持与代码压缩和源码映射同时使用:terminal
--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 是例外:await。
版本兼容性
字节码不跨 Bun 版本通用。字节码格式绑定于 JavaScriptCore 内部表示,不同版本会改变。 更新 Bun 后必须重新生成字节码:terminal
.jsc 文件提交到 Git。更新 Bun 时重新生成字节码。
仍需源码文件
.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 版本更新导致的缓存版本不匹配,重新生成可解决。 文件太大:这是预期的。建议:- 先用
--minify减小代码体积再生成字节码 - 网络传输时压缩
.jsc文件(gzip/brotli) - 评估启动性能提升是否值得文件增大代价
什么是字节码?
运行 JavaScript 时,JavaScript 引擎并不直接执行源代码,而是经过以下步骤:- 解析:读取代码,生成抽象语法树(AST)
- 字节码编译:将 AST 编译为字节码,一种更低级但执行更快的中间表示
- 执行:引擎通过解释器或 JIT 编译器执行字节码
为什么懒解析让字节码更有效
现代 JavaScript 引擎有种巧妙的优化叫懒解析,不会一开始解析所有代码,只有函数首次调用时才解析:字节码格式
.jsc 文件内部结构
.jsc 文件包含序列化的字节码结构。理解其内容能解释性能优势与文件增大间的权衡。
头部部分(每次加载验证):
- 缓存版本:与 JavaScriptCore 框架版本关联的哈希,确保字节码只在对应版本的 Bun 中运行。
- 代码块类型标签:指示这是程序、模块、eval 还是函数代码块。
- 源码哈希:原始 JavaScript 代码的哈希,Bun 会校验匹配后才使用字节码。
- 源码长度:源码精确长度,用于进一步验证。
- 编译标记:如严格模式、脚本或模块类型、eval 类型等。不同标记下同一源码生成不同字节码。
- 指令流:实际的字节码操作码,表示 JavaScript 编译后代码,是可变长度序列。
- 元数据表:每条操作码相关的元信息,如性能计数器、类型提示、执行次数(即使未填充)。
- 跳转目标:针对控制流(if/else、循环、switch)预计算的地址。
- Switch 表:针对 switch 语句的优化查找表。
- 常量池:代码中所有字面量,包括数字、字符串、布尔值、null、undefined。以 JavaScript 值(JSValue)存储,运行时无需解析源码来加载。
- 标识符表:代码使用过的所有变量与函数名,采用去重字符串存储。
- 源码表示标记:指示常量如何存储(整数、双精度、BigInt 等)。
- 寄存器分配:函数所需寄存器数,如
thisRegister、scopeRegister、numVars、numCalleeLocals、numParameters。 - 代码特性:函数特性位掩码,如是否构造函数、箭头函数、使用
super、尾调用等,影响执行行为。 - 词法作用域特性:严格模式及其他词法上下文。
- 解析模式:函数被解析时的模式(普通、异步、生成器、异步生成器)。
- 函数声明和表达式:每个内嵌函数含独立字节码块。文件包含 100 个函数即有 100 个嵌套字节码块。
- 异常处理:try/catch/finally 边界及处理地址经过预计算。
- 表达式信息:字节码位置映射回源码位置,方便报错及调试。
字节码不包含什么
重要的是,字节码不包含你的源码文本,而是:- JavaScript 源码保存在
.js文件中 - 字节码只存储源码的哈希和长度
- 加载时 Bun 验证字节码与当前源码匹配
.js 和 .jsc 文件,缺一不可。
权衡:文件大小
字节码文件显著大于源码——一般比源码大 2-8 倍。为什么字节码更大?
字节码指令冗长一行压缩后的 JS 代码可能对应数十条字节码指令,例如:
- 加载
arr变量 - 取
reduce属性 - 创建箭头函数(其本身含字节码)
- 加载初始值 0
- 设定调用参数数量
- 执行调用
- 结果赋值给
sum
例如字符串
"hello" 出现 100 次,常量池只存储一次,但标识符表和引用增加额外空间。
每函数元数据即使是一行小函数,也有完整的元数据:
- 寄存器分配信息
- 代码特性掩码
- 解析模式
- 异常处理
- 调试用表达式信息
即使未填充,也有用来记录性能数据的结构:
- 值类型分析槽
- 数组访问分析槽
- 二元算术分析槽
- 一元算术分析槽
跳转目标、switch 表、异常边界预先计算存储,加快执行也增加大小。
减少体积的策略
压缩字节码对 gzip / brotli 压缩非常友好,可减小 60-70%。 先压缩代码
先用
--minify 可带来:
- 较短的标识符 → 标识符表更小
- 消除死代码 → 减少字节码
- 常量折叠 → 常量池变小
通常你用 2-4 倍更大的文件换 2-4 倍更快的启动。对 CLI 来说极具价值;对长时间运行服务器,几兆字节无所谓。
版本与可移植性
跨架构可移植:✅
字节码与架构无关。可以:- 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 可获得性能和调试性的平衡。