📦 Webpack & Bundlers Guide

Complete guide to JavaScript bundlers: webpack, Vite, Rollup, esbuild & modern build tools

📦 What Are Bundlers

A bundler is a tool that takes all your JavaScript files, CSS, images, and other assets and combines them into optimized bundles that browsers can efficiently load.

Your Project Browser ───────────────── ───────────── src/ ├── index.js ┌─────────────┐ dist/ ├── utils.js │ │ ├── bundle.js (combined, minified) ├── api.js ───▶ │ Bundler │ ──▶ ├── styles.css (optimized) ├── styles.css │ │ └── images/ (compressed) └── components/ └─────────────┘ ├── Header.js └── Footer.js

Key Functions

FunctionDescription
Combine filesMerge multiple JS files into fewer bundles
Transform codeConvert modern JS/TS to browser-compatible code
OptimizeMinify, tree-shake, compress
Handle assetsProcess CSS, images, fonts
Dev serverHot-reloading during development

🕰️ Before Bundlers

The Old Days (Pre-2012)

<!-- You had to manually include EVERY script in correct order! -->
<html>
<head>
  <!-- jQuery must come first -->
  <script src="jquery.min.js"></script>
  <!-- Then jQuery plugins -->
  <script src="jquery.slider.js"></script>
  <script src="jquery.modal.js"></script>
  <!-- Then your libraries -->
  <script src="lodash.js"></script>
  <script src="moment.js"></script>
  <!-- Then YOUR code, in dependency order! -->
  <script src="utils.js"></script>
  <script src="api.js"></script>
  <script src="app.js"></script>
</head>
</html>

Problems with This Approach

ProblemDescription
Manual orderingYou had to know which file depends on which
Global scope pollutionEverything was on window object
HTTP requests10 scripts = 10 HTTP requests (very slow)
No import/exportJavaScript had no module system
No optimizationFiles weren't minified, no tree-shaking
Cache bustingUpdating one file meant users re-download everything

Intermediate Solutions

// 1. IIFE Pattern (Immediately Invoked Function Expression)
var MyApp = (function() {
  var privateVar = 'secret';
  
  return {
    publicMethod: function() {
      return privateVar;
    }
  };
})();

// 2. AMD (RequireJS)
define(['jquery', 'lodash'], function($, _) {
  return { init: function() { /* ... */ } };
});

// 3. CommonJS (Node.js style)
const utils = require('./utils');
module.exports = { /* ... */ };

✅ Problems Bundlers Solve

🔗

1. Module Resolution

// Before: No imports, everything global
window.utils = { ... };

// After: Clean imports
import { formatDate } from './utils';
import React from 'react';
📊

2. Fewer HTTP Requests

Before: 50 HTTP requests (one per file)
After: 3-5 HTTP requests (bundled chunks)

🔄

3. Code Transformation

// You write modern JavaScript:
const add = (a, b) => a + b;
const user = { ...defaults, name };

// Bundler transforms for old browsers:
var add = function(a, b) { return a + b; };
var user = Object.assign({}, defaults, { name: name });
🌳

4. Tree Shaking (Remove Unused Code)

// lodash has 300+ functions
import { debounce } from 'lodash-es';

// Only debounce is included, not entire library!
// Saves 70KB+ in final bundle
✂️

5. Code Splitting (Lazy Loading)

// This component only loads when user visits /settings
const Settings = React.lazy(() => import('./Settings'));

// Creates separate chunk: settings.chunk.js

📦 Module Systems (CJS vs ESM)

Understanding module systems is foundational to understanding bundlers.

📦 CommonJS (CJS)

Node.js original, synchronous

// Importing
const lodash = require('lodash');
const { debounce } = require('lodash');

// Exporting
module.exports = { myFunction };
module.exports.helper = helper;

❌ Problems:

  • Synchronous - bad for browsers
  • Dynamic - can't tree-shake easily
  • require() can be anywhere

📦 ES Modules (ESM)

Modern JavaScript standard

// Importing
import lodash from 'lodash';
import { debounce } from 'lodash';
import * as utils from './utils';

// Exporting
export const myFunction = () => {};
export default MyComponent;

