| | |

Bun 12 ⚡ Bundling Applications for Production with bun build

⏱️ Estimated read: 10 min | |

Bun’s bundler is not a separate tool. It is a component of the runtime, exposed through the bun build command and the Bun.build() JavaScript API. The bundler reads TypeScript, JSX, and CSS natively, resolves imports using Bun’s own resolver, and produces optimized output for a specified target. The LFCS exam does not test Bun, but the Bun Certified Associate exam does. The conceptual model is the same as any bundler: an entrypoint, a dependency graph, a target environment, and an output format. The difference is that Bun’s bundler is fast enough to run on every save, and it integrates directly with the runtime that will execute the bundle.

Key point: The target determines the execution environment. target: "browser" is the default and prioritizes the "browser" export condition. target: "bun" is for server-side code and marks the bundle with a // @bun pragma so the runtime skips re-transpilation. target: "node" prioritizes the "node" export condition and does not polyfill Bun-specific globals .


Why bun build exists

The startup problem. A Bun server can run TypeScript directly without bundling. The runtime transpiles on the fly, and the startup cost is paid once. For a long-running server, this is acceptable. For a CLI tool or a serverless function that starts and stops frequently, the transpilation cost is paid on every invocation. Bundling moves the transpilation to build time. The --bytecode flag goes further, pre-compiling the JavaScript to JavaScriptCore bytecode so the runtime skips parsing entirely . The documented improvement is 1.5x to 4x faster startup depending on application size.

The deployment problem. A Node.js application deployed to a container includes node_modules, the package.json, the lockfile, and the source files. The image is large, and the startup is slow. bun build --compile produces a single-file executable that contains the runtime and the application code. The executable has no external dependencies. It can be copied to a server without Node.js or Bun installed . The --compile flag is the deployment mechanism for Bun applications.

The minification problem. A development build is readable. A production build should be small. The --minify flag enables whitespace removal, syntax optimization, and identifier shortening. The --production flag enables all three and sets process.env.NODE_ENV to "production" . The --keep-names flag preserves function and class names for readable stack traces while still shortening identifiers .

The target problem. A bundle for the browser must not include Node.js built-ins like fs. A bundle for Bun can use bun:sqlite and Bun.serve. A bundle for Node must not include Bun-specific globals. The target option applies the correct module resolution rules for the intended environment . Without it, the bundle may include code that fails at runtime.

The trade-off. Bundling adds a build step. The source is no longer the deployed artifact; the bundle is. This means the bundle must be regenerated when the source changes, and the build pipeline must be configured correctly. The benefit is faster startup, smaller deployment artifacts, and the ability to use features (like bytecode) that are only available at build time.


a. The basic bundle: CLI and JavaScript API

The bun build command takes an entrypoint and an output directory. The simplest invocation produces a bundle in the default format (ESM) for the default target (browser).

# Basic bundle
bun build ./index.tsx --outdir ./out

# Bundle for Bun runtime
bun build ./server.ts --outdir ./out --target bun

# Bundle for Node.js
bun build ./cli.ts --outdir ./out --target node --format cjs

The JavaScript API is the programmatic equivalent. The Bun.build() function returns a BuildArtifact object that contains the output files, the success status, and any logs.

const result = await Bun.build({
  entrypoints: ["./index.tsx"],
  outdir: "./out",
  target: "browser",
});

if (!result.success) {
  console.error(result.logs);
}

The entrypoints array can contain multiple files. When multiple entrypoints import the same module, the splitting option extracts the shared code into a separate chunk. The chunk filename includes a content hash to avoid collisions .


b. Minification, bytecode, and source maps

The --minify flag enables all three minification modes: whitespace, syntax, and identifiers. The modes can be enabled individually for finer control.

# Full minification
bun build ./index.ts --outdir ./out --minify

# Production mode (minify + NODE_ENV)
bun build ./index.ts --outdir ./out --production

# Keep function names for stack traces
bun build ./index.ts --outdir ./out --minify --keep-names

The --bytecode flag pre-compiles the JavaScript to JavaScriptCore bytecode. This is the optimization for startup time. The bytecode is not portable across Bun versions; it must be regenerated after a Bun upgrade .

# Minify, generate source maps, and pre-compile
bun build ./index.ts --outdir ./out --minify --sourcemap=linked --bytecode --target=bun

The --sourcemap option generates source maps. The linked value produces an external .map file and adds a //# sourceMappingURL comment. The inline value embeds the map in the bundle. The external value produces a separate map without the comment .

The --drop flag removes calls to specific functions or statements. The --drop=console removes all console.* calls. The --drop=debugger removes debugger statements. This is useful for production builds where logging is handled by a different mechanism .


c. Compiling to a single-file executable

The --compile flag produces a standalone executable. The output is a binary that contains the Bun runtime and the application code. It runs on the target platform without Bun installed.

# Compile to a single executable
bun build --compile ./cli.ts --outfile ./mycli

# Compile with embedded runtime arguments
bun build --compile --compile-exec-argv="--smol" ./app.ts --outfile ./myapp

The executable reads the BUN_OPTIONS environment variable at runtime, so flags like --smol or --cpu-prof can be passed without recompiling . The --compile-autoload-tsconfig and --compile-autoload-package-json flags enable runtime loading of those files, which is disabled by default in compiled executables .

The experimental --aot flag (or bun compile) performs ahead-of-time compilation to machine code. The executable has no JIT and no bytecode, so it starts faster and uses less memory. This is the optimization for production CLIs where startup time is critical .


Complete Example Session

# ============================================
# PART 1: BASIC BUNDLE
# ============================================

bun build ./index.ts --outdir ./dist
# Produces ./dist/index.js in ESM format for browsers.
# ============================================
# PART 2: TARGET BUN
# ============================================

bun build ./server.ts --outdir ./dist --target bun
# Produces a bundle with the // @bun pragma.
# The runtime skips re-transpilation.
# ============================================
# PART 3: TARGET NODE
# ============================================

bun build ./cli.ts --outdir ./dist --target node --format cjs
# Produces a CommonJS bundle for Node.js.
# ============================================
# PART 4: PRODUCTION BUILD
# ============================================

bun build ./index.ts --outdir ./dist --production
# Minifies all modes, sets NODE_ENV=production.
# ============================================
# PART 5: BYTECODE
# ============================================

bun build ./server.ts --outdir ./dist --target bun --bytecode
# Produces index.js and index.jsc.
# The runtime uses the .jsc file automatically.
# ============================================
# PART 6: DROP CONSOLE
# ============================================

bun build ./index.ts --outdir ./dist --production --drop=console
# Removes all console.* calls from the bundle.
# ============================================
# PART 7: SINGLE-FILE EXECUTABLE
# ============================================

bun build --compile ./cli.ts --outfile ./mycli
# ./mycli is a standalone binary.
./mycli
# Runs without Bun installed.
# ============================================
# PART 8: JAVASCRIPT API
# ============================================

const result = await Bun.build({
  entrypoints: ["./index.ts"],
  outdir: "./dist",
  target: "browser",
  minify: true,
});
if (!result.success) console.error(result.logs);
# ============================================
# PART 9: VERIFY OUTPUT
# ============================================

ls -la ./dist/
# index.js (minified)
# index.js.map (linked source map)
# index.jsc (bytecode, if --bytecode)

The nine parts trace the build lifecycle: basic bundle, target selection, format selection, production minification, bytecode, drop statements, single-file compilation, JavaScript API, and output verification.


Quick Reference

Targets

TargetDefault FormatExport ConditionUse Case
browseresmbrowserWeb applications
bunesmbunBun servers, full-stack
nodeesmnodeNode.js CLIs, libraries

Formats

FormatFlagNotes
ESM--format esmDefault, supports top-level await
CJS--format cjsCommonJS, target changes to node
IIFE--format iifeBrowser global, no export exposure

Build Flags

FlagPurpose
--outdirOutput directory
--targetExecution environment
--formatModule format
--minifyAll minification modes
--productionMinify + NODE_ENV
--bytecodePre-compile to JavaScriptCore bytecode
--sourcemap=linkedExternal source map
--drop=consoleRemove console calls
--keep-namesPreserve function names
--compileSingle-file executable
--splittingCode splitting for multiple entrypoints

Best Practices

✅ Do This:

bun build ./server.ts --outdir ./dist --target bun --production
<!-- Target the runtime, minify for production -->     # ✅
bun build ./cli.ts --outfile ./cli --compile
<!-- Compile for single-file deployment -->           # ✅
bun build ./index.ts --outdir ./dist --target bun --bytecode
<!-- Use bytecode for faster startup -->              # ✅

❌ Don’t Do This:

