4 min read
On this page

Bundlers & Build Tools

Modern web applications are written in modules, TypeScript, JSX, and other formats browsers do not understand natively. Build tools transform your source code into optimized assets the browser can run.

Why Bundling Exists

Browsers load files over the network. Without bundling, a project with 500 modules would make 500 HTTP requests. Bundlers solve this and several other problems.

What Bundlers Do

  1. Module resolution: combine many files into fewer bundles
  2. Tree shaking: remove unused exports from the final bundle
  3. Minification: shrink code by removing whitespace, shortening variables
  4. Transpilation: convert TypeScript, JSX, and modern syntax into browser-compatible JavaScript
  5. Asset handling: process CSS, images, fonts, and other non-JS files
  6. Code splitting: create separate chunks loaded on demand

Before and After

Source:
  src/
    main.ts          (imports 20 modules)
    components/      (30 files)
    utils/           (15 files)
    styles/          (10 CSS files)

After bundling:
  dist/
    index.html
    assets/
      main-abc123.js     (150 KB, minified, tree-shaken)
      vendor-def456.js   (80 KB, third-party code)
      style-ghi789.css   (12 KB, combined and minified)

Vite

Vite is the dominant modern build tool. It provides an extremely fast development server and uses Rollup for production builds.

Why Vite Is Fast in Development

Traditional bundlers (Webpack) bundle the entire application before serving it. Vite serves source files directly using native ES modules. The browser requests modules as needed, and Vite transforms them on the fly.

Traditional bundler:
  All files → Bundle → Serve → Browser

Vite dev server:
  Browser requests main.ts → Vite transforms just main.ts → Serves it
  Browser requests utils.ts → Vite transforms just utils.ts → Serves it

Getting Started

npm create vite@latest my-app -- --template svelte-ts
cd my-app
npm install
npm run dev

Configuration

// vite.config.ts
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
  build: {
    target: 'es2022',
    minify: 'esbuild',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['svelte'],
        },
      },
    },
  },
  server: {
    port: 3000,
    open: true,
  },
});

Production Builds

Vite uses Rollup under the hood for production. Rollup produces smaller bundles than Webpack because it was designed specifically for ES modules.

npm run build

# Output:
dist/
  index.html
  assets/
    index-abc123.js    (hashed for cache busting)
    index-def456.css

esbuild

esbuild is an extremely fast JavaScript bundler written in Go. It is 10-100x faster than JavaScript-based bundlers.

Speed Comparison

Bundling a large codebase:
  Webpack:  30 seconds
  Rollup:   15 seconds
  esbuild:  0.3 seconds

Usage

npx esbuild src/main.ts --bundle --outfile=dist/main.js --minify --sourcemap
// build.mjs
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/main.ts'],
  bundle: true,
  outfile: 'dist/main.js',
  minify: true,
  sourcemap: true,
  target: ['es2022'],
  format: 'esm',
});

Where esbuild Fits

  • Vite uses esbuild for dependency pre-bundling and TypeScript transpilation
  • Fast CI builds where Rollup's tree-shaking perfection is not needed
  • Simple projects that do not need Rollup's plugin ecosystem

Limitations

  • Less mature plugin ecosystem than Rollup or Webpack
  • Tree shaking is good but not as thorough as Rollup
  • No built-in HTML handling (unlike Vite or Webpack)

Webpack

Webpack was the standard bundler from 2015-2022. It is highly configurable but infamously complex.

Why Teams Move Away

// webpack.config.js — even a basic setup is verbose
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
    new MiniCssExtractPlugin(),
  ],
  resolve: {
    extensions: ['.ts', '.js'],
  },
};

Compare that to Vite, which handles most of this with zero configuration.

When You Still See Webpack

  • Large existing projects that cannot easily migrate
  • Projects needing Webpack-specific features (Module Federation for micro-frontends)
  • Legacy toolchains where the team has deep Webpack expertise

The Trend: Less Config, More Convention

The evolution of build tools follows a clear pattern.

Webpack (2015):   Maximum configuration, maximum complexity
Rollup (2017):    ES module-focused, simpler for libraries
Parcel (2018):    Zero config, automatic detection
esbuild (2020):   Blazing speed, written in Go
Vite (2021):      Best of all worlds: fast dev, Rollup prod

Modern tools work out of the box. TypeScript, CSS modules, JSON imports, and asset handling all work without plugins. Configuration is for edge cases, not the default.

Convention Over Configuration

# Vite: just works
src/main.ts        → Detected as entry point
src/App.svelte     → Svelte plugin handles it
src/styles.css     → Imported in JS, extracted in build
public/favicon.ico → Copied to dist/

No loaders, no rules, no resolve configuration. The tool understands common patterns and does the right thing.

When You Do Not Need a Bundler

Not every project requires a build step.

Small Projects

<!-- Just HTML, CSS, and a little JS -->
<script type="module" src="/main.js"></script>

Modern browsers support ES modules natively. For a small project (a few files, no npm dependencies, no TypeScript), you can ship source directly.

Deno

Deno supports TypeScript natively and imports from URLs. No bundler, no node_modules.

// Works directly in Deno, no build step
import { serve } from "https://deno.land/std/http/mod.ts";

When to Add a Bundler

You need a bundler when:

  • You use npm packages (bundler resolves node_modules)
  • You write TypeScript and want type checking
  • Bundle size matters (tree shaking, minification)
  • You need code splitting for a large SPA
  • You need CSS processing (PostCSS, Tailwind)

Common Pitfalls

  • Over-configuring Vite: Vite works out of the box. Adding configuration you do not need creates maintenance burden.
  • Staying on Webpack for new projects: unless you need Module Federation or a specific Webpack plugin, Vite is the better default for new projects.
  • Not enabling source maps: minified code is unreadable. Enable source maps in production for error tracking (but do not expose them publicly if concerned about code visibility).
  • Ignoring bundle size: add npx vite-bundle-visualizer to your workflow. A single large dependency can double your bundle.
  • Transpiling to ES5: unless you need to support IE11 (you probably do not), target ES2022. Less transpilation means smaller, faster code.
  • Not using content hashes: filenames like main.js cause caching problems. Use main.[hash].js so browsers cache until the content changes.

Key Takeaways

  • Bundlers combine modules, tree-shake unused code, minify, and transpile
  • Vite is the modern default: fast native ESM dev server with Rollup-based production builds
  • esbuild is 10-100x faster than JavaScript bundlers and is used inside Vite for transforms
  • Webpack is the old standard with maximum configurability but significant complexity
  • The trend is toward convention over configuration: modern tools work out of the box
  • Small projects and Deno applications may not need a bundler at all
  • Always enable content hashing and source maps in production builds