esbuild can be configured to produce highly optimized production builds, but most people don’t realize its minification and tree-shaking are opt-in by default, meaning you’re likely shipping unminified, larger code without realizing it.
Let’s see esbuild in action for a production build. Imagine we have a simple index.js file:
// index.js
import { add } from './utils';
const result = add(5, 10);
console.log(`The result is: ${result}`);
export function unusedFunction() {
console.log('This should be removed!');
}
And a utils.js file:
// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
To build this for production with esbuild, you’d run:
esbuild index.js --bundle --outfile=dist/bundle.js --platform=node --format=cjs --minify --tree-shaking=true
Let’s break down this command:
esbuild index.js: The entry point for our application.--bundle: Tells esbuild to resolve all import paths and combine everything into a single output file.--outfile=dist/bundle.js: Specifies the output file path.--platform=node: Informs esbuild that the target environment is Node.js, affecting module resolution and available APIs. You’d use--platform=browserfor web environments.--format=cjs: Sets the output module format to CommonJS. Other options includeesm(ECMAScript Modules) andiife(Immediately Invoked Function Expression).--minify: This is crucial. It enables minification, which includes whitespace removal, identifier renaming, and dead code elimination after tree-shaking.--tree-shaking=true: Explicitly enables tree-shaking. This process removes unused exports from modules.
After running the command, dist/bundle.js will contain something like this:
// dist/bundle.js (simplified output)
function add(a,b){return a+b}var c=add(5,10);console.log(`The result is: ${c}`);
Notice how unusedFunction from index.js and subtract from utils.js are completely gone, and the code is minified.
The problem esbuild solves is the complexity and slowness of traditional bundlers. For a long time, configuring Webpack or Rollup for optimal production builds involved dozens of plugins and intricate settings. esbuild, written in Go, achieves remarkable speed by leveraging parallelism and efficient algorithms, while offering a simpler API for common tasks like bundling, minification, and transpilation.
Internally, esbuild first parses all your JavaScript code into an Abstract Syntax Tree (AST). For bundling, it then performs a static analysis of your import and export statements to build a dependency graph. Tree-shaking happens by traversing this graph. If an exported symbol is never imported or used by any other part of the code that is used, esbuild marks it as dead code. Minification then takes this pruned AST and serializes it back into code, renaming variables to short, meaningless characters (like a, b, c) and removing all unnecessary characters.
The --tree-shaking=true flag is the key to removing unused code. Without it, even with --minify, code that is never referenced will still be present in the output bundle. esbuild’s tree-shaking works by analyzing which exports are actually imported and used by the code that is kept. If an export from a module is never imported by any "live" code path, it’s eliminated. This is why unusedFunction and subtract were removed: they were exported but never imported by index.js or any other code that was ultimately included.
When you’re building for the browser, you’ll often want to leverage esbuild’s ability to bundle multiple entry points and manage assets. For instance, if you have an index.html that references main.js, you might run:
esbuild src/main.js --bundle --outfile=dist/bundle.js --platform=browser --format=esm --minify --tree-shaking=true --loader:.css=text
The --loader:.css=text part is a powerful feature. It tells esbuild how to handle files with a .css extension. In this case, text means the CSS content will be imported as a string. You’d then typically use a JavaScript library to inject this CSS into the DOM. Other loaders include js, jsx, ts, tsx, json, file, and dataurl.
A common pitfall is forgetting to set --minify and --tree-shaking=true for production. Many developers assume these are defaults when esbuild is used. However, esbuild prioritizes build speed for development, so it’s often configured to skip these heavier optimizations unless explicitly requested. This means you might be deploying significantly larger bundles than necessary, impacting load times.
If you’re targeting modern browsers and want to emit ES Modules, you’d use --platform=browser --format=esm. This allows for native module loading in the browser, and when combined with --splitting, esbuild can generate multiple output files that work together, enabling better caching and parallel loading.