# Bundle for browser with Bun-specific globals
<!-- Bun.serve is not available in the browser -->    # ❌
# Skip source maps in production
<!-- Error traces become unreadable -->               # ❌
# Assume bytecode survives Bun upgrades
<!-- Regenerate after each Bun version -->            # ❌

Common Pitfalls

PitfallWhy It HappensFix
Bundle fails at runtimeWrong targetMatch target to execution environment
Bun globals undefinedtarget: "node" usedUse target: "bun" for Bun APIs
Startup not fasterNo --bytecodeAdd --bytecode for Bun target
Stack traces unreadableNo --keep-names with minifyAdd --keep-names
Bytecode mismatchBun version changedRebuild with current Bun version

Real-World Examples

1. Browser Bundle

bun build ./app.tsx --outdir ./dist

2. Bun Server Bundle

bun build ./server.ts --outdir ./dist --target bun

3. Node CLI Bundle

bun build ./cli.ts --outdir ./dist --target node --format cjs

4. Production Minify

bun build ./index.ts --outdir ./dist --production

5. Bytecode Startup

bun build ./server.ts --outdir ./dist --target bun --bytecode

6. Single Executable

bun build --compile ./cli.ts --outfile ./mycli

7. Drop Console

bun build ./index.ts --outdir ./dist --production --drop=console

8. Code Splitting

bun build ./entry-a.ts ./entry-b.ts --outdir ./dist --splitting

9. JavaScript API

await Bun.build({ entrypoints: ["./app.ts"], outdir: "./dist", target: "bun" });

10. External Package

bun build ./index.ts --outdir ./dist --external zod

Visual

Build Target Selection

┌──────────────────────────────────────────────┐
│  Where will the bundle run?                  │
└──────────┬───────────────────────┬───────────┘
           │ BROWSER               │ SERVER
           ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  target: "browser"   │  │  Bun or Node?        │
│  (default)           │  └──────────┬───────────┘
│  export: "browser"   │             │
└──────────────────────┘    ┌────────┴────────┐
                            │ BUN             │ NODE
                            ▼                 ▼
                  ┌───────────────┐  ┌───────────────┐
                  │ target: "bun" │  │ target: "node"│
                  │ // @bun pragma│  │ export: "node"│
                  └───────────────┘  └───────────────┘

Compilation Outputs

┌──────────────────────────────────────────────┐
│  bun build ./app.ts --outdir ./dist          │
│  → dist/app.js (transpiled bundle)           │
└──────────────────┬───────────────────────────┘
                   │
                   │ --bytecode
                   ▼
┌──────────────────────────────────────────────┐
│  dist/app.js (transpiled)                    │
│  dist/app.jsc (bytecode, faster startup)     │
└──────────────────┬───────────────────────────┘
                   │
                   │ --compile
                   ▼
┌──────────────────────────────────────────────┐
│  ./myapp (standalone executable)             │
│  Contains Bun runtime + application code     │
└──────────────────────────────────────────────┘

Summary

ItemValue
Bundler commandbun build <entrypoint>
JavaScript APIBun.build({ ... })
Default targetbrowser
Bun target--target bun (adds // @bun)
Node target--target node
Default formatesm
CommonJS format--format cjs
Minification--minify or --production
Bytecode--bytecode (Bun target)
Single executable--compile
Source maps--sourcemap=linked
Drop statements--drop=console

Key takeaways:

  • The target determines the runtime. browser, bun, and node apply different module resolution rules and expose different globals. A bundle built for the wrong target fails at runtime .
  • --production is the production build flag. It enables all minification modes and sets NODE_ENV=production. --minify enables the same modes without the environment variable .
  • --bytecode is the startup optimization. It pre-compiles to JavaScriptCore bytecode, moving parsing out of the critical path. The bytecode is version-specific and must be regenerated after Bun upgrades .
  • --compile produces a single-file executable. The binary contains the Bun runtime and the application. It runs without Bun installed .
  • Source maps and --keep-names preserve debuggability. A minified bundle without source maps produces unreadable error traces. The --sourcemap=linked flag generates the map, and --keep-names preserves function names .

Remember: bun build is the bundler built into the runtime. It reads TypeScript and JSX natively, resolves imports using Bun’s resolver, and produces output for a specified target. The target determines what the bundle can access. The format determines how it is loaded. The minification determines how small it is. The bytecode determines how fast it starts. The compilation determines whether it needs Bun installed. Each flag is a trade-off between size, speed, and debuggability.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!