Welcome to the World of Module Bundling
Imagine you're preparing for a trip. You have clothes, toiletries, electronics, and documents scattered across your room. Webpack is like a smart suitcase that not only packs everything efficiently but also organizes items based on when you'll need them, compresses them to save space, and ensures nothing important is left behind. Today, we'll learn how to pack our JavaScript applications professionally!
Why Do We Need Module Bundlers?
In the early days of web development, we included JavaScript files like this:
<!-- The old way -->
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="utils.js"></script>
<script src="api.js"></script>
<script src="app.js"></script>
Problems with the Traditional Approach
- Global namespace pollution
- Dependency management nightmare
- Loading order matters
- Network performance issues (many HTTP requests)
- No built-in way to use modern JavaScript features
What is Webpack?
Webpack is a static module bundler that creates a dependency graph of your entire application and bundles it into one or more optimized files.
Core Concepts
- Entry: Where webpack starts building its dependency graph
- Output: Where to emit the bundles and how to name them
- Loaders: Transform files into modules
- Plugins: Perform wider range of tasks
- Mode: Development or production optimizations
Setting Up Webpack
Installation
# Create a new project
mkdir webpack-demo
cd webpack-demo
npm init -y
# Install webpack and webpack-cli
npm install --save-dev webpack webpack-cli
# Create source directory
mkdir src
touch src/index.js
Basic Project Structure
webpack-demo/
├── node_modules/
├── src/
│ ├── index.js
│ ├── components/
│ │ ├── header.js
│ │ └── footer.js
│ └── utils/
│ └── helpers.js
├── dist/
├── package.json
├── package-lock.json
└── webpack.config.js
First Bundle
// src/index.js
import { formatDate } from './utils/helpers.js';
import { createHeader } from './components/header.js';
const header = createHeader('Welcome to Webpack!');
document.body.appendChild(header);
const today = formatDate(new Date());
console.log(`Today is ${today}`);
// src/utils/helpers.js
export function formatDate(date) {
return date.toLocaleDateString();
}
// src/components/header.js
export function createHeader(text) {
const header = document.createElement('h1');
header.textContent = text;
return header;
}
Running Webpack
# Add script to package.json
{
"scripts": {
"build": "webpack"
}
}
# Run webpack
npm run build
# Webpack will create dist/main.js by default
Webpack Configuration
Webpack uses a configuration file to customize its behavior. This is like giving detailed instructions to our smart packing system.
Basic webpack.config.js
const path = require('path');
module.exports = {
// Entry point of our application
entry: './src/index.js',
// Output configuration
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true // Clean the output directory before emit
},
// Development mode for better debugging
mode: 'development'
};
Multiple Entry Points
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js', // app.bundle.js, admin.bundle.js
path: path.resolve(__dirname, 'dist')
}
};
Development vs Production
// webpack.config.js
const path = require('path');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map'
};
};
// package.json scripts
{
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
}
Loaders: Teaching Webpack New Languages
Loaders are like translators that help webpack understand different file types. By default, webpack only understands JavaScript and JSON.
CSS Loader Setup
# Install loaders
npm install --save-dev css-loader style-loader
# webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/i, // Regular expression to match .css files
use: ['style-loader', 'css-loader'] // Process right to left
}
]
}
};
// src/styles.css
.header {
color: blue;
font-size: 24px;
}
// src/index.js
import './styles.css';
const header = document.createElement('h1');
header.className = 'header';
header.textContent = 'Styled with Webpack!';
document.body.appendChild(header);
Asset Loading
# Install file-loader (or use built-in asset modules in webpack 5)
npm install --save-dev file-loader
# webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource' // Webpack 5 built-in
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource'
}
]
}
};
// src/index.js
import Logo from './logo.png';
const img = new Image();
img.src = Logo;
document.body.appendChild(img);
Popular Loaders
Babel Loader for Modern JavaScript
# Install babel and babel-loader
npm install --save-dev babel-loader @babel/core @babel/preset-env
# webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
// .babelrc (alternative configuration)
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
Sass/SCSS Loader
# Install sass and sass-loader
npm install --save-dev sass sass-loader css-loader style-loader
# webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
'style-loader', // Creates style nodes from JS strings
'css-loader', // Translates CSS into CommonJS
'sass-loader' // Compiles Sass to CSS
]
}
]
}
};
// src/styles.scss
$primary-color: #007bff;
.button {
background-color: $primary-color;
&:hover {
background-color: darken($primary-color, 10%);
}
}
Plugins: Extending Webpack's Powers
While loaders work on individual files, plugins can affect the entire build process. They're like special tools in our packing system.
HtmlWebpackPlugin
# Install the plugin
npm install --save-dev html-webpack-plugin
# webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'My Webpack App',
template: './src/index.html',
filename: 'index.html'
})
]
};
// src/index.html (template)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
MiniCssExtractPlugin
# Install the plugin
npm install --save-dev mini-css-extract-plugin
# webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
]
};
Common Plugins
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// Clean dist folder before each build
new CleanWebpackPlugin(),
// Copy static files
new CopyWebpackPlugin({
patterns: [
{ from: 'public', to: 'static' }
]
}),
// Define environment variables
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify('https://api.example.com')
}),
// Analyze bundle size
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
};
Development Server
Webpack Dev Server provides live reloading for a better development experience.
# Install webpack-dev-server
npm install --save-dev webpack-dev-server
# webpack.config.js
module.exports = {
devServer: {
static: './dist',
hot: true, // Hot Module Replacement
port: 3000, // Dev server port
open: true, // Open browser automatically
historyApiFallback: true // For SPAs
}
};
# package.json scripts
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}
Code Splitting
Code splitting is like creating multiple suitcases for different parts of your trip, loading only what you need when you need it.
Dynamic Imports
// Static import (included in main bundle)
import { heavyFunction } from './heavy-module';
// Dynamic import (separate chunk)
button.addEventListener('click', async () => {
const module = await import('./heavy-module');
module.heavyFunction();
});
// webpack.config.js for code splitting
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
Multiple Entry Points
module.exports = {
entry: {
home: './src/home.js',
about: './src/about.js',
contact: './src/contact.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
splitChunks: {
chunks: 'all',
name: 'common'
}
}
};
Environment Configuration
Environment Variables
# Install dotenv-webpack
npm install --save-dev dotenv-webpack
# webpack.config.js
const Dotenv = require('dotenv-webpack');
module.exports = {
plugins: [
new Dotenv({
systemvars: true // Load system environment variables
})
]
};
// .env file
API_URL=https://api.example.com
API_KEY=your-secret-key
// src/api.js
const apiUrl = process.env.API_URL;
const apiKey = process.env.API_KEY;
Multiple Configurations
// webpack.common.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist'
}
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map'
});
Performance Optimization
Bundle Analysis
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
// package.json
{
"scripts": {
"analyze": "webpack --mode production --analyze"
}
}
Tree Shaking
// package.json - mark as side-effect free
{
"name": "my-library",
"sideEffects": false
}
// Or specify files with side effects
{
"sideEffects": ["*.css", "*.scss"]
}
// webpack.config.js
module.exports = {
mode: 'production', // Tree shaking is enabled in production
optimization: {
usedExports: true
}
};
Caching
module.exports = {
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
Real-World Webpack Configuration
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
minify: isProduction
}),
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
].filter(Boolean),
optimization: {
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all'
}
},
devServer: {
static: './dist',
hot: true,
port: 3000,
historyApiFallback: true
},
devtool: isProduction ? 'source-map' : 'eval-source-map'
};
};
Practice Exercises
Exercise 1: Basic Setup
// Create a webpack project that:
// 1. Has src/index.js as entry point
// 2. Outputs to dist/bundle.js
// 3. Supports CSS files
// 4. Includes source maps for development
// 5. Has npm scripts for dev and build
Exercise 2: Asset Pipeline
// Enhance your webpack config to:
// 1. Process SCSS files
// 2. Handle image imports
// 3. Extract CSS to separate files in production
// 4. Add HTML template support
// 5. Implement cache busting with contenthash
Common Issues and Solutions
Key Takeaways
- Webpack creates a dependency graph and bundles modules
- Loaders transform non-JavaScript files into modules
- Plugins extend webpack's functionality
- Code splitting improves application performance
- Development server provides hot module replacement
- Environment-specific configurations optimize builds
- Tree shaking removes unused code in production