Skip to main content
๐Ÿ“ฆintermediate

Why Bundlers?

Bundlers like Webpack and Vite take your many source files and combine them into optimized bundles for the browser. Learn why they exist and what problems they solve.

The Problem Bundlers Solve

In the early web, you'd include scripts with <script> tags:

hljs html
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="myapp.js"></script>
<script src="utils.js"></script>

Problems with this:

  • Global scope pollution โ€” every script shares the window namespace
  • Dependency order โ€” you have to load dependencies in the right order manually
  • Performance โ€” each file = a separate HTTP request (slow!)
  • No modules โ€” no import/export
  • No transforms โ€” can't use TypeScript, JSX, or modern CSS

A bundler solves all of this.

What a Bundler Does

At its core, a bundler:

  1. Starts at an entry point (e.g., index.js)
  2. Follows all imports โ€” builds a dependency graph
  3. Transforms source files โ€” TypeScript โ†’ JS, JSX โ†’ JS, SCSS โ†’ CSS
  4. Bundles โ€” combines related code into output files
  5. Optimizes โ€” minifies, tree-shakes dead code, splits into chunks
src/ index.js โ† entry point App.jsx โ† imported by index.js api/ users.js โ† imported by App.jsx posts.js โ† imported by App.jsx utils/ format.js โ† imported by users.js โ†“ bundler processes dependency graph dist/ main.js โ† one optimized bundle main.css โ† extracted CSS

Dependency Graph

The bundler builds a tree (actually a DAG โ€” directed acyclic graph) of all your modules:

hljs javascript
// index.js
import App from "./App";     // entry depends on App
import "./styles.css";        // and on CSS

// App.jsx
import { fetchUsers } from "./api/users"; // App depends on users.js
import { formatDate } from "./utils/format"; // and on format.js

The bundler statically analyzes these imports, discovers every module, and includes only what's needed.

Tree Shaking

Tree shaking eliminates dead code โ€” exports that are imported but never used:

hljs javascript
// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }

// app.js
import { add } from "./math.js"; // only uses add

// After tree shaking, sub and multiply are not included in the bundle!

Tree shaking requires:

  • ES module syntax (import/export) โ€” CJS (require) is dynamic and can't be tree-shaken reliably
  • "sideEffects": false in package.json to tell bundlers the package has no side effects

Code Splitting

Large apps don't need to load all code upfront. Code splitting creates multiple chunks:

hljs javascript
// app.js
import React from "react";

// This component is dynamically imported โ€” it gets its own chunk
const HeavyChart = React.lazy(() => import("./HeavyChart"));

// Dashboard only loads when the user navigates there
const Dashboard = React.lazy(() => import("./Dashboard"));

Webpack/Vite will create:

  • main.js โ€” core app (~50KB)
  • HeavyChart.js โ€” loaded lazily (~200KB)
  • Dashboard.js โ€” loaded lazily (~80KB)

The user downloads the minimum needed upfront.

Loaders and Plugins

Bundlers extend with plugins/loaders for handling different file types:

hljs javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      { test: /\.tsx?$/, use: "ts-loader" },      // TypeScript
      { test: /\.jsx?$/, use: "babel-loader" },   // Modern JS/JSX
      { test: /\.css$/, use: ["style-loader", "css-loader"] },
      { test: /\.(png|jpg)$/, type: "asset/resource" }, // images
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: "./index.html" }),
    new MiniCssExtractPlugin(),
  ],
};

Source Maps

Source maps link minified production code back to your original source, making debugging possible:

// Browser shows you: app.min.js:1:43920 Uncaught TypeError: cannot read property... // With source maps, DevTools shows: src/components/UserCard.jsx:42 Uncaught TypeError: cannot read property...

The Main Players

BundlerBest ForKey Strength
WebpackComplex apps, mature ecosystemHighly configurable, huge plugin ecosystem
ViteModern apps, development speedESM-native dev server, blazing fast HMR
RollupLibraries, clean outputExcellent tree shaking, clean ESM output
esbuildSpeed, tooling10-100x faster than webpack (written in Go)
ParcelZero-configWorks out of the box, no config needed
TurbopackNext.js/Rust ecosystemIncremental compilation, Rust-based speed

Development vs Production

Bundlers behave differently in each mode:

Development:

  • Fast rebuilds (only recompile changed files)
  • Source maps for debugging
  • Hot Module Replacement (HMR) โ€” update the page without full reload
  • No minification โ€” readable output

Production:

  • Full optimization pass
  • Minification (remove whitespace, shorten names)
  • Tree shaking
  • CSS extraction to separate files
  • Asset hashing (e.g., main.a3f2d1.js for cache busting)
โ–ถTry it yourself

Key Takeaways

  • Bundlers solve: module resolution, dependency ordering, transforms, optimization
  • Tree shaking removes unused exports โ€” requires ESM (not CommonJS)
  • Code splitting creates multiple chunks for lazy loading
  • Loaders/plugins extend bundlers to handle TypeScript, CSS, images, etc.
  • Dev mode = speed + DX; Production mode = optimization + performance
  • Main options: Webpack (mature), Vite (modern), Rollup (libraries), esbuild (speed)

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’