Module Bundlers
Module bundlers combine multiple JavaScript files into optimized bundles for browsers. They handle dependencies, code transformation, and code splitting for performance. They enable the use of modern JavaScript features, improve loading times, and streamline development workflows.
Examples: Webpack, Rollup, Parcel, Vite, esbuild, SWC...
Why Use a Module Bundler?
Problems Solved
- Dependency Management - Automatic resolution of imports/exports
- Browser Compatibility - Transformation of ES6+ to ES5
- Optimization - Minification, tree-shaking, code splitting
- Modern Development - Hot reload, source maps, TypeScript
- Performance - Intelligent bundling, lazy loading
Without a Bundler (Problems)
<!-- Manual dependency management -->
<script src="lib/jquery.js"></script>
<script src="lib/lodash.js"></script>
<script src="utils/helpers.js"></script>
<script src="components/header.js"></script>
<script src="components/footer.js"></script>
<script src="app.js"></script>
<!-- Important order, no modules, global pollution -->
With a Bundler
// Modern imports
import { debounce } from 'lodash'
import { fetchUser } from './api/users'
import Header from './components/Header'
// Modern code, automatic dependency management
The Main Bundlers
Vite (Recommended for new projects)
Advantages:
- Ultra-fast startup (native ESM in dev)
- Instant hot reload
- Minimal configuration
- Native TypeScript/JSX support
- Rollup in production
Disadvantages:
- Newer (fewer resources)
- Fewer plugins than Webpack
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns']
}
}
}
},
server: {
port: 3000,
open: true
}
})
Webpack (The most popular)
Advantages:
- Huge ecosystem
- Very flexible configuration
- Loaders for everything (CSS, images, fonts...)
- Advanced code splitting
- Hot Module Replacement
Disadvantages:
- Complex configuration
- Slow build times on large projects
- Steep learning curve
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
devServer: {
port: 3000,
hot: true,
open: true
}
}
Rollup (Ideal for libraries)
Advantages:
- Highly optimized bundles
- Excellent tree-shaking
- Simple configuration
- Perfect for libraries
- Native ES modules
Disadvantages:
- Fewer features for apps
- Smaller ecosystem
- Limited code splitting
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.cjs.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/bundle.esm.js',
format: 'esm',
sourcemap: true
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary',
sourcemap: true
}
],
plugins: [
resolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: ['@babel/preset-env']
}),
terser() // Minification
],
external: ['react', 'react-dom'] // External dependencies
}
Parcel (Zero-config)
Advantages:
- Zero configuration
- Very fast
- Native multi-format support
- Excellent hot reload
- Intelligent cache
Disadvantages:
- Less control
- Smaller ecosystem
- Limited customization
// package.json (enough for Parcel!)
{
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html --dist-dir dist"
}
}
esbuild (Ultra-fast)
Advantages:
- Extreme speed (written in Go)
- Native TypeScript/JSX support
- Simple API
- Very fast minification
Disadvantages:
- Limited features
- No native Hot reload
- Nascent ecosystem
// build.js
const esbuild = require('esbuild')
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
target: 'es2015',
loader: {
'.png': 'file',
'.svg': 'text'
},
define: {
'process.env.NODE_ENV': '"production"'
}
}).catch(() => process.exit(1))
Key Concepts
📥 Entry Points
// Single entry point
entry: './src/index.js'
// Multiple entry points
entry: {
app: './src/app.js',
admin: './src/admin.js',
vendor: ['react', 'lodash']
}
📤 Output
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js', // Cache busting
publicPath: '/assets/',
clean: true // Cleans the output folder
}
Loaders (Webpack)
module: {
rules: [
// JavaScript/TypeScript
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: 'babel-loader'
},
// CSS/SCSS
{
test: /\.s?css$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
// Images
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource',
generator: {
filename: 'images/[name].[hash][ext]'
}
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash][ext]'
}
}
]
}
🔌 Plugins
plugins: [
// Generates the HTML
new HtmlWebpackPlugin({
template: './public/index.html',
minify: true
}),
// Extracts the CSS
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
}),
// Environment variables
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
// Bundle analysis
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
Code Splitting
// Automatic splitting
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Vendors (node_modules)
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
// Common code
common: {
minChunks: 2,
chunks: 'all',
name: 'common'
}
}
}
}
// Dynamic imports (lazy loading)
const LazyComponent = lazy(() => import('./LazyComponent'))
// Webpack magic comments
const utils = import(
/* webpackChunkName: "utils" */
/* webpackPreload: true */
'./utils'
)
🌳 Tree Shaking
// package.json - Mark as side-effect free
{
"sideEffects": false
}
// Or specify files with side effects
{
"sideEffects": [
"*.css",
"src/polyfills.js"
]
}
// Specific import for tree shaking
import { debounce } from 'lodash' // Imports all of lodash
import debounce from 'lodash/debounce' // Imports only debounce
Complete Vite Configuration (Recommended)
vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [
react({
// Fast Refresh
fastRefresh: true
})
],
// Path aliases
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets')
}
},
// Environment variables
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
},
// Dev server configuration
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// Build configuration
build: {
outDir: 'dist',
sourcemap: true,
minify: 'terser',
// Chunk optimization
rollupOptions: {
output: {
manualChunks: {
// Vendor chunks
react: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@mui/material', '@emotion/react'],
utils: ['lodash', 'date-fns', 'axios']
}
}
},
// Terser configuration
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// Dependency optimization
optimizeDeps: {
include: ['react', 'react-dom'],
exclude: ['@vite/client', '@vite/env']
},
// CSS
css: {
modules: {
localsConvention: 'camelCase'
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"analyze": "vite-bundle-analyzer",
"webpack:dev": "webpack serve --mode development",
"webpack:build": "webpack --mode production",
"webpack:analyze": "webpack-bundle-analyzer dist/stats.json",
"rollup:build": "rollup -c",
"rollup:watch": "rollup -c -w",
"parcel:dev": "parcel src/index.html",
"parcel:build": "parcel build src/index.html"
}
}
Advanced Optimizations
Performance
// Lazy loading of routes
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
// Critical preloading
const criticalData = import(
/* webpackPreload: true */
'./critical-data'
)
// Prefetching for later
const nonCritical = import(
/* webpackPrefetch: true */
'./non-critical'
)
Bundle Analysis
# Webpack Bundle Analyzer
npm install -D webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/stats.json
# Vite Bundle Analyzer
npm install -D vite-bundle-analyzer
npx vite-bundle-analyzer
# Rollup Plugin Visualizer
npm install -D rollup-plugin-visualizer
Multi-Environment Configuration
// vite.config.js
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production'
return {
plugins: [
react(),
...(isProduction ? [
// Production plugins only
] : [])
],
build: {
minify: isProduction ? 'terser' : false,
sourcemap: !isProduction
},
define: {
__DEV__: !isProduction
}
}
})
Practical Tips
Best Practices
- Choose according to the project - Vite for new projects, Webpack for legacy
- Optimize chunks - Separate vendor, common, and pages
- Use cache - Contenthash for cache busting
- Analyze regularly - Monitor bundle size
- Lazy loading - Load components on demand
Pitfalls to Avoid
- Over-optimization - Do not optimize prematurely
- Too large bundles - Monitor size (< 250KB initial)
- Too many chunks - Avoid excessive fragmentation
- Unnecessary dependencies - Audit regularly
Debugging
// Source maps for debugging
devtool: 'eval-source-map', // Dev
devtool: 'source-map', // Production
// Verbose logging
stats: 'verbose',
// Performance analysis
performance: {
hints: 'warning',
maxEntrypointSize: 250000,
maxAssetSize: 250000
}
Migration Between Bundlers
Webpack → Vite
// Webpack
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
// Vite equivalent
export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})