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.

ToolBest forKey feature
ViteApps (React, Vue, Svelte)Instant HMR, ESM dev
WebpackComplex enterprise appsMassive plugin ecosystem
RollupLibraries/packagesClean ESM output
esbuildSpeed-critical builds100x faster (Go-based)
TurbopackNext.js appsIncremental (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 analyze

Tree 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 package

FAQ

Common questions about bundlers.