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.5x-4x倍 |
何时使用字节码
非常适合:
CLI工具
- 频繁调用(linters、formatters、git hooks)
- 启动时间就是整个用户体验
- 用户会注意到90毫秒和45毫秒启动时间的差异
- 示例:TypeScript编译器、Prettier、ESLint
构建工具和任务运行器
- 在开发过程中运行数百或数千次
- 每次运行节省的毫秒数迅速累积
- 提升开发者体验
- 示例:构建脚本、测试运行器、代码生成器
独立可执行文件
- 分发给关心快速性能的用户
- 单文件分发很方便
- 文件大小不如启动时间重要
- 示例:通过npm或作为二进制文件分发的CLIs
跳过的情况:
- ❌ 小型脚本
- ❌ 一次运行的代码
- ❌ 开发构建
- ❌ 空间受限的环境
- ❌ 具有顶级await的代码(不支持)
限制
仅CommonJS
字节码缓存当前仅适用于CommonJS输出格式。Bun的打包器会自动将大多数ESM代码转换为CommonJS,但顶级await是个例外:版本兼容性
字节码不能跨Bun版本移植。字节码格式与JavaScriptCore的内部表示绑定,版本之间会发生变化。 更新Bun时,必须重新生成字节码:terminal
.jsc文件提交到git。每当更新Bun时重新生成它们。
仍需要源代码
- [.js]文件(您的打包源代码)
- [.jsc]文件(字节码缓存)
- Bun加载[.js]文件,看到
@bytecode指令,并检查[.jsc]文件 - Bun加载[.jsc]文件
- Bun验证字节码哈希与源码匹配
- 如果有效,Bun使用字节码
- 如果无效,Bun回退到解析源码
字节码不是混淆
字节码不会隐藏您的源代码。它是一种优化,而不是安全措施。生产部署
Docker
在Dockerfile中包含字节码生成:Dockerfile
CI/CD
在构建管道期间生成字节码:workflow.yml
调试
验证字节码是否正在使用
检查[.jsc]文件是否存在:terminal
BUN_JSC_verboseDiskCache=1。
成功时,它将记录如下内容:
常见问题
字节码被静默忽略: 通常是由Bun版本更新引起的。缓存版本不匹配,因此字节码被拒绝。重新生成即可修复。 文件太大: 这是预期的。考虑:- 使用
--minify在字节码生成前减小代码大小 - 压缩[.jsc]文件以进行网络传输(gzip/brotli)
- 评估启动性能增益是否值得大小增加
什么是字节码?
运行JavaScript时,JavaScript引擎不会直接执行源代码。相反,它经历几个步骤:- 解析: 引擎读取JavaScript源代码并将其转换为抽象语法树(AST)
- 字节码编译: AST编译为字节码 - 一种更低级的表示,执行速度更快
- 执行: 字节码由引擎的解释器或JIT编译器执行
为什么延迟解析使其更好
现代JavaScript引擎使用一种称为延迟解析的巧妙优化。它们不会预先解析所有代码 - 相反,只有在首次调用时才解析函数:字节码格式
[.jsc]文件内部
[.jsc]文件包含序列化的字节码结构。了解内部内容有助于解释性能优势和文件大小权衡。 头部部分(每次加载时验证):- 缓存版本: 与JavaScriptCore框架版本相关的哈希。这确保用某个Bun版本生成的字节码只能与该确切版本一起运行。
- 代码块类型标签: 确定这是Program、Module、Eval还是Function代码块。
- 源代码哈希: 原始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验证字节码与当前源码匹配
权衡: 文件大小
字节码文件明显大于源代码 - 通常大2-8倍。为什么字节码如此大?
字节码指令很冗长: 一行压缩的JavaScript可能编译为几十个字节码指令。例如:- 加载
arr变量 - 获取
reduce属性 - 创建箭头函数(本身有字节码)
- 加载初始值
0 - 设置具有正确参数数量的调用
- 实际执行调用
- 将结果存储在
sum中
- 寄存器分配信息
- 代码特性位掩码
- 解析模式
- 异常处理程序
- 用于调试的表达式信息
- 值分析槽(跟踪每种操作中流动的类型)
- 数组分析槽(跟踪数组访问模式)
- 二元算术分析槽(跟踪数学运算中的数字类型)
- 一元算术分析槽
缓解策略
压缩: 字节码通过gzip/brotli压缩效果极佳(60-70%压缩率)。重复的结构和元数据压缩效率高。 先压缩: 在字节码生成前使用--minify有帮助:
- 较短的标识符 → 较小的标识符表
- 死代码消除 → 生成较少的字节码
- 常量折叠 → 池中较少的常量
版本和可移植性
跨架构可移植性: ✅
字节码是架构无关的。您可以:- 在macOS ARM64上构建,在Linux x64上部署
- 在Linux x64上构建,在AWS Lambda ARM64上部署
- 在Windows x64上构建,在macOS ARM64上部署
跨版本可移植性: ❌
字节码在Bun版本间不稳定。原因如下: 字节码格式变化: JavaScriptCore的字节码格式在演变。添加新操作码,删除旧操作码或更改旧操作码,元数据结构改变。JavaScriptCore的每个版本都有不同的字节码格式。 版本验证: [.jsc]文件头中的缓存版本是JavaScriptCore框架的哈希。当Bun加载字节码时:- 它从[.jsc]文件提取缓存版本
- 它计算当前JavaScriptCore版本
- 如果不匹配,字节码被静默拒绝
- Bun回退到解析[.js]源代码
未链接vs已链接字节码
JavaScriptCore在”未链接”和”已链接”字节码之间做出重要区分。这种分离使得字节码缓存成为可能:未链接字节码(缓存的)
保存在[.jsc]文件中的字节码是未链接字节码。它包含:- 编译的字节码指令
- 代码的结构信息
- 常量和标识符
- 控制流信息
- 指向实际运行时对象的指针
- JIT编译的机器代码
- 以前运行的分析数据
- 调用链接信息(哪些函数调用哪些)
已链接字节码(运行时执行)
当Bun运行字节码时,它”链接”它 - 创建一个运行时包装器,添加:- 调用链接信息: 随着代码运行,引擎了解哪些函数调用哪些,并优化那些调用站点。
- 分析数据: 引擎跟踪每个指令执行多少次,什么类型的值流经代码,数组访问模式等。
- JIT编译状态: 对热代码的基础JIT或优化JIT(DFG/FTL)编译版本的引用。
- 运行时对象: 指向实际JavaScript对象、原型、作用域等的指针。
- 缓存昂贵的工作(解析和编译为未链接字节码)
- 仍收集运行时分析数据以指导优化
- 仍应用JIT优化基于实际执行模式
--bytecode --minify --sourcemap的组合为您提供最佳性能,同时保持可调试性。