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.

Languages·45 commands·Last updated 2026-07-21
webpackBuildPackagingFrontend

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"
Single entry file
entry: { app: "./src/app.js", vendor: "./src/vendor.js" }
Multi-entry config
output.filename: "[name].[contenthash].js"
Output filename (content hash)
output.path: path.resolve(__dirname, "dist")
Output dir (absolute path)
output.publicPath: "/assets/"
Public path prefix
output.clean: true
Clean dist before build (Webpack 5)

Loaders 8

{ test: /\.css$/, use: ["style-loader", "css-loader"] }
Inline CSS into DOM
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] }
Extract CSS + PostCSS
{ test: /\.js$/, exclude: /node_modules/, use: "babel-loader" }
Transpile JS with Babel
{ test: /\.tsx?$/, use: "ts-loader" }
Handle TypeScript
{ test: /\.(png|jpg|gif)$/, type: "asset/resource" }
Image assets (asset modules)
{ test: /\.svg$/, type: "asset/source" }
Inline SVG as string
{ test: /\.(woff2?|ttf|eot)$/, type: "asset/resource" }
Font assets
{ test: /\.(js|ts)$/, enforce: "pre", use: "source-map-loader" }
Load existing source maps

Plugins 7

new HtmlWebpackPlugin({ template: "./src/index.html" })
Auto-generate HTML
new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })
Extract CSS to a file
new DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
Inject global env vars
new CopyWebpackPlugin({ patterns: [{ from: "public", to: "." }] })
Copy static files to dist
new ProvidePlugin({ _: "lodash" })
Auto-load a module (no import)
new HotModuleReplacementPlugin()
HMR plugin
new BannerPlugin({ banner: "Built at " + new Date().toISOString() })
Add a banner comment

Dev Server 6

devServer.hot: true
Enable HMR
devServer.historyApiFallback: true
SPA history fallback to index
devServer.port: 5000, open: true
Set port and auto-open
devServer.static: path.join(__dirname, "public")
Static file dir
devServer.proxy: { "/api": "http://localhost:3000" }
Proxy API to backend
devServer.proxy: { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } }
Proxy and rewrite path

Optimization 7

optimization.splitChunks: { chunks: "all" }
Auto-split shared code
optimization.minimize: true
Enable minification
optimization.minimizer: [new TerserPlugin({ extractComments: false })]
Custom JS minifier
optimization.runtimeChunk: "single"
Split runtime to a file
import("./module").then(m => m.default)
Dynamic import (lazy route)
/* webpackChunkName: "vendor" */
Magic comment chunk name
/* webpackPrefetch: true */
Prefetch lazy chunk

Mode & Environment 6

mode: "production" | "development" | "none"
Three built-in modes
webpack --config webpack.prod.js --mode production
CLI config and mode
module.exports = (env, argv) => ({ mode: argv.mode })
Function config reads argv
devtool: argv.mode === "development" ? "eval-cheap-source-map" : "source-map"
Toggle source map by mode
const { merge } = require("webpack-merge"); merge(common, prodConfig)
Merge common and env config
new webpack.EnvironmentPlugin(["NODE_ENV", "API_URL"])
Bulk-inject process.env vars

Build Analysis 5

new BundleAnalyzerPlugin({ analyzerMode: "static" })
Static bundle analysis report
stats: "minimal" | "normal" | "detailed"
Control log verbosity
performance: { hints: "warning", maxAssetSize: 250000 }
Asset-size performance hint
webpack --json > stats.json
Export stats JSON
devtool: "source-map" | "eval-cheap-module-source-map"
Source map speed vs quality

Parameter matrix

ParameterEffectExample
entryDeclares the bundle entry point; accepts either a string or a map of named entriesentry: { 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 viableoutput.filename: "[name].[contenthash].js"
output.pathDirectory that receives the bundle; must be an absolute pathoutput.path: path.resolve(__dirname, "dist")
output.publicPathPublic path prefix the runtime uses when it loads chunks and assetsoutput.publicPath: "/assets/"
output.clean: trueEmpties the dist directory before every build (built into Webpack 5)output.clean: true
modeThe built-in modes: production, development or nonemode: "production"
devtoolChooses the source map flavour, trading build speed against debug fidelitydevtool: "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.loaderExtracts 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 assetsnew HtmlWebpackPlugin({ template: "./src/index.html" })
MiniCssExtractPlugin({ filename })Pairs with the extraction loader to emit a content-hashed stylesheetnew MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })
DefinePluginSubstitutes constants into the bundle at compile time, the usual way to inject environment checksnew DefinePlugin({ ... })
EnvironmentPlugin([...])Injects a whole batch of environment variables as globals in a single callnew EnvironmentPlugin(["NODE_ENV", "API_URL"])
CopyWebpackPlugin({ patterns })Copies files that need no compilation straight into the output directorynew CopyWebpackPlugin({ patterns: [{ from: "public", to: "." }] })
ProvidePluginInjects module references automatically, sparing you the per-file importsnew ProvidePlugin({ _: "lodash" })
BannerPlugin({ banner })Prepends a comment banner to the emitted filesnew BannerPlugin({ banner: "Built at " + new Date().toISOString() })
devServer.hot: trueTurns on hot module replacement, so edits land without a page reloaddevServer.hot: true
devServer.historyApiFallback: trueFalls back to index.html when an SPA history route returns 404devServer.historyApiFallback: true
devServer.port / openPicks the dev server port and opens a browser window automaticallydevServer.port: 5000, open: true
devServer.staticNames the directory that is served as static assetsdevServer.static: path.join(__dirname, "public")
devServer.proxy: stringForwards requests carrying that prefix to the backend unchangeddevServer.proxy: { "/api": "http://localhost:3000" }
devServer.proxy: { target, pathRewrite }Proxies the request and rewrites the path on the way throughdevServer.proxy: { "/api": { target: "http://localhost:3000", pathRewrite: { "^/api": "" } } }
optimization.splitChunks: { chunks: "all" }Carves shared code out into common chunks automaticallyoptimization.splitChunks: { chunks: "all" }
optimization.runtimeChunk: "single"Pulls the runtime into its own file so the main bundle churns far lessoptimization.runtimeChunk: "single"
optimization.minimizerSupplies your own minifier configurationoptimization.minimizer: [new TerserPlugin({ extractComments: false })]
import("./module").then(...)Dynamic import: the on-demand entry point that makes code splitting possibleimport("./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-mergeReturns a deep-merged copy of the shared config and the environment-specific oneconst { merge } = require("webpack-merge"); merge(common, prodConfig)
function (env, argv) => ({...})Function form of the config, which can read argv.mode and diverge per environmentmodule.exports = (env, argv) => ({ mode: argv.mode })
statsSets how much detail the build log printsstats: "minimal"
performance: { hints, maxAssetSize }Raises a performance warning once an asset crosses the size limitperformance: { hints: "warning", maxAssetSize: 250000 }
BundleAnalyzerPlugin({ analyzerMode })Emits a static bundle size reportnew 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

  1. 1Export build stats for size analysis

    npx webpack --json > stats.json

    Once stats.json exists you can walk it chunk by chunk to see which modules dominate and find the fat.

  2. 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.

  3. 3Pick config and source map strategy per mode

    npx webpack --config webpack.prod.js --mode production

    The console should print a production build summary, with devtool following the argv.mode branch in the function form.

  4. 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.

  5. 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.

  6. 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.json

Development 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