📦 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.
Key Functions
| Function | Description |
|---|---|
| Combine files | Merge multiple JS files into fewer bundles |
| Transform code | Convert modern JS/TS to browser-compatible code |
| Optimize | Minify, tree-shake, compress |
| Handle assets | Process CSS, images, fonts |
| Dev server | Hot-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
| Problem | Description |
|---|---|
| Manual ordering | You had to know which file depends on which |
| Global scope pollution | Everything was on window object |
| HTTP requests | 10 scripts = 10 HTTP requests (very slow) |
| No import/export | JavaScript had no module system |
| No optimization | Files weren't minified, no tree-shaking |
| Cache busting | Updating 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
🔧 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
| Feature | Description |
|---|---|
| Loaders | Transform files (Babel, TypeScript, SCSS) |
| Plugins | Extend functionality (HTML generation, compression) |
| Code Splitting | Split code into chunks for lazy loading |
| Tree Shaking | Remove unused exports |
| HMR | Hot Module Replacement - update without refresh |
| Module Federation | Share 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
⚡ Vite Approach
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
| Feature | Webpack | Vite |
|---|---|---|
| Dev Server Start | 30-60s (large apps) | < 1s |
| HMR Speed | Slower as app grows | Consistently instant |
| Bundler | JavaScript (slow) | esbuild (Go, 100x faster) |
| Prod Build | webpack | Rollup (optimized) |
| Config | Complex | Simple defaults |
| Ecosystem | Huge (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
- ✅ Building libraries/packages
- ✅ Need multiple output formats (cjs, esm, umd)
- ✅ Want cleanest possible output
- ✅ Tree-shaking is critical
🚀 esbuild
esbuild (2020) is an extremely fast bundler/transpiler written in Go.
Speed Comparison
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 |
|---|---|
| Bundling | HMR (Hot Module Replacement) |
| Minification | Limited plugin ecosystem |
| TypeScript syntax | Type checking |
| JSX transformation | Complex code splitting |
How Vite Uses Both
✂️ 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,
}),
],
}
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
| Development | Production |
|---|---|
| ✅ 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 nomodulenomodule → Modern browsers IGNORE this, legacy browsers execute
🎯 When to Use Which
| Scenario | Recommendation |
|---|---|
| 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
| Bundler | Description |
|---|---|
| Parcel | Zero-config bundler, good for beginners |
| Turbopack | Next.js bundler (Rust, by Vercel) |
| SWC | Fast Rust-based transformer (used by Next.js) |
| Bun | All-in-one runtime + bundler |
Summary
| Bundler | Best For | Language | Speed |
|---|---|---|---|
| Webpack | Complex apps, legacy | JavaScript | Medium |
| Vite | Modern app development | JavaScript | Fast |
| Rollup | Libraries | JavaScript | Medium |
| esbuild | Fast bundling/transpiling | Go | Extremely Fast |
| Turbopack | Next.js projects | Rust | Extremely Fast |