How to Configure Bun's Bundler for Production Builds with Tree-Shaking
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) validates your options and passes them to the native layer.
The internal LinkerContext.Options struct in src/bundler/LinkerContext.zig stores flags like emit_dce_annotations and ignore_dce_annotations. According to the source in 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:
computeCrossChunkDependencies.zig– Calculates which symbols are reachable across all chunks to establish the live code boundary.postProcessJSChunk.zig– Usesprint_dce_annotationsto mark symbols with no import/export references for elimination.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:
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:
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:
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:
// 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:
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:
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:
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:
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: trueor configuring specificminifysub-options (syntax,identifiers,whitespace) in yourBun.buildcall. - Configure native options via the
BuildConfiginterface insrc/js/builtins/BundlerPlugin.ts, which maps toLinkerContext.Optionsinsrc/bundler/LinkerContext.zig. - Control symbol retention using
keep_names: truefor public APIs, dummy exports for specific functions, or theexternalarray for entire libraries. - Optimize assets by enabling
experimentalCssandexperimentalHtmlto extend dead-code elimination beyond JavaScript. - Preserve debugging capabilities by setting
sourcemap: "external"and disablingidentifiersminification while keepingsyntax: truefor 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →