Modules & Bundlers
From many files to one optimized bundle. The build tooling that powers modern JS.
Why Bundle
Browsers can't efficiently load 1000 separate files.
// Your project has hundreds of files: // src/ // components/Button.tsx // components/Modal.tsx // utils/format.ts // hooks/useAuth.ts // ... 200 more files // node_modules/ (thousands of files) // Problems without bundling: // 1. Too many HTTP requests (one per file) // 2. No tree-shaking (ship unused code) // 3. No transpilation (JSX, TypeScript don't run in browsers) // 4. No minification (larger file sizes) // 5. No polyfills for older browsers // A bundler solves all of this: // 200+ source files → 3-5 optimized bundles // TypeScript/JSX → plain JavaScript // Dead code eliminated // Minified and compressed
What a bundler does:
1. Resolve all imports (build dependency graph)
2. Transform code (TS, JSX, new syntax → old JS)
3. Remove dead code (tree shaking)
4. Split into chunks (code splitting)
5. Minify and optimize output
Bundler Landscape
The major tools and when to use them.
| Tool | Best for | Key feature |
|---|---|---|
| Vite | Apps (React, Vue, Svelte) | Instant HMR, ESM dev |
| Webpack | Complex enterprise apps | Massive plugin ecosystem |
| Rollup | Libraries/packages | Clean ESM output |
| esbuild | Speed-critical builds | 100x faster (Go-based) |
| Turbopack | Next.js apps | Incremental (Rust-based) |
// Vite config (most popular for new projects):
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
target: "es2020",
minify: "terser",
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
},
},
},
},
});
// Dev: vite (instant startup, ESM-native)
// Build: vite build (uses Rollup under the hood)Tree Shaking
Eliminate unused code from the final bundle.
// utils.js exports 4 functions:
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
// app.js only uses one:
import { add } from "./utils.js";
console.log(add(2, 3));
// After tree shaking:
// Only 'add' is in the bundle!
// subtract, multiply, divide are removed
// This ONLY works with ES modules (import/export)
// NOT with CommonJS (require) — dynamic, can't analyzeTree shaking analyzes static import/export statements to detect unused exports and removes them from the bundle.
1 / 2
Code Splitting
Load code on demand — not everything upfront.
// Without splitting: one huge bundle
// main.js (2MB) — loads everything at once
// With splitting: many smaller chunks
// main.js (200KB) — critical code only
// admin.chunk.js (150KB) — loaded if user is admin
// chart.chunk.js (300KB) — loaded when chart is visible
// Dynamic import creates split points:
const AdminPanel = lazy(() => import("./AdminPanel"));
const Chart = lazy(() => import("./Chart"));
// Route-based splitting (React):
const routes = {
"/": lazy(() => import("./pages/Home")),
"/about": lazy(() => import("./pages/About")),
"/dashboard": lazy(() => import("./pages/Dashboard")),
};
// Vendor splitting (separate lib code from app code):
// vite.config.ts:
build: {
rollupOptions: {
output: {
manualChunks: {
// React rarely changes → cached separately
"react-vendor": ["react", "react-dom"],
"chart-vendor": ["chart.js", "d3"],
}
}
}
}Module Resolution
How bundlers find the file for each import.
// When you write:
import { Button } from "@/components/Button";
import React from "react";
import "./styles.css";
// The bundler resolves each:
// 1. Aliases (@ → src/):
// "@/components/Button" → "src/components/Button.tsx"
// Configured in tsconfig.json or bundler config
// 2. Bare specifiers (package names):
// "react" → "node_modules/react/index.js"
// Reads package.json "main", "module", or "exports" field
// 3. Relative paths:
// "./styles.css" → current dir + file
// 4. Extension resolution (tries in order):
// "./Button" → ./Button.ts → ./Button.tsx → ./Button.js
// → ./Button/index.ts → ./Button/index.tsx
// package.json "exports" field (modern):
{
"exports": {
".": {
"import": "./dist/esm/index.js", // for ESM
"require": "./dist/cjs/index.js" // for CJS
},
"./utils": "./dist/esm/utils.js"
}
}
// This controls EXACTLY what can be imported from your packageFAQ
Common questions about bundlers.