Webpack Cheatsheet - Frontend Build Tool Configuration Reference
Webpack’s power is real and its config is where that power lives or hides. This reference covers entry/output, Loaders for each asset type, Plugins, dev-server/HMR, code splitting and optimizations, plus mode/environment. Use it when a new asset type won't load, HMR dies, or a bundle is inexplicably huge. Rather than echo boilerplate, it points to the flag or plugin that addresses the failure. After reading you configure and tune a build without reading the full Webpack docs.
Typical Use Case
For developers setting up front-end tooling from scratch, or inheriting a build configuration nobody fully understands. The difficulty is not the commands but how options couple: entry sets output granularity, output.publicPath decides where chunks load from at runtime, splitChunks with runtimeChunk governs cache hits and first-paint weight, devServer.proxy decides whether local requests reach the backend. Seven sections run in pipeline order with paste-ready snippets, and the pitfalls collect recurring failures: churning content hashes, reversed loader order, tree shaking that never triggers.
Entry & Output 6
entry: "./src/index.js"entry: { app: "./src/app.js", vendor: "./src/vendor.js" }output.filename: "[name].[contenthash].js"output.path: path.resolve(__dirname, "dist")output.publicPath: "/assets/"output.clean: trueLoaders 8
{ test: /\.css$/, use: ["style-loader", "css-loader"] }{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] }{ test: /\.js$/, exclude: /node_modules/, use: "babel-loader" }{ test: /\.tsx?$/, use: "ts-loader" }{ test: /\.(png|jpg|gif)$/, type: "asset/resource" }{ test: /\.svg$/, type: "asset/source" }{ test: /\.(woff2?|ttf|eot)$/, type: "asset/resource" }{ test: /\.(js|ts)$/, enforce: "pre", use: "source-map-loader" }Plugins 7
new HtmlWebpackPlugin({ template: "./src/index.html" })new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })new DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })new CopyWebpackPlugin({ patterns: [{ from: "public", to: "." }] })new ProvidePlugin({ _: "lodash" })new HotModuleReplacementPlugin()new BannerPlugin({ banner: "Built at " + new Date().toISOString() })Dev Server 6
devServer.hot: truedevServer.historyApiFallback: truedevServer.port: 5000, open: truedevServer.static: path.join(__dirname, "public")devServer.proxy: { "/api": "http://localhost:3000" }devServer.proxy: { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } }Optimization 7
optimization.splitChunks: { chunks: "all" }optimization.minimize: trueoptimization.minimizer: [new TerserPlugin({ extractComments: false })]optimization.runtimeChunk: "single"import("./module").then(m => m.default)/* webpackChunkName: "vendor" *//* webpackPrefetch: true */Mode & Environment 6
mode: "production" | "development" | "none"webpack --config webpack.prod.js --mode productionmodule.exports = (env, argv) => ({ mode: argv.mode })devtool: argv.mode === "development" ? "eval-cheap-source-map" : "source-map"const { merge } = require("webpack-merge"); merge(common, prodConfig)new webpack.EnvironmentPlugin(["NODE_ENV", "API_URL"])Build Analysis 5
new BundleAnalyzerPlugin({ analyzerMode: "static" })stats: "minimal" | "normal" | "detailed"performance: { hints: "warning", maxAssetSize: 250000 }webpack --json > stats.jsondevtool: "source-map" | "eval-cheap-module-source-map"Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
entry | Declares the bundle entry point; accepts either a string or a map of named entries | entry: { app: "./src/app.js", vendor: "./src/vendor.js" } |
output.filename: "[name].[contenthash].js" | Puts a content hash in the output filename, which is what makes far-future caching viable | output.filename: "[name].[contenthash].js" |
output.path | Directory that receives the bundle; must be an absolute path | output.path: path.resolve(__dirname, "dist") |
output.publicPath | Public path prefix the runtime uses when it loads chunks and assets | output.publicPath: "/assets/" |
output.clean: true | Empties the dist directory before every build (built into Webpack 5) | output.clean: true |
mode | The built-in modes: production, development or none | mode: "production" |
devtool | Chooses the source map flavour, trading build speed against debug fidelity | devtool: "source-map" |
{ test: /\.css$/, use: [...] } | Matches files by extension and assigns the loader chain that runs on them | { test: /\.css$/, use: ["style-loader", "css-loader"] } |
MiniCssExtractPlugin.loader | Extracts CSS into a standalone file instead of injecting it at runtime | { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] } |
exclude: /node_modules/ | Skips directories entirely, so third-party code is never processed | { test: /\.js$/, exclude: /node_modules/, use: "babel-loader" } |
type: "asset/resource" | Webpack 5 asset module: emits a separate file and hands back its URL | { test: /\.(png|jpg|gif)$/, type: "asset/resource" } |
type: "asset/source" | Asset module: inlines the file contents as a plain string | { test: /\.svg$/, type: "asset/source" } |
enforce: "pre" | Runs the loader ahead of the ordinary loader phase | { test: /\.(js|ts)$/, enforce: "pre", use: "source-map-loader" } |
HtmlWebpackPlugin({ template }) | Generates the HTML from a template and injects references to the built assets | new HtmlWebpackPlugin({ template: "./src/index.html" }) |
MiniCssExtractPlugin({ filename }) | Pairs with the extraction loader to emit a content-hashed stylesheet | new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }) |
DefinePlugin | Substitutes constants into the bundle at compile time, the usual way to inject environment checks | new DefinePlugin({ ... }) |
EnvironmentPlugin([...]) | Injects a whole batch of environment variables as globals in a single call | new EnvironmentPlugin(["NODE_ENV", "API_URL"]) |
CopyWebpackPlugin({ patterns }) | Copies files that need no compilation straight into the output directory | new CopyWebpackPlugin({ patterns: [{ from: "public", to: "." }] }) |
ProvidePlugin | Injects module references automatically, sparing you the per-file imports | new ProvidePlugin({ _: "lodash" }) |
BannerPlugin({ banner }) | Prepends a comment banner to the emitted files | new BannerPlugin({ banner: "Built at " + new Date().toISOString() }) |
devServer.hot: true | Turns on hot module replacement, so edits land without a page reload | devServer.hot: true |
devServer.historyApiFallback: true | Falls back to index.html when an SPA history route returns 404 | devServer.historyApiFallback: true |
devServer.port / open | Picks the dev server port and opens a browser window automatically | devServer.port: 5000, open: true |
devServer.static | Names the directory that is served as static assets | devServer.static: path.join(__dirname, "public") |
devServer.proxy: string | Forwards requests carrying that prefix to the backend unchanged | devServer.proxy: { "/api": "http://localhost:3000" } |
devServer.proxy: { target, pathRewrite } | Proxies the request and rewrites the path on the way through | devServer.proxy: { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } } |
optimization.splitChunks: { chunks: "all" } | Carves shared code out into common chunks automatically | optimization.splitChunks: { chunks: "all" } |
optimization.runtimeChunk: "single" | Pulls the runtime into its own file so the main bundle churns far less | optimization.runtimeChunk: "single" |
optimization.minimizer | Supplies your own minifier configuration | optimization.minimizer: [new TerserPlugin({ extractComments: false })] |
import("./module").then(...) | Dynamic import: the on-demand entry point that makes code splitting possible | import("./module").then(m => m.default) |
/* webpackChunkName: "vendor" */ | Magic comment that names the chunk produced by a dynamic import | /* webpackChunkName: "vendor" */ |
/* webpackPrefetch: true */ | Magic comment that prefetches a lazy resource while the browser is idle | /* webpackPrefetch: true */ |
the merge() helper from webpack-merge | Returns a deep-merged copy of the shared config and the environment-specific one | const { merge } = require("webpack-merge"); merge(common, prodConfig) |
function (env, argv) => ({...}) | Function form of the config, which can read argv.mode and diverge per environment | module.exports = (env, argv) => ({ mode: argv.mode }) |
stats | Sets how much detail the build log prints | stats: "minimal" |
performance: { hints, maxAssetSize } | Raises a performance warning once an asset crosses the size limit | performance: { hints: "warning", maxAssetSize: 250000 } |
BundleAnalyzerPlugin({ analyzerMode }) | Emits a static bundle size report | new BundleAnalyzerPlugin({ analyzerMode: "static" }) |
Common pitfalls
SymptomThe hash in the filename changes on every build, so long-lived CDN caching is completely defeated.
Causecontenthash hashes module contents, but build-varying text such as a build-time banner got injected into the output; coarse hash granularity can also let a runtime change invalidate every file.
FixDrop the build-varying injection, or keep BannerPlugin on a fixed string and enable it only when needed; hold output.runtimeChunk: "single" so runtime churn stays isolated.
SymptomStyles are written and nothing happens on the page, with no error in the console.
CauseLoader chains run right to left and the order here is inverted; alternatively nothing matched the extension, so the CSS was treated as an ordinary asset.
FixAudit the use array right to left: css-loader parses, style-loader or MiniCssExtractPlugin.loader emits, and those two roles must never swap.
SymptomUnused functions still ship inside the production bundle, so tree shaking did nothing.
CauseThe module uses CommonJS module.exports and require, which limits the bundler to static analysis and leaves it unable to tell which exports are unreferenced.
FixRewrite the modules you want shaken in ESM import/export form; only then does production mode actually drop the dead code.
SymptomEverything works locally, but deployed route pages render blank or their assets 404.
Causeoutput.publicPath does not match the real deployment path, so the runtime loads chunks and assets under the wrong prefix.
FixSet publicPath explicitly to the true site prefix, for example "/assets/", and pin dynamic-import chunk names with a webpackChunkName comment so the failing asset is easy to locate.
SymptomIn local dev, /api/xxx returns 404 even though curl against the backend works.
CausedevServer.proxy is missing or does not cover that prefix, so the request is handled as a static asset.
FixConfigure devServer.proxy and strip the prefix when you need to: { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } }.
SymptomThe main bundle only ever grows, and a one-line change invalidates the whole JS download.
CauseEvery dependency is bundled into a single entry with no splitChunks or runtimeChunk split, so any edit changes the main file.
FixAdd optimization.splitChunks: { chunks: "all" } plus optimization.runtimeChunk: "single" to park third-party libraries and the runtime in files that rarely change.
Symptomdist accumulates files from earlier builds and production serves stale assets.
CauseOutput directory cleaning is off, so old hashed files linger forever.
FixTurn on output.clean: true, which is built into Webpack 5 and empties the output directory before each build.
SymptomBundle size investigations are pure guesswork and nobody can say which dependency is heaviest.
CauseThe build log runs at a terse level such as minimal, which carries no per-asset size data at all.
FixWire in new BundleAnalyzerPlugin({ analyzerMode: "static" }) for an offline HTML report, or export stats with webpack --json > stats.json and drill down chunk by chunk.
Troubleshooting
1Export build stats for size analysis
npx webpack --json > stats.jsonOnce stats.json exists you can walk it chunk by chunk to see which modules dominate and find the fat.
2Produce a static bundle size report
new BundleAnalyzerPlugin({ analyzerMode: "static" })Add it to plugins and rebuild; you get a self-contained HTML report you can open offline.
3Pick config and source map strategy per mode
npx webpack --config webpack.prod.js --mode productionThe console should print a production build summary, with devtool following the argv.mode branch in the function form.
4Switch to the terse log to scan the overall result
stats: "minimal"Only errors, warnings and the asset list are printed, so the outcome of a build is readable at a glance.
5Turn on size warnings to catch oversized assets
performance: { hints: "warning", maxAssetSize: 250000 }Anything past 250000 bytes shows up as a warning in the build log.
6Verify the public path used for runtime assets
output.publicPath: "/assets/"When assets 404 after a deploy, check this prefix against the real site path first, then look at the references inside the HTML.
Command Examples
Production skeleton: contenthash, clean, and HTML plugin
module.exports = {
mode: "production",
entry: { app: "./src/app.js" },
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
publicPath: "/assets/",
clean: true,
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
}[contenthash] changes only when content changes, and output.clean removes the stale artifacts — together they are the precondition for long-lived caching. publicPath must match the deployment root or every asset 404s at runtime.
Extract CSS to its own file and split shared chunks
module: {
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] },
],
},
plugins: [new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })],
optimization: {
splitChunks: { chunks: "all" },
runtimeChunk: "single",
}CSS inlined by style-loader rides inside the JS bundle and blocks rendering. Extraction lets the browser fetch and parse CSS in parallel, while a single runtimeChunk stops every chunk from repeating the runtime.
devServer: SPA fallback plus a proxied API with the prefix stripped
devServer: {
port: 5000,
hot: true,
historyApiFallback: true,
static: path.join(__dirname, "public"),
proxy: {
"/api": {
target: "http://localhost:3000",
pathRewrite: { "^/api": "" },
},
},
}Calling the backend over the relative /api path also removes cross-origin issues. pathRewrite decides what the backend sees — without it you get a 404 whenever the two ports differ. historyApiFallback keeps deep-route reloads working.
Route-level lazy loading with a named, prefetched chunk
const Settings = () =>
import(/* webpackChunkName: "settings" */ /* webpackPrefetch: true */ "./pages/Settings")Each dynamic import becomes its own chunk, downloaded on route change. webpackChunkName keeps artifact names readable and webpackPrefetch fetches during idle time, so the first paint only pays for the main bundle.
Switch devtool by mode and export stats for size analysis
module.exports = (env, argv) => ({
mode: argv.mode,
devtool: argv.mode === "development" ? "eval-cheap-source-map" : "source-map",
})
// CLI: webpack --config webpack.config.js --json > stats.jsonDevelopment trades rebuild speed for eval-cheap-source-map, production trades it back for real source maps in stack traces — hence the per-mode switch. The stats JSON feeds directly into a bundle analyzer to locate oversized modules.
Common Pitfalls
- Using [hash] instead of [contenthash] in output.filename: any module change rewrites every hash, invalidating all user caches so a returning visitor re-downloads the whole site.
- eval-* devtool variants emit code that only runs through eval. On a page whose CSP forbids unsafe-eval every script dies, and the symptom is a blank page rather than a build error — very expensive to diagnose.
- Deploying production source maps publishes restorable business logic. Upload them to your error tracker and hide the sourceMap URL, or keep them on an internal host only.
- Without mode, Webpack defaults to production: local debugging runs minification and tree-shaking, so stacks point at a single mangled line and dev no longer matches prod.
- If output.publicPath does not match the real deployment root (subdirectory, changed CDN host) the build succeeds while every static asset 404s and the page is blank with only resource errors in the console.
- Injecting the whole process.env object via DefinePlugin replaces the process identifier too, so libraries that branch on NODE_ENV under process.env — and polyfill feature-detection code — silently take the wrong path.
Tips
- Webpack 5 has built-in asset modules, replacing file-loader and url-loader.
- devServer.hot enables HMR for a smoother dev experience.
- splitChunks splits code by vendor/common to shrink first-load size.
- webpack-bundle-analyzer visualizes bundle size.
- Production mode enables tree shaking, which needs ESM imports/exports.
- [name].[contenthash].js gives long-term caching; unchanged files keep their hash and hit the browser cache.
FAQ
What does putting [contenthash] in the output filename actually solve?
Long-term caching. With output.filename: "[name].[contenthash].js" the asset name changes with its content: an unchanged chunk keeps the same hash and the browser is free to reuse its strong cache, while any edit changes the hash and therefore the URL, so users pick up the new file without clearing caches and never run stale code. A fixed name like bundle.js instead pins the browser to the old file. Three settings support this: output.path must be absolute (path.resolve(__dirname, "dist")), output.publicPath: "/assets/" sets the URL prefix, and output.clean: true makes Webpack 5 purge dist before building so stale hashed files do not pile up. The CSS side follows the same pattern with MiniCssExtractPlugin emitting [name].[contenthash].css.
When should I switch from style-loader to MiniCssExtractPlugin.loader?
It depends on whether the CSS is injected at runtime or emitted as its own file. During development use { test: /\.css$/, use: ["style-loader", "css-loader"] }: style-loader injects the CSS into the DOM and, with devServer.hot: true, gives you hot style replacement so edits appear immediately with no extra request. For production switch to { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] }, which extracts a real file and lets PostCSS in, then name it with new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }). The reasoning is simple: the inline approach cannot use the browser cache and causes a flash of unstyled content, so built output should always be extracted.
How do images and fonts work in Webpack 5 — do I still need file-loader?
No. Asset modules are built in and replace file-loader and url-loader. Images use { test: /\.(png|jpg|gif)$/, type: "asset/resource" } to emit a separate file and return a URL; SVG, when you want it as a string, uses { test: /\.svg$/, type: "asset/source" }; fonts follow the same resource pattern with { test: /\.(woff2?|ttf|eot)$/, type: "asset/resource" }. There is also { test: /\.(js|ts)$/, enforce: "pre", use: "source-map-loader" } which runs in the pre stage to pick up source maps shipped by dependencies, so stack traces from minified code can still be traced back to the original TypeScript. To copy a whole static directory, use new CopyWebpackPlugin({ patterns: [{ from: "public", to: "." }] }).
Refreshing a client-side route 404s and API calls hit CORS — how do I configure devServer?
Each symptom has its own option. historyApiFallback: true rewrites unmatched SPA history routes to index.html, so opening /users/123 directly is handled by the front-end router and a refresh no longer 404s. For cross-origin calls, proxy the request: { "/api": "http://localhost:3000" } solves most cases, and when the path needs rewriting use the object form { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } } to strip the prefix before forwarding, so the backend needs no dev-only /api prefix. Related settings: devServer.hot: true for HMR, devServer.port: 5000 with open: true to pick a port and launch the browser, and devServer.static pointing at the public directory.
The first-paint bundle is too large — how do I split chunks and lazy-load?
Four settings work together. optimization.splitChunks: { chunks: "all" } automatically lifts shared code out of each entry; optimization.runtimeChunk: "single" extracts the webpack runtime so it is not duplicated inside every chunk; optimization.minimize: true turns on minification, and the custom form is optimization.minimizer: [new TerserPlugin({ extractComments: false })]. Route-level lazy loading uses the dynamic import import("./module").then(m => m.default), combined with the magic comments /* webpackChunkName: "vendor" */ to name the chunk and /* webpackPrefetch: true */ to fetch it during idle time. Production mode enables tree shaking automatically, but it only bites when the code is written with ESM import/export.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us