What a bundler actually does to your files

You write twenty JavaScript modules. The browser needs one file — or at least, far fewer than twenty. That gap is what a bundler closes.

The core job is straightforward: follow every import statement from your entry point, collect the dependency graph, and emit something a browser can execute efficiently. In practice, bundlers also minify variable names, strip dead code, transform modern syntax for older targets, inline small assets, and split the result so users only download what a given page needs. That's the job. Everything else is configuration surface area built on top of it.

Understanding this helps you debug build pipelines without just cargo-culting config files someone else wrote three years ago.

The graph, the transform, the output

Every bundler starts with a module graph. Starting at your index.ts or main.js, the tool reads each file, parses it into an abstract syntax tree (AST), and records what it imports. It then does the same for each of those imports, recursively, until the whole dependency tree is mapped.

Once the graph is built, the transform pass runs. This is where TypeScript gets compiled to JavaScript, JSX turns into React.createElement calls (or Solid's equivalent), and optional chaining or nullish coalescing gets rewritten if the target environment requires it. Third-party tools — Babel plugins, PostCSS, SVG loaders — hook into this stage.

Then comes output: merging modules into one or more bundles, renaming local variables to short identifiers (userName becomes a), removing unreachable code branches, and writing the final files with source maps alongside them.

Code splitting complicates the output stage in a useful way. Instead of one enormous bundle, the tool emits a small initial chunk and separate async chunks for routes or heavy dependencies. The browser fetches extras on demand. Done correctly, the user downloads only the code needed for what they're actually looking at.

esbuild: performance as a design choice

esbuild, written by Evan Wallace in Go, landed as something of a shock to the JavaScript tooling world. Benchmarks at the time showed it bundling large codebases ten to a hundred times faster than Webpack or Rollup, because Go compiles to native code and the tool does its passes in a single traversal rather than separate plugin stages.

The tradeoffs are real. esbuild's plugin API is deliberately limited compared to Webpack's ecosystem. It doesn't perform full TypeScript type-checking — it strips types and moves on, trusting you to run tsc --noEmit separately. And its code-splitting support, while present, is less sophisticated than Rollup's.

For build pipelines that are genuinely slow, esbuild is worth examining. It's also the engine underneath other tools: Vite uses it during development for dependency pre-bundling and for individual file transforms.

Vite: the developer-experience layer

Vite (French for "fast") was created by Evan You, the same person behind Vue, and has become the default build tool for Vue, Svelte, SolidJS, and many React projects. Its insight was architectural rather than purely algorithmic.

During development, Vite doesn't bundle your application code at all. It leans on native ES modules in the browser: the dev server serves each file individually, transformed on request. Import a component, the server compiles just that file. This means cold starts are nearly instant regardless of project size, because there's nothing to bundle upfront. Hot module replacement is granular — swap a single component without touching the rest of the module graph.

For production, Vite switches to Rollup under the hood, which produces well-optimised, split output with a mature plugin ecosystem. The configuration API is unified so you write one vite.config.ts that covers both modes. The Vite team is working on Rolldown, a Rollup-compatible bundler written in Rust, as a future replacement for the Rollup production pipeline — a Rollup-compatible bundler written in Rust — to bring esbuild-class speed to production builds while keeping the full Rollup plugin surface intact.

ts.ts
// vite.config.ts — a minimal setup
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'es2022',
    sourcemap: true,
  },
})

That's a complete production-ready configuration for a React project. Vite fills in sensible defaults everywhere else.

Picking the right tool

For most new projects: start with Vite. The dev experience is excellent, the defaults are sane, and the plugin ecosystem covers the common cases — asset handling, environment variables, CSS modules, TypeScript, and JSX out of the box.

Reach for esbuild directly when you're building tooling rather than an application: a library bundler, a CLI build step, a Node server that needs to be compiled quickly in CI. Its API is clean, its speed is real, and its simplicity is a feature when you don't need Vite's dev-server machinery.

Webpack remains the right answer when you're maintaining a large existing codebase already built around it, or when you need a specific loader that nothing else supports yet. Its ecosystem depth is unmatched; its configuration complexity is a known cost.

The bundler is not the hard part of your project. Know what it does, pick a reasonable default, and spend your attention on the code it's transforming.