Webpack is the most widely used JavaScript module bundler. This reference covers everything from the basic webpack.config.js to advanced code splitting and optimisation in Webpack 5.
Quick reference
The 25 Webpack patterns you use every day.
| Pattern | What it does |
|---|---|
npx webpack |
Bundle with default config |
mode: 'development' |
Fast build, readable output |
mode: 'production' |
Minified, optimised output |
entry: './src/index.js' |
Single entry point |
entry: { app, vendor } |
Multiple entry points |
output.filename: '[name].[contenthash].js' |
Cache-busting filenames |
output.clean: true |
Clear dist/ before each build |
module.rules |
Array of loader rules |
test: /\.css$/ |
Match files by extension regex |
use: ['style-loader', 'css-loader'] |
Chain loaders (right to left) |
plugins: [new HtmlWebpackPlugin()] |
Add plugins |
devServer: { hot: true } |
Enable HMR |
devtool: 'source-map' |
Full source maps |
devtool: 'eval-cheap-module-source-map' |
Fast dev source maps |
optimization.splitChunks |
Automatic code splitting |
import() |
Dynamic import (lazy chunk) |
resolve.alias |
Path aliases |
resolve.extensions |
Auto-resolve extensions |
externals |
Exclude from bundle (CDN libs) |
DefinePlugin |
Replace constants at build time |
MiniCssExtractPlugin |
Extract CSS to separate files |
webpack-bundle-analyzer |
Visualise bundle size |
module federation |
Share modules between apps |
cache: { type: 'filesystem' } |
Persistent build cache |
experiments.outputModule |
Output ESM bundles |
Installation
# Install webpack and CLI
npm install --save-dev webpack webpack-cli
# Install dev server for local development
npm install --save-dev webpack-dev-server
# Common loaders and plugins
npm install --save-dev \
babel-loader @babel/core @babel/preset-env @babel/preset-react \
css-loader style-loader \
html-webpack-plugin \
mini-css-extract-plugin \
css-minimizer-webpack-plugin
Add scripts to package.json:
{
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"start": "webpack serve --mode development",
"analyze": "webpack --mode production --env analyze"
}
}
Basic config
webpack.config.js — the minimal config that covers most projects:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// Build mode: 'development' | 'production' | 'none'
mode: 'development',
// Entry point(s)
entry: './src/index.js',
// Output
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true, // Clean dist/ before build (Webpack 5+)
publicPath: '/',
},
// Loaders
module: {
rules: [
// JavaScript + JSX via Babel
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: 'babel-loader',
},
// CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
// Images and fonts
{
test: /\.(png|jpg|gif|svg|woff2?|ttf|eot)$/,
type: 'asset/resource', // Webpack 5 built-in (replaces file-loader)
},
],
},
// Plugins
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
// Module resolution
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
// Source maps
devtool: 'eval-cheap-module-source-map',
// Dev server
devServer: {
port: 3000,
hot: true,
historyApiFallback: true, // SPA fallback for React Router
open: true,
},
};
Entry and output
// Single entry
entry: './src/index.js'
// Multiple entries (creates separate bundles)
entry: {
app: './src/app.js',
admin: './src/admin.js',
}
// Entry with dependencies
entry: {
app: {
import: './src/app.js',
dependOn: 'shared',
},
admin: {
import: './src/admin.js',
dependOn: 'shared',
},
shared: ['react', 'react-dom'],
}
// Output
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js', // Cache busting
chunkFilename: '[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[hash][ext]',
clean: true,
publicPath: 'auto', // Webpack 5: infer from document.currentScript
}
Loaders
Loaders transform files before they are bundled. They run right to left in a use array.
JavaScript: Babel
npm install --save-dev babel-loader @babel/core @babel/preset-env
// webpack.config.js
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
'@babel/preset-react',
'@babel/preset-typescript',
],
},
},
}
// babel.config.json (preferred over options inline)
{
"presets": [
["@babel/preset-env", { "targets": "defaults" }],
"@babel/preset-react",
"@babel/preset-typescript"
]
}
TypeScript: ts-loader
npm install --save-dev ts-loader typescript
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
Or use Babel with @babel/preset-typescript (no type-checking, but faster):
{
test: /\.tsx?$/,
use: 'babel-loader', // with @babel/preset-typescript in babel config
exclude: /node_modules/,
}
CSS
npm install --save-dev css-loader style-loader postcss-loader
npm install --save-dev mini-css-extract-plugin # Extract to file
// Development: inject into DOM via <style> tag
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}
// Production: extract to .css file
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
}
// With PostCSS (Tailwind, autoprefixer)
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['tailwindcss', 'autoprefixer'],
},
},
},
],
}
// CSS Modules
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true },
},
],
}
Sass/SCSS
npm install --save-dev sass sass-loader
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
}
Static assets (Webpack 5)
Webpack 5 replaces file-loader and url-loader with built-in Asset Modules:
// Emit a file and export the URL
{ test: /\.(png|jpg|gif)$/, type: 'asset/resource' }
// Export as data URI (base64)
{ test: /\.svg$/, type: 'asset/inline' }
// Auto: small files → inline, large files → resource
{
test: /\.(png|jpg)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 }, // 8 KB threshold
},
}
// Raw file content as string
{ test: /\.txt$/, type: 'asset/source' }
Plugins
HtmlWebpackPlugin
npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({
template: './public/index.html', // Your HTML template
title: 'My App', // Inject into <title>
favicon: './public/favicon.ico',
minify: {
collapseWhitespace: true,
removeComments: true,
},
})
MiniCssExtractPlugin
npm install --save-dev mini-css-extract-plugin css-minimizer-webpack-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css',
}),
],
optimization: {
minimizer: [
'...', // Keep default JS minimizer (TerserPlugin)
new CssMinimizerPlugin(),
],
},
DefinePlugin
Replace constants at compile time:
const { DefinePlugin } = require('webpack');
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.API_URL': JSON.stringify(process.env.API_URL),
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
__VERSION__: JSON.stringify(require('./package.json').version),
})
CopyWebpackPlugin
npm install --save-dev copy-webpack-plugin
const CopyPlugin = require('copy-webpack-plugin');
new CopyPlugin({
patterns: [
{ from: 'public/robots.txt', to: 'robots.txt' },
{ from: 'public/manifest.json' },
],
})
Mode and environments
Use a function to return different configs per environment:
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isDev = argv.mode === 'development';
return {
mode: argv.mode,
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
module: {
rules: [
{
test: /\.css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
...(!isDev ? [new MiniCssExtractPlugin()] : []),
],
};
};
Or merge separate configs:
npm install --save-dev webpack-merge
// webpack.common.js
const common = { entry: './src/index.js', /* ... */ };
// webpack.dev.js
const { merge } = require('webpack-merge');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: { hot: true, port: 3000 },
});
// webpack.prod.js
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
});
// package.json
{
"scripts": {
"start": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}
}
Dev server
devServer: {
port: 3000,
host: '0.0.0.0', // Expose to network
hot: true, // Hot Module Replacement
open: true, // Open browser on start
compress: true, // gzip responses
historyApiFallback: true, // SPA: serve index.html for all routes
// Proxy API calls to backend
proxy: [
{
context: ['/api'],
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
],
// Serve static files
static: {
directory: path.resolve(__dirname, 'public'),
},
// Custom headers
headers: {
'Access-Control-Allow-Origin': '*',
},
}
Source maps
devtool value |
Build speed | Rebuild speed | Quality | Use case |
|---|---|---|---|---|
false |
Fastest | Fastest | None | Production (no maps) |
eval |
Fastest | Fastest | Low | Very fast builds |
eval-cheap-module-source-map |
Fast | Fast | Medium | Dev recommended |
cheap-module-source-map |
Medium | Fast | Medium | CI builds |
source-map |
Slow | Slow | Best | Production recommended |
hidden-source-map |
Slow | Slow | Best | Production + Sentry |
nosources-source-map |
Slow | Slow | No source | Production (hide source) |
Code splitting
Dynamic imports (lazy chunks)
// React lazy loading
import React, { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Router>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Router>
</Suspense>
);
}
// Named chunks for better debugging
const module = await import(/* webpackChunkName: "analytics" */ './analytics');
SplitChunksPlugin (automatic vendor splitting)
optimization: {
splitChunks: {
chunks: 'all', // Split both sync and async chunks
cacheGroups: {
// Put all node_modules in one vendor chunk
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
// Separate React from other vendors
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20,
},
// Bundle shared code used by 2+ chunks
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
// Extract webpack runtime to its own chunk
runtimeChunk: 'single',
}
Prefetch and preload
// Prefetch (low priority, loaded during idle)
import(/* webpackPrefetch: true */ './HeavyModal');
// Preload (high priority, fetched in parallel with parent)
import(/* webpackPreload: true */ './CriticalChart');
Environment variables
# .env file (use dotenv-webpack)
npm install --save-dev dotenv-webpack
const Dotenv = require('dotenv-webpack');
plugins: [
new Dotenv({
path: `./.env.${argv.mode}`, // .env.development or .env.production
safe: true, // Require .env.example
systemvars: true, // Also load from process.env
}),
]
# .env.development
API_URL=http://localhost:8080
FEATURE_FLAGS=debug,verbose
# .env.production
API_URL=https://api.example.com
// Access in code
const url = process.env.API_URL;
TypeScript
npm install --save-dev typescript ts-loader
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
// webpack.config.js (using ts-loader)
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // Skip type-checking (use tsc separately for CI)
},
},
],
exclude: /node_modules/,
}
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: { '@': path.resolve(__dirname, 'src') },
}
React setup
npm install react react-dom
npm install --save-dev @babel/preset-react
// babel.config.json
{
"presets": [
["@babel/preset-env", { "targets": "defaults" }],
["@babel/preset-react", { "runtime": "automatic" }]
]
}
// webpack.config.js (minimal React setup)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true,
},
module: {
rules: [
{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
resolve: { extensions: ['.js', '.jsx'] },
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
devServer: { port: 3000, hot: true, historyApiFallback: true },
};
Bundle analysis
npm install --save-dev webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = (env) => ({
plugins: [
...(env.analyze ? [new BundleAnalyzerPlugin()] : []),
],
});
npm run build -- --env analyze
# Opens interactive treemap in browser
Module federation (Webpack 5)
Share code between separately deployed apps (micro-frontends):
// Shell app (host)
const { ModuleFederationPlugin } = require('webpack').container;
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@http://localhost:3001/remoteEntry.js',
catalog: 'catalog@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
})
// Use remote component
const CheckoutWidget = lazy(() => import('checkout/CheckoutWidget'));
// Checkout app (remote)
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutWidget': './src/components/CheckoutWidget',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
Performance optimisation
// Production config optimisations
module.exports = {
mode: 'production',
optimization: {
// Tree shaking (automatic in production)
usedExports: true,
sideEffects: true, // Honour package.json "sideEffects" field
// Minification
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { drop_console: true },
},
}),
new CssMinimizerPlugin(),
],
// Code splitting
splitChunks: { chunks: 'all' },
runtimeChunk: 'single',
},
// Persistent filesystem cache (Webpack 5 — huge speed boost)
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
// Only rebuild changed modules
snapshot: {
managedPaths: [path.resolve(__dirname, 'node_modules')],
},
}
Exclude large libraries (use CDN)
// webpack.config.js
externals: {
react: 'React',
'react-dom': 'ReactDOM',
lodash: '_',
}
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
Common patterns
Alias @/ to src/
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
}
// Usage in code
import Button from '@/components/Button';
import { formatDate } from '@utils/date';
Handle SVG as React components
npm install --save-dev @svgr/webpack
{
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: { svgoConfig: { plugins: [{ name: 'removeViewBox', active: false }] } },
},
],
}
import { ReactComponent as Logo } from './logo.svg';
// or (Webpack 5 named export)
import Logo from './logo.svg?react';
Multi-page application (MPA)
const pages = ['index', 'about', 'contact'];
module.exports = {
entry: Object.fromEntries(pages.map((p) => [p, `./src/pages/${p}.js`])),
plugins: pages.map(
(p) =>
new HtmlWebpackPlugin({
filename: `${p}.html`,
template: `./public/${p}.html`,
chunks: [p],
}),
),
};
Progress output
const { ProgressPlugin } = require('webpack');
plugins: [new ProgressPlugin()]
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using file-loader / url-loader in Webpack 5 |
Deprecated, warnings | Use type: 'asset/resource' / 'asset/inline' |
| Loaders in wrong order | CSS not applied | use: ['style-loader', 'css-loader'] (right→left) |
output.publicPath mismatch |
Assets 404 in production | Set publicPath: '/' or 'auto' |
No contenthash in filename |
Browser caches stale bundles | Use [contenthash] in filenames |
Committing dist/ to git |
Large repo, merge conflicts | Add dist/ to .gitignore |
No exclude: /node_modules/ on babel-loader |
Extremely slow builds | Always exclude node_modules |
DefinePlugin without JSON.stringify |
Runtime errors | JSON.stringify('value') not 'value' |
Forgetting runtimeChunk: 'single' |
Long-term cache broken | Add to optimization in production |
Webpack vs other bundlers
| Feature | Webpack 5 | Vite | Parcel | esbuild |
|---|---|---|---|---|
| Config complexity | High | Low | Zero | Low |
| Dev server speed | Moderate | Very fast (native ESM) | Fast | Very fast |
| Prod build speed | Slow | Fast (Rollup) | Moderate | Fastest |
| Ecosystem maturity | Most plugins | Growing | Mature | Limited |
| Code splitting | Excellent | Good | Good | Basic |
| Module federation | Yes (native) | Via plugin | No | No |
| Legacy browser support | Best | Via plugin | Good | Limited |
| Config language | JS / TS | TS first | Auto-detect | Go / JS API |
| Best for | Complex enterprise apps | Modern SPAs | Zero-config | Libraries |
CLI flags
| Command | What it does |
|---|---|
webpack |
Build once |
webpack serve |
Start dev server |
webpack --watch |
Watch mode (rebuild on change) |
webpack --config webpack.prod.js |
Custom config file |
webpack --mode production |
Override mode |
webpack --profile --json > stats.json |
Generate build stats |
webpack --analyze |
(needs env variable or plugin) |
webpack --env key=value |
Pass env variable to config function |
webpack --progress |
Show build progress |
webpack --display-error-details |
Verbose error output |
FAQ
Q: Should I use Webpack or Vite in 2025?
For new projects, Vite is generally the better choice: faster dev server via native ES modules, simpler config, and comparable production output. Use Webpack when you need advanced features (Module Federation, complex multi-page setups, legacy browser support) or are maintaining an existing Webpack project.
Q: What's the difference between webpack and webpack-cli?
webpack is the core bundler; webpack-cli provides the command-line interface. You need both to run npx webpack from the terminal. For programmatic use, you only need webpack.
Q: How do I speed up slow Webpack builds?
- Add
cache: { type: 'filesystem' }(Webpack 5 — biggest win). - Use
ts-loaderwithtranspileOnly: true(skip type-checking during build). - Exclude
node_modulesfrom babel-loader. - Use
thread-loaderfor CPU-intensive loaders. - Use
eval-cheap-module-source-mapin dev (notsource-map).
Q: Why does process.env.NODE_ENV not work in my frontend code?
Webpack doesn't automatically inject process.env. You must use DefinePlugin:
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
Vite automatically replaces import.meta.env.MODE — Webpack requires the explicit plugin setup.
Q: What's tree shaking and does Webpack do it automatically?
Tree shaking removes unused exports from your bundle. Webpack does it automatically in mode: 'production' for ES modules. For it to work: 1) use ES module syntax (import/export, not require/module.exports), 2) add "sideEffects": false (or a list of side-effectful files) in package.json of your libraries.
Q: How do I handle environment-specific API URLs?
Use dotenv-webpack with .env.development and .env.production files, or use DefinePlugin with process.env.API_URL set via the build command:
API_URL=https://api.example.com npm run build
Then in webpack.config.js:
new DefinePlugin({ 'process.env.API_URL': JSON.stringify(process.env.API_URL) })