# How to Configure Bun's Bundler for Production Builds with Tree-Shaking

> Learn to configure Bun's bundler for production builds using tree-shaking. Enable dead-code elimination for smaller, faster code in your next project.

- Repository: [Bun/bun](https://github.com/oven-sh/bun)
- Tags: how-to-guide
- Published: 2026-02-28

---

**Set `minify: true` (or a granular minify object) in your `Bun.build` configuration to enable dead-code elimination, which automatically activates tree-shaking by marking unused symbols for removal in the Zig-based linker.**

To configure Bun's bundler for production builds with tree-shaking, you use the **`Bun.build`** API or the `bun build` CLI exposed by the **oven-sh/bun** repository. The production pipeline translates JavaScript configuration options into internal Zig structures that analyze the module graph, compute cross-chunk dependencies, and eliminate dead code before final minification.

## Understanding Bun's Tree-Shaking Architecture

Bun's bundler implements tree-shaking through a multi-phase Zig pipeline defined in several key source files. When you invoke `Bun.build`, the TypeScript **`BuildConfig`** interface in [[`src/js/builtins/BundlerPlugin.ts`](https://github.com/oven-sh/bun/blob/main/src/js/builtins/BundlerPlugin.ts)](https://github.com/oven-sh/bun/blob/main/src/js/builtins/BundlerPlugin.ts) validates your options and passes them to the native layer.

The internal **`LinkerContext.Options`** struct in [`src/bundler/LinkerContext.zig`](https://github.com/oven-sh/bun/blob/main/src/bundler/LinkerContext.zig) stores flags like `emit_dce_annotations` and `ignore_dce_annotations`. According to the source in [`src/bundler/bundle_v2.zig`](https://github.com/oven-sh/bun/blob/main/src/bundler/bundle_v2.zig) (lines 972–1003), these flags control whether the linker merely annotates dead code or actually removes it.

The tree-shaking execution flow follows three stages:

1. **`computeCrossChunkDependencies.zig`** – Calculates which symbols are reachable across all chunks to establish the live code boundary.
2. **`postProcessJSChunk.zig`** – Uses `print_dce_annotations` to mark symbols with no import/export references for elimination.
3. **`renameSymbolsInChunk.zig`** – Renames identifiers only after dead-code removal completes, ensuring minification does not affect the DCE analysis.

## Essential Production Configuration Options

### Enabling Dead-Code Elimination

The **`minify`** option controls tree-shaking aggressiveness. Setting `minify: true` enables all minification steps, but you can fine-tune behavior using an object:

```typescript
await Bun.build({
  entrypoints: ["src/index.ts"],
  target: "browser",
  minify: {
    syntax: true,       // Removes unreachable statements and dead branches
    identifiers: true,    // Renames variables after DCE completes
    whitespace: true,   // Strips unnecessary characters
  },
  outdir: "dist",
});

```

By default, **`emit_dce_annotations`** is `true` and **`ignore_dce_annotations`** is `false`, meaning the linker actively drops unused exports. These boolean flags map directly to fields in `LinkerContext.Options` within the Zig source.

### Preserving Public API Surface

When building libraries, prevent identifier renaming while maintaining tree-shaking by setting **`keep_names: true`** inside the minify object:

```typescript
minify: {
  syntax: true,
  identifiers: true,
  keep_names: true,  // Prevents renaming of exported symbols
}

```

Alternatively, add a dummy reference to force the linker to treat an export as live code:

```typescript
export const publicAPI = () => { /* ... */ };
export const __keep = publicAPI;  // Reference prevents DCE removal

```

### Targeting Specific Runtimes

The **`target`** option (`"browser"`, `"node"`, or `"bun"`) determines which built-ins and polyfills are included in the bundle. Tree-shaking operates on the resulting AST after target-specific transformations, ensuring platform-specific dead code is also eliminated.

### Experimental Asset Optimization

Enable **`experimentalCss: true`** and **`experimentalHtml: true`** to extend tree-shaking to stylesheets and markup. Unused CSS rules are stripped during the linking phase, and HTML entry points are minified alongside JavaScript.

## Production-Ready Implementation Patterns

### Full TypeScript Build Configuration

Create a dedicated build script that exposes all production optimizations:

```typescript
// build-prod.ts
import { Bun } from "bun";

const result = await Bun.build({
  entrypoints: ["src/index.ts"],
  target: "browser",
  minify: {
    syntax: true,
    identifiers: true,
    whitespace: true,
  },
  sourcemap: "external",
  outdir: "dist",
  experimentalHtml: true,
  experimentalCss: true,
});

if (!result.success) {
  console.error("Build failed:", result.errors);
  process.exit(1);
}

```

Execute the script with `bun run build-prod.ts`.

### CLI-Based Production Builds

For CI/CD pipelines or quick builds, use the `bun build` command with equivalent flags:

```bash
bun build src/index.ts \
  --target=browser \
  --minify \
  --outdir=dist \
  --sourcemap=external \
  --experimental-html \
  --experimental-css

```

Both methods set identical `LinkerContext.Options` in the underlying Zig bundler.

### Multiple Entry Points with Custom Naming

Configure separate chunks for different application sections while maintaining tree-shaking across shared dependencies:

```typescript
await Bun.build({
  entrypoints: {
    app: "src/app.ts",
    admin: "src/admin.ts",
  },
  target: "browser",
  minify: true,
  outdir: "dist",
  naming: "[name].bundle.js",  // Produces app.bundle.js and admin.bundle.js
});

```

## Advanced Tree-Shaking Control

### Excluding Libraries from DCE

Mark dependencies as **`external`** to prevent the linker from analyzing or tree-shaking them. This preserves side-effects in libraries that may not be statically analyzable:

```typescript
await Bun.build({
  entrypoints: ["src/index.ts"],
  external: ["lodash", "some-side-effect-lib"],
  minify: true,
});

```

### Bypassing Dead-Code Annotations

In rare cases where you need to force a module to remain in the graph despite appearing unused, set **`ignore_dce_annotations: true`** (exposed as `--ignore-dce-annotations` in the CLI). This instructs the linker in `postProcessJSChunk.zig` to retain all symbols regardless of reachability analysis.

### Granular Minification Without Renaming

To eliminate dead code while keeping original variable names for debugging:

```typescript
await Bun.build({
  entrypoints: ["src/index.ts"],
  minify: {
    syntax: true,      // Enable tree-shaking
    identifiers: false, // Disable renaming
    whitespace: true,
  },
});

```

## Summary

- **Enable tree-shaking** by setting `minify: true` or configuring specific `minify` sub-options (`syntax`, `identifiers`, `whitespace`) in your `Bun.build` call.
- **Configure native options** via the `BuildConfig` interface in [`src/js/builtins/BundlerPlugin.ts`](https://github.com/oven-sh/bun/blob/main/src/js/builtins/BundlerPlugin.ts), which maps to `LinkerContext.Options` in `src/bundler/LinkerContext.zig`.
- **Control symbol retention** using `keep_names: true` for public APIs, dummy exports for specific functions, or the `external` array for entire libraries.
- **Optimize assets** by enabling `experimentalCss` and `experimentalHtml` to extend dead-code elimination beyond JavaScript.
- **Preserve debugging capabilities** by setting `sourcemap: "external"` and disabling `identifiers` minification while keeping `syntax: true` for DCE.

## Frequently Asked Questions

### Does Bun enable tree-shaking by default in production builds?

No, you must explicitly set `minify: true` or enable at least `minify.syntax: true` to trigger the dead-code elimination passes in the linker. Without minification enabled, the bundler in `bundle_v2.zig` skips the `postProcessJSChunk.zig` DCE phase and emits all imported symbols regardless of usage.

### How does Bun's tree-shaking implementation differ from esbuild?

While Bun's bundler API is compatible with esbuild conventions, the implementation resides in Zig rather than Go. The specific pipeline—`computeCrossChunkDependencies.zig` followed by `postProcessJSChunk.zig` and `renameSymbolsInChunk.zig`—represents a distinct architecture optimized for Bun's runtime. Both tools rely on static analysis for DCE, but Bun defaults `emit_dce_annotations` to true, automatically removing dead code rather than merely annotating it.

### Can I remove dead code while keeping original variable names for debugging?

Yes. Configure `minify: { syntax: true, identifiers: false, whitespace: true }` to execute the tree-shaking logic in `postProcessJSChunk.zig` without triggering the symbol renaming phase in `renameSymbolsInChunk.zig`. This preserves original identifier names while still eliminating unreachable code branches and unused exports.

### Why is my exported function being removed even though I'm using it?

The linker marks symbols as dead when it cannot detect a reference in the static module graph. If you dynamically reference an export (e.g., via `window[myExport]`), add a static dummy reference like `const __keep = myExport` in the source file, or set `keep_names: true` in the minify configuration to prevent the identifier from being pruned during the DCE pass.