Webpack: Configuration, Loaders and Optimization
In this tutorial, you'll learn Webpack including loaders, plugins, Code Splitting, Tree Shaking, caching strategies, and performance optimization for production builds.
Why Webpack Matters
Webpack is the most widely adopted module bundler in the JavaScript ecosystem. It powers thousands of production applications. Understanding Webpack means you can configure builds from scratch, debug bundling issues, and optimize bundle size for performance-critical applications. Even with modern alternatives like Vite emerging, Webpack remains essential for legacy projects and complex enterprise setups.
By the end of this guide, you will configure Webpack from scratch, use loaders and plugins, split code effectively, and optimize builds.
What is Webpack?
Webpack is a static module bundler that takes modules with dependencies and generates static assets representing those modules. It treats everything as a module -- JavaScript, CSS, images, fonts, and HTML.
flowchart LR A[Entry: index.js] --> B[Webpack] B --> C[Loaders] C --> D[CSS Loader] C --> E[Babel Loader] C --> F[File Loader] B --> G[Plugins] G --> H[HtmlWebpackPlugin] G --> I[MinicssExtractPlugin] B --> J[Output: bundle.js]
Basic Setup
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install --save-dev webpack webpack-cli
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
mode: 'development',
}
Package.json Scripts
{
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}
}
Expected Output
$ npm run build
> webpack-demo@1.0.0 build
> webpack
asset bundle.js 21 KiB [emitted] (name: main)
./src/index.js 187 bytes [built] [code generated]
webpack 5.90.0 compiled successfully in 132 ms
Loaders
Loaders transform files before they are added to the bundle. They are the reason Webpack can handle CSS, images, TypeScript, and more.
Babel Loader (JavaScript/JSX)
npm install --save-dev babel-loader @babel/core @babel/preset-env
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['"@babel"/preset-env'],
},
},
},
],
},
}
CSS Loader
npm install --save-dev style-loader css-loader
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
}
The order matters: css-loader interprets @import and url() in CSS. style-loader injects CSS into the DOM.
Asset Modules (Images, Fonts)
Webpack 5 has built-in asset modules. No separate loader needed.
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
},
],
},
}
Plugins
Plugins extend Webpack's capabilities. They can optimize bundles, inject environment variables, generate HTML files, and more.
HtmlWebpackPlugin
npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'My App',
template: './src/index.html',
}),
],
}
MiniCssExtractPlugin
Extracts CSS into separate files instead of inlining in JavaScript.
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
}
Code Splitting
Code Splitting breaks your bundle into smaller chunks that can be loaded on demand.
Entry Points
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js',
},
output: {
filename: '[name].[contenthash].js',
},
}
Dynamic Imports
// src/index.js
async function loadAdminPanel() {
const module = await import('./adminPanel.js')
module.init()
}
document.getElementById('admin-btn').addEventListener('click', loadAdminPanel)
Webpack automatically creates a separate chunk for adminPanel.js.
SplitChunksPlugin
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
}
Tree Shaking
Tree Shaking removes unused code. It works with ES module syntax (import/export), not CommonJS.
// src/math.js
export function square(x) { return x * x }
export function cube(x) { return x * x * x }
// src/index.js
import { square } from './math'
console.log(square(3)) // cube is tree-shaken away
Expected Output
The production bundle will contain only the square function. The cube function is removed during the build.
Enable Tree Shaking in production:
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: false,
},
}
Caching Strategies
Use content hashes in filenames to enable long-term caching.
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
},
}
When file content changes, the hash changes. The browser fetches only the updated file.
Runtime Chunk
module.exports = {
optimization: {
runtimeChunk: 'single',
},
}
This extracts the Webpack runtime into its own chunk, so vendor hashes remain stable.
Production Optimization
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: { compress: { drop_console: true } },
}),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
})
Multi-Environment Config
// webpack.common.js
module.exports = {
entry: './src/index.js',
module: { /* shared rules */ },
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
}
// 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: {
port: 3000,
hot: true,
},
})
// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: { minimize: true },
})
Bundle Analysis
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
}
Run the build and the analyzer opens a dashboard showing bundle composition.
Common Errors
| Problem | Cause | Fix |
|---|---|---|
Module not found: Error: Can't resolve 'module' |
Missing npm package | Install the package with npm |
You may need an appropriate loader |
File type not handled | Add a loader rule for that file type |
Content hash changed despite no code change |
Module order changed | Use optimization.moduleIds: 'deterministic' |
MiniCssExtractPlugin doesn't work with style-loader |
Both cannot be used together | Use one or the other based on environment |
Entrypoint size exceeds the recommended limit |
Bundle too large | Enable Code Splitting and Tree Shaking |
Practice Questions
1. What is the purpose of loaders in Webpack?
Loaders transform files before they are added to the bundle (e.g., transpile TypeScript, process CSS).
2. How does Code Splitting improve performance?
It breaks the bundle into smaller chunks loaded on demand, reducing initial load time.
3. What is Tree Shaking and what syntax does it require?
Tree Shaking removes unused exports. It requires ES module syntax (import/export).
4. Why use contenthash in output filenames?
It creates unique filenames when content changes, enabling long-term browser caching.
5. What does the HtmlWebpackPlugin do?
It generates an HTML file that automatically includes the bundled scripts and stylesheets.
Challenge
Create a Webpack configuration for a TypeScript + React project with CSS modules, image handling, Code Splitting, and environment-specific configs. The production build should minify, tree-shake, and generate source maps.
Real-World Task
Take an existing JavaScript project and set up Webpack from scratch. Configure loaders for all file types used, enable Code Splitting for route-based pages, set up content hashing for caching, and analyze the bundle size. Reduce the total bundle size by at least 20% through Tree Shaking and Code Splitting.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro