cheat sheet
webpack
Package-level reference for webpack on npm — loaders, plugins, code splitting, asset modules, module federation, and the slow migration toward Rspack and Vite.
webpack
What it is
webpack is the classic JavaScript module bundler — the tool that made it possible to write import statements across thousands of files and ship one (or a few) optimised browser bundles. It pioneered loaders (transform any file type into a module), plugins (hook into the build lifecycle), code splitting, tree shaking, and module federation. For most of the 2010s, "JavaScript build" meant webpack.
It's no longer the default for new apps — Vite, Rspack, Rolldown, and esbuild-based pipelines have eaten that market. But it remains overwhelmingly the most-used bundler in production: every Create React App descendant, the older Next.js builds, most enterprise React/Vue/Angular setups, and every project that depends on a webpack-only loader or plugin is still on webpack. Its plugin ecosystem is the largest of any bundler — by an order of magnitude.
Install
# Project-local (always — webpack should never be global)
npm install -D webpack webpack-cli
pnpm add -D webpack webpack-cli
yarn add -D webpack webpack-cli
Output: webpack binary in node_modules/.bin/; ready to run via npx webpack or in npm scripts.
# Dev server (separate package)
npm install -D webpack-dev-server
Output: adds the webpack serve subcommand for local development with HMR.
# Common companion loaders/plugins
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react
npm install -D css-loader style-loader mini-css-extract-plugin
npm install -D html-webpack-plugin
Output:
added 84 packages in 12s
added 5 packages in 2s
added 1 package in 1s
Versioning & Node support
- Current line is
5.x(released October 2020). Long-running 5.x — no 6.x announced as of writing. - Requires Node 10.13+ for webpack 5; in practice all modern Node releases work.
webpack-cliis a separate package; pin both to the same lockfile.- SemVer respected for the core, but loaders and plugins often have their own breaking changes. Pin everything in lockfile.
- Long-term maintenance is steady. Public roadmap: the team focuses on Rspack (drop-in Rust port) as the path forward, while webpack itself receives bug-fix releases.
Package metadata
- Maintainers: Tobias Koppers + webpack core team / OpenJS Foundation
- Project home: github.com/webpack/webpack
- Docs: webpack.js.org
- npm: npmjs.com/package/webpack
- License: MIT
- First released: 2012
- Downloads: still ~30M+ weekly — top-tier despite the shift to Vite for new projects.
Peer dependencies & extras
webpack itself has zero peers. Its plugin ecosystem is huge — a few that nearly every project uses:
| Package | Role |
|---|---|
webpack-cli | CLI front-end for the webpack binary; required for npx webpack. |
webpack-dev-server | Local dev with HMR and proxy. |
babel-loader | Transpile JS/TS/JSX via Babel. Most common loader. |
ts-loader / swc-loader / esbuild-loader | TypeScript transpilers; swc-loader is the fastest. |
css-loader, style-loader, sass-loader, postcss-loader | CSS pipeline. |
mini-css-extract-plugin | Emit CSS to separate .css files in prod. |
html-webpack-plugin | Generate index.html referencing emitted assets. |
terser-webpack-plugin | JS minifier (default in prod mode). |
webpack-bundle-analyzer | Treemap of bundle composition. |
@module-federation/* | Module Federation runtime + helpers (now also works in Rspack and Vite). |
Alternatives
| Tool | Trade-off |
|---|---|
| Vite | ESM-first dev server, Rollup-based prod builds. Default for new SPAs. Drops some webpack-specific patterns (Module Federation MVP works but matured slower). |
| Rspack | Rust port of webpack with API compatibility. ~10× faster builds. The recommended migration for performance-sensitive webpack projects. |
| Rolldown | Rust rewrite of Rollup. Aiming for Rollup-compat first; webpack-compat is a stretch goal. |
| esbuild | Single-binary Go bundler. Extremely fast; minimal plugin ecosystem; less ergonomic for SPA dev. |
| Parcel | Zero-config bundler. Great for prototypes; smaller plugin ecosystem. |
| Turbopack | Rust bundler from Vercel, default in Next.js. Tied to Next; not yet a general drop-in. |
| swc / oxc | Transformers, not bundlers — usable inside webpack via loaders. |
Real-world recipes
Minimal webpack config
// webpack.config.js
const path = require("node:path");
module.exports = {
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.[contenthash].js",
clean: true,
},
};
npx webpack
Output:
asset bundle.f3a9c2d8.js 1.2 KiB [emitted] [immutable]
webpack 5.96.0 compiled successfully in 154 ms
The [contenthash] placeholder gives long-cache filenames; clean: true wipes dist/ before each build.
Loader chain — TypeScript + CSS + assets
// webpack.config.js
module.exports = {
mode: "development",
entry: "./src/index.tsx",
module: {
rules: [
{
test: /\.tsx?$/,
use: [{ loader: "swc-loader", options: { jsc: { parser: { syntax: "typescript", tsx: true } } } }],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(png|jpg|svg)$/,
type: "asset/resource",
},
],
},
resolve: { extensions: [".ts", ".tsx", ".js"] },
};
Loaders apply right-to-left: css-loader parses imports, postcss-loader transforms, style-loader injects <style> at runtime. Reverse order = silent failure.
Code splitting
// webpack.config.js
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
name: "react",
chunks: "all",
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: "all",
priority: -10,
},
},
},
},
};
// src/index.js — dynamic import emits a separate chunk
const Settings = () => import(/* webpackChunkName: "settings" */ "./Settings");
Output: three chunks — react.js, vendor.js, settings.js — loaded on demand.
Tree shaking
// webpack.config.js — production mode enables this by default
module.exports = {
mode: "production",
optimization: {
usedExports: true,
sideEffects: true,
},
};
// package.json — tell webpack which files have side effects
{
"sideEffects": ["*.css", "./src/polyfills.js"]
}
sideEffects: false for the whole package means webpack can drop any unused export. "sideEffects": [...] whitelists files that must remain even when no export is imported (e.g. CSS, polyfills, automatic registration).
Multi-target build
// webpack.config.js — array config builds multiple bundles in one run
module.exports = [
{
name: "web",
target: "web",
entry: "./src/web/index.ts",
output: { path: __dirname + "/dist/web", filename: "bundle.js" },
},
{
name: "node",
target: "node",
entry: "./src/server/index.ts",
output: { path: __dirname + "/dist/server", filename: "bundle.cjs" },
externals: ["express"],
},
];
npx webpack --config-name web # build a single target by name
Output:
[web] asset bundle.js 154 KiB
[node] asset bundle.cjs 89 KiB
Module Federation (multi-app composition)
// host webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
checkout: "checkout@https://checkout.example.com/remoteEntry.js",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
],
};
// host application code
const Checkout = React.lazy(() => import("checkout/CartPage"));
Module Federation is webpack's killer feature for micro-frontends — runtime composition of independently-deployed apps sharing dependencies. Rspack mirrors the API; Vite has a community plugin (@originjs/vite-plugin-federation) that's close but not perfect.
Production deployment
// webpack.prod.js
module.exports = {
mode: "production",
devtool: "source-map", // separate .map; do NOT ship
output: {
filename: "[name].[contenthash].js",
publicPath: "/assets/",
},
optimization: {
minimize: true,
runtimeChunk: "single",
moduleIds: "deterministic",
},
performance: {
hints: "warning",
maxAssetSize: 250_000,
maxEntrypointSize: 250_000,
},
};
Run:
NODE_ENV=production npx webpack --config webpack.prod.js
Output:
asset main.f3a9c2d8.js 47 KiB [emitted] [immutable] [minimized]
asset main.f3a9c2d8.js.map 144 KiB [emitted] [dev]
asset vendor.b1d4ac88.js 89 KiB [emitted] [immutable] [minimized]
webpack 5.96.0 compiled with 2 warnings in 1893 ms
- Always hash filenames + immutable cache headers for the assets,
no-cacheonindex.html. devtool: "source-map"emits separate.mapfiles; upload to your error tracker (Sentry / Datadog), don't ship publicly.runtimeChunk: "single"extracts the runtime so app code can change without invalidating the runtime chunk.
Performance tuning
webpack is slow by default. The five knobs that matter most:
Swap babel-loader for swc-loader or esbuild-loader
{
test: /\.tsx?$/,
use: { loader: "swc-loader" }, // ~5–10× faster than babel-loader
}
This is usually the single biggest speedup — Babel parses every file with a slow JS-implemented AST.
cache: { type: "filesystem" }
module.exports = {
cache: {
type: "filesystem",
buildDependencies: { config: [__filename] },
},
};
Persists compiled artefacts across builds. First build is normal; subsequent builds are ~3× faster.
Narrow the loader test scope
{
test: /\.tsx?$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
// ...
}
include is faster than exclude (positive matching short-circuits). Excluding node_modules is almost always correct unless you have an ESM-only dep that needs transpiling.
Parallel minification
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [new TerserPlugin({ parallel: true })],
},
};
Default parallel: true since webpack 5 — confirm in your config that no other minimizer overrides it.
Profile the build
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json
Output: opens a treemap visualisation. Look for huge transitive deps (lodash, moment, three.js) and replace with smaller alternatives.
Version migration guide
| From → To | Highlights |
|---|---|
| 3 → 4 | `mode: "development" |
| 4 → 5 | Big migration. Node polyfills removed (Buffer, process, path no longer auto-bundled — must resolve.fallback). Asset Modules replaced file-loader / url-loader / raw-loader. Module Federation introduced. Long-term caching by default. |
| 5.x ongoing | Minor releases add Rspack-compatibility shims. Stable since 2020. No 6.x roadmap. |
The 4→5 migration is the painful one. Common breakages:
Module not found: Can't resolve 'path' (or 'crypto')— addresolve.fallback: { path: require.resolve("path-browserify"), ... }or remove the Node-only import from browser code.file-loader/url-loaderno longer auto-installed — replace with Asset Modules (type: "asset/resource",type: "asset/inline").IgnorePluginconstructor signature changed.
Security considerations
- Supply-chain risk in loaders/plugins. webpack's plugin API runs full Node — a malicious loader can read your filesystem, exfiltrate env vars, or inject into emitted bundles. Pin loader versions in lockfile; audit suspicious additions.
- Bundle source maps.
devtool: "source-map"writes separate.mapfiles; do NOT ship them publicly. Either upload to Sentry/Datadog and skip the public copy, or usedevtool: "hidden-source-map"(no//# sourceMappingURLcomment). DefinePluginsubstitutions are literal.new DefinePlugin({ API_KEY: process.env.API_KEY })inlines whateverAPI_KEYis at build time into bundle source. NeverDefinePluginserver secrets.webpack-dev-serverexposure.host: "0.0.0.0"binds on every interface. Anyone on your LAN can hit the dev server and download source. Default tolocalhost.- Module Federation cross-origin trust. Loading a federated remote runs code from another origin in your security context. Pin the remoteEntry URL, enforce CSP, audit the remote's deploy process.
- CSP and inline scripts. Default webpack output includes an inline runtime — incompatible with strict CSP. Use
output.scriptType: "module"+runtimeChunk: "single"and serve the runtime via<script src=...>.
Testing & CI integration
webpack isn't a test runner. Pair with:
- Jest — has its own transform pipeline (
babel-jest/ts-jest); doesn't use webpack. - Vitest / Playwright — completely independent of webpack.
- storybook — uses webpack (or Vite) under the hood for its dev server.
CI integration is just npx webpack --config webpack.prod.js && verify — make sure the build is reproducible and fail the pipeline on warnings if your config opts into strict performance budgets.
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx webpack --config webpack.prod.js
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/ }
Ecosystem integrations
- React — Create React App, Next.js (legacy webpack mode), Storybook.
- Vue — Vue CLI (now in maintenance; Vite is the recommended path).
- Angular — historically uses webpack via
@angular-devkit/build-angular; newer versions also offer esbuild. - NestJS — uses webpack for the
--webpackbuild flag (alternative totsc-based output). - Module Federation — webpack's killer feature. Rspack now matches the API; Vite has community plugins.
- Storybook — supports both webpack and Vite; webpack remains the default for many addons.
Troubleshooting common errors
Module not found: Can't resolve 'X'
The import path doesn't resolve. Causes:
- Missing
npm install X. resolve.extensionsdoesn't include the file's extension (.ts,.tsx).resolve.aliasmismatched a real path.- Node-only module imported from browser code — add to
resolve.fallbackor remove the import.
You may need an appropriate loader to handle this file type
A loader rule's test regex doesn't match the file. Either fix the regex or add a new rule. Common pitfall: forgetting .tsx? to cover both .ts and .tsx.
Build is slow (10+ seconds for a small project)
Almost always babel-loader on every file. Switch to swc-loader or esbuild-loader. Enable cache: { type: "filesystem" }.
Cannot read properties of undefined (reading 'tap')
A plugin is expected by another plugin but isn't loaded — usually a plugin order issue. Check that all plugins are constructed (new MyPlugin()), not just referenced.
Hot Module Replacement doesn't fire
webpack-dev-server must be in HMR mode (hot: true), and a module must call module.hot.accept(). Framework loaders (react-refresh-webpack-plugin) wire this for components; arbitrary code falls back to full reload.
Bundle is huge despite tree shaking
Mostly: a transitive dep is CJS-only (CJS exports can't be tree-shaken). Use webpack-bundle-analyzer to find the culprit; swap for an ESM-only alternative (lodash → lodash-es, date-fns already ESM, moment → dayjs).
When NOT to use this
- New SPAs. Vite is faster, simpler, ESM-first. Pick it for green-field projects.
- Performance-critical builds. Rspack is a near-drop-in webpack-API-compatible Rust port. ~10× faster builds. The recommended migration target for "we love our webpack config, hate the build time" teams.
- Pure libraries. Rollup or tsup produces cleaner outputs (smaller, with proper ESM
exports). - Server-side bundling. Most Node code doesn't need bundling; ship source. Bundle only if the runtime requires it (Lambdas with cold-start constraints, edge workers).
- Greenfield with no Module Federation need. webpack's main moat is the plugin/loader catalogue and Module Federation. Without those, Vite is strictly easier.
See also
- JavaScript: webpack — CLI commands, config patterns, dev server
- JavaScript: modules — CJS/ESM interop webpack handles
- Packages: npm-vite — the modern alternative
- Concept: HTTP — chunk loading, publicPath, dev server