webpack
webpack is the module bundler that defined the category: it walks your import graph, runs every non-JS file (CSS, images, fonts, WASM) through configurable loaders, and packs the result into optimized bundles with code splitting, lazy loading, and long-term caching hashes. Almost anything is possible through its loader and plugin system, which is exactly why configs grow into hundreds of lines. In 2026 it is the incumbent, not the default for new work: it still powers a huge share of production apps and frameworks built years ago, and it remains the tool with the deepest escape hatches (module federation, custom chunk graphs) when simpler bundlers hit their limits.
Still the most capable and battle-tested bundler, and the wrong default for new projects. Keep it where it already works or where you need module federation; reach for Vite or Rspack otherwise.
Use it if
- You maintain an existing webpack-based app; migrating a working production build rarely pays for itself
- You need module federation to share code between separately deployed micro-frontends, which is still webpack's home turf
- You need fine-grained control over chunking, caching, or an unusual asset pipeline that Vite's plugin model cannot express
- Your framework or platform (older Next.js versions, many enterprise toolchains) is built on it and you just need to extend the config
- You are starting a new browser app: Vite gives you a dev server that starts in milliseconds and a fraction of the config, and it is the current community default
- Build speed matters and your team is large: webpack cold builds and rebuilds are noticeably slower than esbuild, Rspack, or Vite on the same codebase
- You want low-maintenance tooling: a typical setup needs webpack, webpack-cli, webpack-dev-server, html-webpack-plugin, plus a loader per file type, each with its own release cycle and breaking changes
- You are bundling a library or a Node service; esbuild or rollup produce cleaner output with far less ceremony
- You expect config to be learnable in an afternoon; the options surface is enormous and error messages frequently point at the wrong layer (loader vs plugin vs resolver)
Setup reality
Nothing works out of one install. You need webpack plus webpack-cli, then webpack-dev-server for local dev, then html-webpack-plugin to get an HTML file, then loaders for every file type (css-loader plus style-loader just for plain CSS, ts-loader or babel-loader for TypeScript). Each piece versions independently and major bumps regularly break configs. The config file itself is JavaScript with deeply nested union-typed options; expect to copy patterns from the docs and adjust until the errors stop. Node >=10.13 is the floor for webpack itself, but the plugin ecosystem effectively requires a modern LTS.
Patterns
Minimal production configminimal-config
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
}mode is required in practice; omitting it triggers a warning and defaults to production without env-specific optimizations you probably expect.
Local dev server with live reloaddev-server
// webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.js',
devServer: {
static: './dist',
port: 3000,
hot: true,
},
}
// run: npx webpack servewebpack-dev-server is a separate install; v4/v5 of it renamed contentBase to static, which breaks most old tutorials.
Import CSS in JavaScriptcss-loading
// npm i -D css-loader style-loader
module.exports = {
module: {
rules: [
{ test: /\.css$/i, use: ['style-loader', 'css-loader'] },
],
},
}Loader order is right to left: css-loader parses, style-loader injects; swap MiniCssExtractPlugin.loader for style-loader in production to get real .css files.
Compile TypeScripttypescript
// npm i -D ts-loader typescript
module.exports = {
entry: './src/index.ts',
module: {
rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }],
},
resolve: { extensions: ['.ts', '.tsx', '.js'] },
}Without resolve.extensions webpack will not find extensionless TS imports; use transpileOnly plus fork-ts-checker-webpack-plugin for faster builds.
Lazy-load a route with dynamic importcode-splitting
button.addEventListener('click', async () => {
const { renderChart } = await import('./chart')
renderChart()
})Each dynamic import() becomes its own chunk automatically; name it with /* webpackChunkName: "chart" */ if you need stable filenames.
Split vendor code for cachingvendor-chunk
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
}chunks: 'all' is the one-line version; the default only splits async chunks, which surprises people checking bundle sizes.
Handle images and fonts without loadersasset-modules
module.exports = {
module: {
rules: [
{ test: /\.(png|svg|jpg|woff2?)$/i, type: 'asset/resource' },
],
},
}Asset modules (webpack 5) replace file-loader and url-loader; type: 'asset' auto-inlines small files as data URIs.
Inject environment variablesdefine-env
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
],
}DefinePlugin does textual substitution, so always JSON.stringify string values or you inject a bare identifier.
Generate the HTML entry filehtml-output
// npm i -D html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
}Without this plugin hashed bundle filenames are useless, since nothing rewrites the script tags.
Path aliases for importsresolve-alias
const path = require('path')
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}TypeScript does not read webpack aliases; mirror them in tsconfig paths or the editor will show phantom errors.
Pick the right source map per environmentsource-maps
module.exports = (env, argv) => ({
devtool: argv.mode === 'production'
? 'source-map'
: 'eval-cheap-module-source-map',
})devtool strings are a performance/fidelity tradeoff matrix; 'eval' variants rebuild fastest, 'source-map' is the only one safe to ship.
See what is inside the bundlebundle-analysis
// npm i -D webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [new BundleAnalyzerPlugin()],
}Run it before optimizing anything; the biggest wins are usually one accidentally bundled dependency, not config tuning.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| vite | npm | Any new browser app; instant dev server, sane defaults, and the ecosystem has moved there |
| @rspack/core | npm | You want webpack-compatible config and plugins with much faster Rust-based builds; built as a drop-in migration path |
| esbuild | npm | Bundling Node services or libraries where you want speed and near-zero config over extensibility |
| parcel | npm | Small projects where you want bundling with no config file at all |