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
- Module resolution: combine many files into fewer bundles
- Tree shaking: remove unused exports from the final bundle
- Minification: shrink code by removing whitespace, shortening variables
- Transpilation: convert TypeScript, JSX, and modern syntax into browser-compatible JavaScript
- Asset handling: process CSS, images, fonts, and other non-JS files
- 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-visualizerto 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.jscause caching problems. Usemain.[hash].jsso 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