✅ Benefits:

  • Static analysis - tree-shaking works!
  • Async by nature - perfect for browsers
  • Imports must be at top level
💡 Why This Matters: Bundlers convert CJS → ESM for tree-shaking. That's why Vite/esbuild pre-bundles node_modules! React is written in CJS, but Vite converts it to ESM so browsers understand.

🔧 Webpack

Webpack (2014) became the industry standard bundler with powerful features but complex configuration.

Core Concepts

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 1. ENTRY - Where to start bundling
  entry: './src/index.js',
  
  // 2. OUTPUT - Where to put the bundle
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',  // Cache busting!
  },
  
  // 3. MODE
  mode: 'production',
  
  // 4. LOADERS - Transform files
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.(png|jpg)$/, type: 'asset/resource' },
    ],
  },
  
  // 5. PLUGINS - Extra functionality
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
  
  // 6. DEV SERVER
  devServer: {
    hot: true,
    port: 3000,
  },
};

Webpack Features

FeatureDescription
LoadersTransform files (Babel, TypeScript, SCSS)
PluginsExtend functionality (HTML generation, compression)
Code SplittingSplit code into chunks for lazy loading
Tree ShakingRemove unused exports
HMRHot Module Replacement - update without refresh
Module FederationShare code between apps (micro-frontends)

⚡ Vite

Vite (2020, by Evan You) takes a completely different approach using native ES modules.

The Core Difference

🐢 Webpack Approach

Start dev server ↓ Bundle ENTIRE app ← SLOW! ↓ Serve bundled code Time: 30-60 seconds

⚡ Vite Approach

Start dev server ↓ Serve files AS-IS ← INSTANT! (Native ES modules) Time: < 1 second

Why Vite Is Faster

1. No Bundling in Dev Mode

// Browser requests: /src/App.jsx
// Vite transforms just that ONE file and serves it

// index.html
<script type="module" src="/src/main.jsx"></script>

// Browser natively understands ES modules:
import React from 'react';   // → /node_modules/.vite/react.js
import App from './App';     // → /src/App.jsx (transformed on-demand)

2. Pre-bundling with esbuild

Webpack uses JavaScript-based bundler → Slow
Vite uses esbuild (written in Go) → 100x faster

3. Fast HMR

Webpack HMR: Rebundles affected module + dependencies → Slower as app grows
Vite HMR: Only transforms the changed file → Consistently instant

Vite Configuration

// vite.config.js - Much simpler than webpack!
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  
  server: {
    port: 3000,
    open: true,
  },
  
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
});

// That's often ALL you need!

Vite vs Webpack Comparison

FeatureWebpackVite
Dev Server Start30-60s (large apps)< 1s
HMR SpeedSlower as app growsConsistently instant
BundlerJavaScript (slow)esbuild (Go, 100x faster)
Prod BuildwebpackRollup (optimized)
ConfigComplexSimple defaults
EcosystemHuge (mature)Growing fast

📜 Rollup

Rollup (2015) is optimized for ES modules and library development. It pioneered tree-shaking!

What Makes Rollup Special

// Rollup pioneered "Tree Shaking"
import { debounce } from 'lodash-es';
// Only debounce is included, not the other 300+ functions!

Rollup vs Webpack Output

Webpack Output

// Has runtime code!
/******/ (() => {
/******/   var __webpack_modules__ = ({
/******/     "./src/index.js": ((...) => {
              // Your code here...
/******/     })
/******/   });
// ... 100+ lines of webpack runtime

Rollup Output

// Clean! No runtime bloat.
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

export { add, multiply };
// That's it!

When to Use Rollup

🚀 esbuild

esbuild (2020) is an extremely fast bundler/transpiler written in Go.

Speed Comparison

webpack
60s
Rollup
35s
Parcel 2
25s
esbuild
0.4s
⚡ esbuild is 100x faster! Because it's written in Go (compiled), parallelized across CPU cores, and has efficient memory usage.

esbuild Usage

# CLI
esbuild src/index.js --bundle --minify --outfile=dist/bundle.js
// JavaScript API
import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ['es2020'],
  outfile: 'dist/bundle.js',
});

esbuild Limitations

✅ Does Well❌ Doesn't Do
BundlingHMR (Hot Module Replacement)
MinificationLimited plugin ecosystem
TypeScript syntaxType checking
JSX transformationComplex code splitting

How Vite Uses Both

┌─────────────────────────────────────────────────────┐ │ VITE │ ├─────────────────────────────────────────────────────┤ │ │ │ DEVELOPMENT: │ │ ┌─────────────┐ │ │ │ esbuild │ ← Pre-bundles dependencies (fast!) │ │ └─────────────┘ │ │ + │ │ Native ES Modules (no bundling needed) │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ PRODUCTION: │ │ ┌─────────────┐ │ │ │ Rollup │ ← Final optimized bundle │ │ └─────────────┘ │ │ + │ │ ┌─────────────┐ │ │ │ esbuild │ ← Minification (faster than terser)│ │ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────┘

✂️ Code Splitting

1. Route-based Splitting (Most Common)

import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

// Result:
// - home.chunk.js (loads on /)
// - dashboard.chunk.js (loads on /dashboard)
// - settings.chunk.js (loads on /settings)

2. Component-based Splitting

const HeavyChart = lazy(() => import('./components/HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && (
        <Suspense fallback={<Loading />}>
          <HeavyChart />  {/* Loads only when shown! */}
        </Suspense>
      )}
    </div>
  );
}

3. Vendor Splitting

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'ui-vendor': ['@mui/material', '@emotion/react'],
        },
      },
    },
  },
}

📊 Optimization Tips

Bundle Analysis

// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      filename: 'stats.html',
      gzipSize: true,
    }),
  ],
}
Always analyze your bundle! Look for large dependencies (moment.js → day.js), unused code, and opportunities for code splitting.

Environment Variables

# .env files (Vite)
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App

# ⚠️ NEVER put secrets here - embedded in bundle!
// Accessing in code
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE);   // 'development' or 'production'
console.log(import.meta.env.DEV);    // true in dev
console.log(import.meta.env.PROD);   // true in production

Dev vs Production Builds

DevelopmentProduction
✅ Source maps (detailed)✅ Minified code
✅ No minification✅ Tree-shaken
✅ Hot Module Replacement✅ Code splitting
✅ Detailed errors✅ Asset optimization
❌ Not optimized✅ Content hashing

🌐 Legacy Browser Support

By default, Vite targets modern browsers only (Chrome 87+, Firefox 78+, Safari 14+).

Vite Legacy Plugin

npm install @vitejs/plugin-legacy --save-dev
// vite.config.js
import legacy from '@vitejs/plugin-legacy';

export default {
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'],
      // Or: ['> 0.5%', 'last 2 versions', 'not dead']
    }),
  ],
};

How It Works

<!-- Generated HTML -->

<!-- Modern browsers load this -->
<script type="module" src="/assets/index.abc123.js"></script>

<!-- Legacy browsers load this instead -->
<script nomodule src="/assets/polyfills-legacy.js"></script>
<script nomodule src="/assets/index-legacy.js"></script>
type="module" → Modern browsers execute this, ignore nomodule
nomodule → Modern browsers IGNORE this, legacy browsers execute

🎯 When to Use Which

ScenarioRecommendation
New project✅ Vite
React/Vue/Svelte app✅ Vite
Fastest dev experience✅ Vite
Building a library✅ Rollup or Vite (library mode)
Legacy browser support (IE11)⚠️ Webpack or Vite + plugin
Existing large webpack project⚠️ Stay with webpack
Micro-frontends⚠️ Webpack (Module Federation)
Complex custom builds⚠️ Webpack (more flexible)

Other Bundlers Worth Knowing

BundlerDescription
ParcelZero-config bundler, good for beginners
TurbopackNext.js bundler (Rust, by Vercel)
SWCFast Rust-based transformer (used by Next.js)
BunAll-in-one runtime + bundler

Summary

BundlerBest ForLanguageSpeed
WebpackComplex apps, legacyJavaScriptMedium
ViteModern app developmentJavaScriptFast
RollupLibrariesJavaScriptMedium
esbuildFast bundling/transpilingGoExtremely Fast
TurbopackNext.js projectsRustExtremely Fast