tsc with bytecode enabled, startup time improves by 2x.
Usage
Basic usage
Enable bytecode caching with the--bytecode flag:
dist/index.js- Your bundled JavaScriptdist/index.jsc- The bytecode cache file
.jsc file:
With standalone executables
When creating executables with--compile, bytecode is embedded into the binary:
Combining with other optimizations
Bytecode works great with minification and source maps:--minifyreduces code size before generating bytecode (less code -> less bytecode)--sourcemappreserves error reporting (errors still point to original source)--bytecodeeliminates parsing overhead
Performance impact
The performance improvement scales with your codebase size:| Application size | Typical startup improvement |
|---|---|
| Small CLI (< 100 KB) | 1.5-2x faster |
| Medium-large app (> 5 MB) | 2.5x-4x faster |
When to use bytecode
Great for:
CLI tools
- Invoked frequently (linters, formatters, git hooks)
- Startup time is the entire user experience
- Users notice the difference between 90ms and 45ms startup
- Example: TypeScript compiler, Prettier, ESLint
Build tools and task runners
- Run hundreds or thousands of times during development
- Milliseconds saved per run compound quickly
- Developer experience improvement
- Example: Build scripts, test runners, code generators
Standalone executables
- Distributed to users who care about snappy performance
- Single-file distribution is convenient
- File size less important than startup time
- Example: CLIs distributed via npm or as binaries
Skip it for:
- ❌ Small scripts
- ❌ Code that runs once
- ❌ Development builds
- ❌ Size-constrained environments
- ❌ Code with top-level await (not supported)
Limitations
CommonJS only
Bytecode caching currently works with CommonJS output format. Bun’s bundler automatically converts most ESM code to CommonJS, but top-level await is the exception:Version compatibility
Bytecode is not portable across Bun versions. The bytecode format is tied to JavaScriptCore’s internal representation, which changes between versions. When you update Bun, you must regenerate bytecode:.jsc files to git. Regenerate them whenever you update Bun.
Source code still required
- The
.jsfile (your bundled source code) - The
.jscfile (the bytecode cache)
- Bun loads the
.jsfile, sees a@bytecodepragma, and checks the.jscfile - Bun loads the
.jscfile - Bun validates the bytecode hash matches the source
- If valid, Bun uses the bytecode
- If invalid, Bun falls back to parsing the source
Bytecode is not obfuscation
Bytecode does not obscure your source code. It’s an optimization, not a security measure.Production deployment
Docker
Include bytecode generation in your Dockerfile:CI/CD
Generate bytecode during your build pipeline:Debugging
Verify bytecode is being used
Check that the.jsc file exists:
.jsc file should be 2-8x larger than the .js file.
To log if bytecode is being used, set BUN_JSC_verboseDiskCache=1 in your environment.
On success, it will log something like:
Common issues
Bytecode silently ignored: Usually caused by a Bun version update. The cache version doesn’t match, so bytecode is rejected. Regenerate to fix. File size too large: This is expected. Consider:- Using
--minifyto reduce code size before bytecode generation - Compressing
.jscfiles for network transfer (gzip/brotli) - Evaluating if the startup performance gain is worth the size increase
What is bytecode?
When you run JavaScript, the JavaScript engine doesn’t execute your source code directly. Instead, it goes through several steps:- Parsing: The engine reads your JavaScript source code and converts it into an Abstract Syntax Tree (AST)
- Bytecode compilation: The AST is compiled into bytecode - a lower-level representation that’s faster to execute
- Execution: The bytecode is executed by the engine’s interpreter or JIT compiler
Why lazy parsing makes this even better
Modern JavaScript engines use a clever optimization called lazy parsing. They don’t parse all your code upfront - instead, functions are only parsed when they’re first called:The bytecode format
Inside a .jsc file
A.jsc file contains a serialized bytecode structure. Understanding what’s inside helps explain both the performance benefits and the file size tradeoff.
Header section (validated on every load):
- Cache version: A hash tied to the JavaScriptCore framework version. This ensures bytecode generated with one version of Bun only runs with that exact version.
- Code block type tag: Identifies whether this is a Program, Module, Eval, or Function code block.
- Source code hash: A hash of the original JavaScript source code. Bun verifies this matches before using the bytecode.
- Source code length: The exact length of the source, for additional validation.
- Compilation flags: Critical compilation context like strict mode, whether it’s a script vs module, eval context type, etc. The same source code compiled with different flags produces different bytecode.
- Instruction stream: The actual bytecode opcodes - the compiled representation of your JavaScript. This is a variable-length sequence of bytecode instructions.
- Metadata table: Each opcode has associated metadata - things like profiling counters, type hints, and execution counts (even if not yet populated).
- Jump targets: Pre-computed addresses for control flow (if/else, loops, switch statements).
- Switch tables: Optimized lookup tables for switch statements.
- Constant pool: All literal values in your code - numbers, strings, booleans, null, undefined. These are stored as actual JavaScript values (JSValues) so they don’t need to be parsed from source at runtime.
- Identifier table: All variable and function names used in the code. Stored as deduplicated strings.
- Source code representation markers: Flags indicating how constants should be represented (as integers, doubles, big ints, etc.).
- Register allocation: How many registers (local variables) the function needs -
thisRegister,scopeRegister,numVars,numCalleeLocals,numParameters. - Code features: A bitmask of function characteristics: is it a constructor? an arrow function? does it use
super? does it have tail calls? These affect how the function is executed. - Lexically scoped features: Strict mode and other lexical context.
- Parse mode: The mode in which the function was parsed (normal, async, generator, async generator).
- Function declarations and expressions: Each nested function gets its own bytecode block, recursively. A file with 100 functions has 100 separate bytecode blocks, all nested in the structure.
- Exception handlers: Try/catch/finally blocks with their boundaries and handler addresses pre-computed.
- Expression info: Maps bytecode positions back to source code locations for error reporting and debugging.
What bytecode does NOT contain
Importantly, bytecode does not embed your source code. Instead:- The JavaScript source is stored separately (in the
.jsfile) - The bytecode only stores a hash and length of the source
- At load time, Bun validates the bytecode matches the current source code
.js and .jsc files. The .jsc file is useless without its corresponding .js file.
The tradeoff: file size
Bytecode files are significantly larger than source code - typically 2-8x larger.Why is bytecode so much larger?
Bytecode instructions are verbose: A single line of minified JavaScript might compile to dozens of bytecode instructions. For example:- Loads the
arrvariable - Gets the
reduceproperty - Creates the arrow function (which itself has bytecode)
- Loads the initial value
0 - Sets up the call with the right number of arguments
- Actually performs the call
- Stores the result in
sum
"hello" a hundred times, the constant pool stores it once, but the identifier table and constant references add overhead.
Per-function metadata:
Each function - even small one-line functions - gets its own complete metadata:
- Register allocation info
- Code features bitmask
- Parse mode
- Exception handlers
- Expression info for debugging
- Value profile slots (tracking what types flow through each operation)
- Array profile slots (tracking array access patterns)
- Binary arithmetic profile slots (tracking number types in math operations)
- Unary arithmetic profile slots
Mitigation strategies
Compression: Bytecode compresses extremely well with gzip/brotli (60-70% compression). The repetitive structure and metadata compress efficiently. Minification first: Using--minify before bytecode generation helps:
- Shorter identifiers → smaller identifier table
- Dead code elimination → less bytecode generated
- Constant folding → fewer constants in the pool
Versioning and portability
Cross-architecture portability: ✅
Bytecode is architecture-independent. You can:- Build on macOS ARM64, deploy to Linux x64
- Build on Linux x64, deploy to AWS Lambda ARM64
- Build on Windows x64, deploy to macOS ARM64
Cross-version portability: ❌
Bytecode is not stable across Bun versions. Here’s why: Bytecode format changes: JavaScriptCore’s bytecode format evolves. New opcodes get added, old ones get removed or changed, metadata structures change. Each version of JavaScriptCore has a different bytecode format. Version validation: The cache version in the.jsc file header is a hash of the JavaScriptCore framework. When Bun loads bytecode:
- It extracts the cache version from the
.jscfile - It computes the current JavaScriptCore version
- If they don’t match, the bytecode is silently rejected
- Bun falls back to parsing the
.jssource code
Unlinked vs. linked bytecode
JavaScriptCore makes a crucial distinction between “unlinked” and “linked” bytecode. This separation is what makes bytecode caching possible:Unlinked bytecode (what’s cached)
The bytecode saved in.jsc files is unlinked bytecode. It contains:
- The compiled bytecode instructions
- Structural information about the code
- Constants and identifiers
- Control flow information
- Pointers to actual runtime objects
- JIT-compiled machine code
- Profiling data from previous runs
- Call link information (which functions call which)
Linked bytecode (runtime execution)
When Bun runs bytecode, it “links” it - creating a runtime wrapper that adds:- Call link information: As your code runs, the engine learns which functions call which and optimizes those call sites.
- Profiling data: The engine tracks how many times each instruction executes, what types of values flow through the code, array access patterns, etc.
- JIT compilation state: References to baseline JIT or optimizing JIT (DFG/FTL) compiled versions of hot code.
- Runtime objects: Pointers to actual JavaScript objects, prototypes, scopes, etc.
- Caching the expensive work (parsing and compilation to unlinked bytecode)
- Still collecting runtime profiling data to guide optimizations
- Still applying JIT optimizations based on actual execution patterns
--bytecode --minify --sourcemap gives you the best performance while maintaining debuggability.