skills/bun-bundler-bytecode/SKILL.md
Speed up JavaScript execution with bytecode caching in Bun's bundler
npx skillsauth add jarle/bun-skills Bun Bytecode CachingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Speed up JavaScript execution with bytecode caching in Bun's bundler
Bytecode caching is a build-time optimization that dramatically improves application startup time by pre-compiling your JavaScript to bytecode. For example, when compiling TypeScript's tsc with bytecode enabled, startup time improves by 2x.
Enable bytecode caching with the --bytecode flag. Without --format, this defaults to CommonJS:
bun build ./index.ts --target=bun --bytecode --outdir=./dist
This generates two files:
dist/index.js - Your bundled JavaScript (CommonJS)dist/index.jsc - The bytecode cache fileAt runtime, Bun automatically detects and uses the .jsc file:
bun ./dist/index.js # Automatically uses index.jsc
When creating executables with --compile, bytecode is embedded into the binary. Both ESM and CommonJS formats are supported:
# ESM (requires --compile)
bun build ./cli.ts --compile --bytecode --format=esm --outfile=mycli
# CommonJS (works with or without --compile)
bun build ./cli.ts --compile --bytecode --outfile=mycli
The resulting executable contains both the code and bytecode, giving you maximum performance in a single file.
ESM bytecode requires --compile because Bun embeds module metadata (import/export information) in the compiled binary. This metadata allows the JavaScript engine to skip parsing entirely at runtime.
Without --compile, ESM bytecode would still require parsing the source to analyze module dependencies—defeating the purpose of bytecode caching.
Bytecode works great with minification and source maps:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli
--minify reduces code size before generating bytecode (less code -> less bytecode)--sourcemap preserves error reporting (errors still point to original source)--bytecode eliminates parsing overheadThe 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 |
Larger applications benefit more because they have more code to parse.
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:
# After updating Bun
bun build --bytecode ./index.ts --outdir=./dist
If bytecode doesn't match the current Bun version, it's automatically ignored and your code falls back to parsing the JavaScript source. Your app still runs - you just lose the performance optimization.
Best practice: Generate bytecode as part of your CI/CD build process. Don't commit .jsc files to git. Regenerate them whenever you update Bun.
.js file (your bundled source code).jsc file (the bytecode cache)At runtime:
.js file, sees a @bytecode pragma, and checks the .jsc file.jsc fileBytecode does not obscure your source code. It's an optimization, not a security measure.
Include bytecode generation in your Dockerfile:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]
The bytecode is architecture-independent.
Generate bytecode during your build pipeline:
# GitHub Actions
- name: Build with bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.ts
Check that the .jsc file exists:
ls -lh dist/
-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jsc
The .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:
[Disk cache] cache hit for sourceCode
If you see a cache miss, it will log something like:
[Disk cache] cache miss for sourceCode
It's normal for it it to log a cache miss multiple times since Bun doesn't currently bytecode cache JavaScript code used in builtin modules.
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:
--minify to reduce code size before bytecode generation.jsc files for network transfer (gzip/brotli)When you run JavaScript, the JavaScript engine doesn't execute your source code directly. Instead, it goes through several steps:
Bytecode is an intermediate representation - it's lower-level than JavaScript source code, but higher-level than machine code. Think of it as assembly language for a virtual machine. Each bytecode instruction represents a single operation like "load this variable," "add two numbers," or "call this function."
This happens every single time you run your code. If you have a CLI tool that runs 100 times a day, your code gets parsed 100 times. If you have a serverless function with frequent cold starts, parsing happens on every cold start.
With bytecode caching, Bun moves steps 1 and 2 to the build step. At runtime, the engine loads the pre-compiled bytecode and jumps straight to execution.
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:
// Without bytecode caching:
function rarely_used() {
// This 500-line function is only parsed
// when it's actually called
}
function main() {
console.log("Starting app");
// rarely_used() is never called, so it's never parsed
}
This means parsing overhead isn't just a startup cost - it happens throughout your application's lifetime as different code paths execute. With bytecode caching, all functions are pre-compiled, even the ones that are lazily parsed. The parsing work happens once at build time instead of being distributed throughout your application's execution.
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):
SourceCodeKey (validates bytecode matches source):
Bytecode instructions:
Constants and identifiers:
Function metadata (for each function in your code):
thisRegister, scopeRegister, numVars, numCalleeLocals, numParameters.super? does it have tail calls? These affect how the function is executed.Nested structures:
Importantly, bytecode does not embed your source code. Instead:
.js file)This is why you need to deploy both the .js and .jsc files. The .jsc file is useless without its corresponding .js file.
Bytecode files are significantly larger than source code - typically 2-8x larger.
Bytecode instructions are verbose: A single line of minified JavaScript might compile to dozens of bytecode instructions. For example:
const sum = arr.reduce((a, b) => a + b, 0);
Compiles to bytecode that:
arr variablereduce property0sumEach of these steps is a separate bytecode instruction with its own metadata.
Constant pools store everything:
Every string literal, number, property name - everything gets stored in the constant pool. Even if your source code has "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:
A file with 1,000 small functions has 1,000 sets of metadata.
Profiling data structures: Even though profiling data isn't populated yet, the structures to hold profiling data are allocated. This includes:
These take up space even when empty.
Pre-computed control flow: Jump targets, switch tables, and exception handler boundaries are all pre-computed and stored. This makes execution faster but increases file size.
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:
The tradeoff: You're trading 2-4x larger files for 2-4x faster startup. For CLIs, this is usually worth it. For long-running servers where a few megabytes of disk space don't matter, it's even less of an issue.
Bytecode is architecture-independent. You can:
The bytecode contains abstract instructions that work on any architecture. Architecture-specific optimizations happen during JIT compilation at runtime, not in the cached bytecode.
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:
.jsc file.js source codeYour application still runs - you just lose the performance optimization.
Graceful degradation: This design means bytecode caching "fails open" - if anything goes wrong (version mismatch, corrupted file, missing file), your code still runs normally. You might see slower startup, but you won't see errors.
JavaScriptCore makes a crucial distinction between "unlinked" and "linked" bytecode. This separation is what makes bytecode caching possible:
The bytecode saved in .jsc files is unlinked bytecode. It contains:
But it doesn't contain:
Unlinked bytecode is immutable and shareable. Multiple executions of the same code can all reference the same unlinked bytecode.
When Bun runs bytecode, it "links" it - creating a runtime wrapper that adds:
This linked representation is created fresh every time you run your code. This allows:
Bytecode caching moves expensive work (parsing and compiling to bytecode) from runtime to build time. For applications that start frequently, this can halve your startup time at the cost of larger files on disk.
For production CLIs and serverless deployments, the combination of --bytecode --minify --sourcemap gives you the best performance while maintaining debuggability.
development
Using TypeScript with Bun, including type definitions and compiler options
development
Learn how to write tests using Bun's Jest-compatible API with support for async tests, timeouts, and various test modifiers
testing
Learn how to use snapshot testing in Bun to save and compare output between test runs
testing
Learn about Bun test's runtime integration, environment variables, timeouts, and error handling