Getting Started with esbuild

· 6 min read · Updated April 20, 2026 · intermediate
javascript esbuild bundler tooling performance

Overview

esbuild is a JavaScript bundler and minifier written in Go. It parses, transforms, and打包 JavaScript in a fraction of the time that JavaScript-based bundlers require. A build that takes webpack 30 seconds can take esbuild under a second.

The speed comes from two things: Go runs natively, and esbuild processes your code in parallel using all available CPU cores. It is not a development server with HMR — it is a build tool. For dev server workflows, you would typically pair esbuild with a tool like Vite (which uses esbuild internally for its transformation step).

esbuild handles TypeScript, JSX, tree shaking, bundling, and minification out of the box. It does not have a plugin system as powerful as Rollup’s, but what it does, it does extremely fast.

Installation

npm install -D esbuild

The package installs both the CLI and the Node.js API.

Using the CLI

Basic bundling

esbuild src/index.js --bundle --outfile=dist/bundle.js

--bundle tells esbuild to follow imports and bundle everything into one file. Without --bundle, esbuild only transforms the single file you give it.

With minification and sourcemaps

esbuild src/index.js \
  --bundle \
  --outfile=dist/bundle.js \
  --minify \
  --sourcemap

Target a specific JavaScript version

esbuild src/index.js --bundle --outfile=dist/bundle.js --target=es2020

--target tells esbuild what syntax to preserve and what to transform. Older targets cause more transformation work but produce wider compatibility.

Multiple entry points

esbuild src/index.js src/admin.js --bundle --outdir=dist

This produces dist/index.js and dist/admin.js.

Watch mode

esbuild src/index.js --bundle --outfile=dist/bundle.js --watch

esbuild watches the input files and rebuilds whenever they change.

The Node.js API

For more control, call esbuild programmatically:

const esbuild = require('esbuild');

const result = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  minify: true,
  sourcemap: true,
  target: ['es2020'],
});

esbuild.build() returns a promise that resolves when the build is complete. It throws on errors.

Get transformed output without bundling

const { outputFiles } = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: false,
  write: false,
});

console.log(outputFiles[0].text);  // the transformed JavaScript

write: false prevents esbuild from writing to disk — it returns the result as an object instead.

Inspect build warnings and errors

const result = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
});

if (result.errors.length > 0) {
  console.error(result.errors);
}

TypeScript and JSX

esbuild strips TypeScript types and transforms JSX without needing a separate TypeScript or Babel setup:

esbuild src/index.tsx --bundle --outfile=dist/bundle.js --loader:.tsx=tsx --loader:.ts=ts

In the Node.js API:

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/bundle.js',
  loader: { '.tsx': 'tsx', '.ts': 'ts' },
});

esbuild does not perform type checking — it only strips types. Run tsc --noEmit in your CI pipeline to catch type errors.

JSX configuration

By default, esbuild transforms JSX into React.createElement calls. You can change this behavior:

await esbuild.build({
  entryPoints: ['src/index.tsx'],
  jsx: 'automatic',      // transforms to React.createElement (React 17+)
  jsxImportSource: 'preact',  // use preact's JSX factory instead
});

Code Splitting

esbuild supports code splitting via dynamic imports:

// src/index.js
const { heavy } = await import('./heavy.js');
heavy.run();
esbuild src/index.js --bundle --outdir=dist --splitting --format=esm

--splitting works with --format=esm. esbuild creates a separate chunk for heavy.js and updates the import URL in the output.

For CommonJS output, use --format=cjs. Note that code splitting with CommonJS requires a runtime loader like Rollup-plugin-commonjs or a custom setup — esbuild does not handle the runtime aspect for CJS.

Tree Shaking

esbuild removes unused code automatically. By default it assumes that code with side effects might be needed, even if not imported:

// This unused function is removed by esbuild
function unusedHelper() {
  return 'not referenced';
}

To explicitly mark code as having no side effects, use /* @__PURE__ */ or /* #__PURE__ */:

const result = /* @__PURE__ */ someCall();

esbuild treats this as side-effect-free and removes someCall() if its result is not used.

Plugins

esbuild’s plugin API lets you handle files that esbuild does not support natively:

const esbuild = require('esbuild');
const { existsSync, readFileSync } = require('fs');

const svgPlugin = {
  name: 'svg',
  setup(build) {
    build.onLoad({ filter: /\.svg$/ }, async (args) => {
      const contents = readFileSync(args.path, 'utf8');
      return {
        contents: `export default ${JSON.stringify(contents)}`,
        loader: 'js',
      };
    });
  },
};

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  plugins: [svgPlugin],
});

This plugin imports SVG files as string exports, which is useful for icon systems.

Official plugins

  • esbuild-plugin-postcss — CSS modules and PostCSS
  • @esbuild/plugin-eslint — ESLint integration
  • @esbuild/plugin-typescript — explicit TypeScript handling

Common Use Cases

Building a library

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/index.js',
  format: 'esm',
  platform: 'node',
  target: 'node18',
});

Use platform: 'node' to set correct tree-shaking behavior for Node.js and prevent bundling of Node.js built-ins like fs and path.

Building for browsers

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/index.js',
  format: 'iife',      // immediately-invoked function expression
  minify: true,
  target: 'es2020',
});

iife format wraps the output in an IIFE so the bundle is safe to include via a <script> tag.

Re-building on changes

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  sourcemap: true,
});

await ctx.watch();
console.log('Watching for changes...');

context() returns a context object with watch() and rebuild() methods. watch() continuously watches. rebuild() does a single rebuild.

Performance Comparison

esbuild is consistently the fastest JS bundler in benchmarks:

BundlerCold build (large project)
esbuild~0.5s
Rollup~8s
webpack~25s
Parcel~12s

These are rough figures for a typical real-world project with many modules. Your mileage varies, but the speed advantage is significant whenever incremental builds matter — in watch mode, in CI pipelines, or during development.

Gotchas

No HMR. esbuild is a bundler, not a dev server. Use it with Vite (which uses esbuild for transformations), a simple static server, or a custom HMR setup. Do not expect esbuild --watch to update your browser automatically.

Single config file. Unlike webpack which has complex configuration, esbuild configuration lives in the CLI flags or the build API call. For complex projects, put the build call in a build.js file and run it with node build.js.

Limited plugin ecosystem. Rollup’s plugin system is more powerful and has more plugins available. esbuild’s plugin API covers most needs but some advanced use cases require workarounds or a different tool.

No hot reloading by default. Pair with a dev server for that experience. esbuild’s --serve flag serves files but does not inject updates — it is not HMR.

Watch mode does not handle symlinks correctly on some platforms. If your project uses symlinked node_modules, watch mode may miss changes. Restart the build or use a different tool for watch workflows in monorepos.

See Also