esbuild is often lauded for its speed, but its true power lies in how it forces you to rethink your entire build process.

Imagine you’re building a web application. Traditionally, you’d have a multi-step process:

  1. Linting: Check code style.
  2. Transpilation: Convert modern JavaScript (ES6+) to older versions for browser compatibility (e.g., Babel).
  3. Bundling: Combine multiple JavaScript files into fewer, larger ones (e.g., Webpack, Rollup).
  4. Minification: Remove whitespace and shorten variable names.
  5. CSS processing: Sass/LESS compilation, autoprefixing, minification.
  6. Asset handling: Copying and optimizing images, fonts.

This sequential nature, especially with heavy Babel plugins, is where the build time really creeps up. esbuild flips this by doing most of it in a single, highly optimized pass.

Let’s see esbuild in action. Suppose we have a simple project structure:

project/
├── src/
│   ├── index.js
│   └── utils.js
├── package.json
└── index.html

src/utils.js:

export function greet(name) {
  return `Hello, ${name}!`;
}

src/index.js:

import { greet } from './utils.js';

const message = greet('World');
document.getElementById('app').innerText = message;

index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Esbuild Demo</title>
</head>
<body>
    <div id="app"></div>
    <script src="bundle.js"></script>
</body>
</html>

package.json:

{
  "name": "esbuild-demo",
  "version": "1.0.0",
  "scripts": {
    "build": "esbuild src/index.js --bundle --outfile=bundle.js --minify --platform=browser --format=esm"
  },
  "devDependencies": {
    "esbuild": "^0.20.0"
  }
}

When you run npm run build, esbuild takes src/index.js, sees the import for utils.js, reads that too, combines them, transpiles any modern JS syntax (like export and import) down to a format suitable for browsers, minifies the resulting code, and outputs it to bundle.js.

The command esbuild src/index.js --bundle --outfile=bundle.js --minify --platform=browser --format=esm breaks down like this:

  • src/index.js: The entry point for the build.
  • --bundle: Tells esbuild to resolve imports and include dependencies.
  • --outfile=bundle.js: Specifies the output file.
  • --minify: Enables code minification.
  • --platform=browser: Targets a browser environment, influencing what global variables are available and how modules are handled.
  • --format=esm: Outputs JavaScript modules in the ES Module format.

The magic esbuild performs here is that it’s not just concatenating files; it’s performing static analysis on your entire dependency graph. It understands import/export statements, resolves them, and then efficiently transforms the code. Unlike tools that rely on plugins for each step (transpilation, bundling, minification), esbuild has these capabilities built-in and written in Go, allowing it to leverage multi-threading and highly efficient memory management.

For CI pipelines, this means replacing a complex sequence of commands with a single, lightning-fast esbuild invocation. You can integrate this directly into your package.json script.

Your CI configuration (e.g., .github/workflows/build.yml for GitHub Actions) might look like this:

name: CI Pipeline

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20' # Or your preferred Node.js version
    - name: Install dependencies
      run: npm ci
    - name: Build with esbuild
      run: npm run build
    - name: Upload artifact (optional)
      uses: actions/upload-artifact@v3
      with:
        name: build-output
        path: bundle.js # Or your build output directory

The npm ci step ensures you have a clean install of dependencies, and npm run build executes the esbuild command. The entire build process, including dependency installation, can often be reduced from minutes to seconds.

The most surprising thing about esbuild’s speed is that it achieves its performance by not using a traditional plugin architecture. Instead of a JavaScript-based plugin system that needs to be interpreted and executed, esbuild’s core functionality is written in Go. When you use esbuild, you’re executing a highly optimized binary that handles parsing, transforming, and bundling directly, with Go’s concurrency features and efficient memory management at its core. This bypasses the overhead inherent in JavaScript execution for these critical build tasks.

When you need to add custom transformations that aren’t built-in, like complex code generation or specific optimizations, you’ll explore esbuild’s plugin API. This API is designed to be minimal and efficient, often involving passing raw source code or ASTs between your plugin and esbuild itself, rather than a full-blown, slow plugin ecosystem.

Want structured learning?

Take the full Esbuild course →