diff --git a/docs/bundler/bytecode.mdx b/docs/bundler/bytecode.mdx
index 748d81eebd..12a9a67875 100644
--- a/docs/bundler/bytecode.mdx
+++ b/docs/bundler/bytecode.mdx
@@ -7,9 +7,9 @@ Bytecode caching is a build-time optimization that dramatically improves applica
## Usage
-### Basic usage
+### Basic usage (CommonJS)
-Enable bytecode caching with the `--bytecode` flag:
+Enable bytecode caching with the `--bytecode` flag. Without `--format`, this defaults to CommonJS:
```bash terminal icon="terminal"
bun build ./index.ts --target=bun --bytecode --outdir=./dist
@@ -17,7 +17,7 @@ bun build ./index.ts --target=bun --bytecode --outdir=./dist
This generates two files:
-- `dist/index.js` - Your bundled JavaScript
+- `dist/index.js` - Your bundled JavaScript (CommonJS)
- `dist/index.jsc` - The bytecode cache file
At runtime, Bun automatically detects and uses the `.jsc` file:
@@ -28,14 +28,24 @@ bun ./dist/index.js # Automatically uses index.jsc
### With standalone executables
-When creating executables with `--compile`, bytecode is embedded into the binary:
+When creating executables with `--compile`, bytecode is embedded into the binary. Both ESM and CommonJS formats are supported:
```bash terminal icon="terminal"
+# 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
+
+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.
+
### Combining with other optimizations
Bytecode works great with minification and source maps:
@@ -90,35 +100,9 @@ Larger applications benefit more because they have more code to parse.
- ❌ **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:
-
-```js
-// This prevents bytecode caching
-const data = await fetch("https://api.example.com");
-export default data;
-```
-
-**Why**: Top-level await requires async module evaluation, which can't be represented in CommonJS. The module graph becomes asynchronous, and the CommonJS wrapper function model breaks down.
-
-**Workaround**: Move async initialization into a function:
-
-```js
-async function init() {
- const data = await fetch("https://api.example.com");
- return data;
-}
-
-export default init;
-```
-
-Now the module exports a function that the consumer can await when needed.
-
### Version compatibility
Bytecode is **not portable across Bun versions**. The bytecode format is tied to JavaScriptCore's internal representation, which changes between versions.
@@ -236,8 +220,6 @@ It's normal for it it to log a cache miss multiple times since Bun doesn't curre
- Compressing `.jsc` files for network transfer (gzip/brotli)
- Evaluating if the startup performance gain is worth the size increase
-**Top-level await**: Not supported. Refactor to use async initialization functions.
-
## What is bytecode?
When you run JavaScript, the JavaScript engine doesn't execute your source code directly. Instead, it goes through several steps:
diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx
index f2f9cd3fb6..4b65ff5fdc 100644
--- a/docs/bundler/executables.mdx
+++ b/docs/bundler/executables.mdx
@@ -322,10 +322,7 @@ Using bytecode compilation, `tsc` starts 2x faster:
Bytecode compilation moves parsing overhead for large input files from runtime to bundle time. Your app starts faster, in exchange for making the `bun build` command a little slower. It doesn't obscure source code.
-
- **Experimental:** Bytecode compilation is an experimental feature. Only `cjs` format is supported (which means no
- top-level-await). Let us know if you run into any issues!
-
+Bytecode compilation supports both `cjs` and `esm` formats when used with `--compile`.
### What do these flags do?
diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx
index 46e4c8ce6d..cba9088253 100644
--- a/docs/bundler/index.mdx
+++ b/docs/bundler/index.mdx
@@ -1508,22 +1508,43 @@ BuildArtifact (entry-point) {
## Bytecode
-The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Only supported for `"cjs"` format, only supports `"target": "bun"` and dependent on a matching version of Bun. This adds a corresponding `.jsc` file for each entrypoint.
+The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Requires `"target": "bun"` and is dependent on a matching version of Bun.
+
+- **CommonJS**: Works with or without `compile: true`. Generates a `.jsc` file alongside each entrypoint.
+- **ESM**: Requires `compile: true`. Bytecode and module metadata are embedded in the standalone executable.
+
+Without an explicit `format`, bytecode defaults to CommonJS.
```ts title="build.ts" icon="/icons/typescript.svg"
+ // CommonJS bytecode (generates .jsc files)
await Bun.build({
entrypoints: ["./index.tsx"],
outdir: "./out",
bytecode: true,
})
+
+ // ESM bytecode (requires compile)
+ await Bun.build({
+ entrypoints: ["./index.tsx"],
+ outfile: "./mycli",
+ bytecode: true,
+ format: "esm",
+ compile: true,
+ })
```
+
```bash terminal icon="terminal"
+ # CommonJS bytecode
bun build ./index.tsx --outdir ./out --bytecode
+
+ # ESM bytecode (requires --compile)
+ bun build ./index.tsx --outfile ./mycli --bytecode --format=esm --compile
```
+
@@ -1690,7 +1711,10 @@ interface BuildConfig {
* start times, but will make the final output larger and slightly increase
* memory usage.
*
- * Bytecode is currently only supported for CommonJS (`format: "cjs"`).
+ * - CommonJS: works with or without `compile: true`
+ * - ESM: requires `compile: true`
+ *
+ * Without an explicit `format`, defaults to CommonJS.
*
* Must be `target: "bun"`
* @default false
diff --git a/docs/snippets/cli/build.mdx b/docs/snippets/cli/build.mdx
index 07999eae33..ec3a9d7490 100644
--- a/docs/snippets/cli/build.mdx
+++ b/docs/snippets/cli/build.mdx
@@ -50,7 +50,8 @@ bun build
- Module format of the output bundle. One of esm, cjs, or iife
+ Module format of the output bundle. One of esm, cjs, or iife. Defaults to{" "}
+ cjs when --bytecode is used.
### File Naming
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index 80a7ec66f7..a3db6771cc 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -2594,7 +2594,10 @@ declare module "bun" {
* start times, but will make the final output larger and slightly increase
* memory usage.
*
- * Bytecode is currently only supported for CommonJS (`format: "cjs"`).
+ * - CommonJS: works with or without `compile: true`
+ * - ESM: requires `compile: true`
+ *
+ * Without an explicit `format`, defaults to CommonJS.
*
* Must be `target: "bun"`
* @default false
diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig
index 9de645a85c..3d652f3b45 100644
--- a/src/bun.js/api/JSBundler.zig
+++ b/src/bun.js/api/JSBundler.zig
@@ -1019,6 +1019,13 @@ pub const JSBundler = struct {
}
}
+ // ESM bytecode requires compile because module_info (import/export metadata)
+ // is only available in compiled binaries. Without it, JSC must parse the file
+ // twice (once for module analysis, once for bytecode), which is a deopt.
+ if (this.bytecode and this.format == .esm and this.compile == null) {
+ return globalThis.throwInvalidArguments("ESM bytecode requires compile: true. Use format: 'cjs' for bytecode without compile.", .{});
+ }
+
return this;
}
diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig
index 2a9c84c880..5c7ad1c2e8 100644
--- a/src/cli/Arguments.zig
+++ b/src/cli/Arguments.zig
@@ -179,7 +179,7 @@ pub const build_only_params = [_]ParamType{
clap.parseParam("--sourcemap ? Build with sourcemaps - 'linked', 'inline', 'external', or 'none'") catch unreachable,
clap.parseParam("--banner Add a banner to the bundled output such as \"use client\"; for a bundle being used with RSCs") catch unreachable,
clap.parseParam("--footer Add a footer to the bundled output such as // built with bun!") catch unreachable,
- clap.parseParam("--format Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\".") catch unreachable,
+ clap.parseParam("--format Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\", or \"cjs\" with --bytecode.") catch unreachable,
clap.parseParam("--root Root directory used for multiple entry points") catch unreachable,
clap.parseParam("--splitting Enable code splitting") catch unreachable,
clap.parseParam("--public-path A prefix to be appended to any import paths in bundled code") catch unreachable,
@@ -1346,10 +1346,18 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
ctx.bundler_options.output_format = format;
- // ESM bytecode is supported for --compile builds (module_info is embedded in binary)
- if (format != .cjs and format != .esm and ctx.bundler_options.bytecode) {
- Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{});
- Global.exit(1);
+ if (ctx.bundler_options.bytecode) {
+ if (format != .cjs and format != .esm) {
+ Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{});
+ Global.exit(1);
+ }
+ // ESM bytecode requires --compile because module_info (import/export metadata)
+ // is only available in compiled binaries. Without it, JSC must parse the file
+ // twice (once for module analysis, once for bytecode), which is a deopt.
+ if (format == .esm and !ctx.bundler_options.compile) {
+ Output.errGeneric("ESM bytecode requires --compile. Use --format=cjs for bytecode without --compile.", .{});
+ Global.exit(1);
+ }
}
}