From e9102e1216702fb7a8a892b0685b818a127a6685 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 20:54:17 -0500 Subject: [PATCH 01/10] docs(performance): document the 5.110 performance checks Adds performance.all, hints: "stats" and the opt-in checks introduced in webpack 5.110.0, plus an overview table grouping them by what they inspect and a note on which ones are not gated on hints. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/configuration/performance.mdx | 336 +++++++++++++++++++++- 1 file changed, 335 insertions(+), 1 deletion(-) diff --git a/src/content/configuration/performance.mdx b/src/content/configuration/performance.mdx index 99cf19ba525f..ebbf0995f290 100644 --- a/src/content/configuration/performance.mdx +++ b/src/content/configuration/performance.mdx @@ -8,17 +8,62 @@ contributors: - madhavarshney - EugeneHlushko - shivxmsharma + - bjohansebas --- These options allows you to control how webpack notifies you of assets and entry points that exceed a specific file limit. This feature was inspired by the idea of [webpack Performance Budgets](https://github.com/webpack/webpack/issues/3216). +Since webpack 5.110.0 the same option also hosts a set of opt-in checks that look at the shape of the bundle and of your configuration, not only at asset sizes: duplicated packages, modules nothing uses, rules that never match, `import()` calls that defer nothing, and so on. Every one of those checks is `false` by default; see [`performance.all`](#performanceall) to turn the whole set on at once. + ## `performance` `object` Configure how performance hints are shown. For example if you have an asset that is over 250kb, webpack will emit a warning notifying you of this. +### Available checks + +Besides the size budget ([`maxAssetSize`](#performancemaxassetsize) and [`maxEntrypointSize`](#performancemaxentrypointsize)), webpack ships these checks, grouped here by what they look at: + +| Area | Checks | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| What ships twice | [`duplicatePackages`](#performanceduplicatepackages), [`duplicateModules`](#performanceduplicatemodules), [`entrypointOverlap`](#performanceentrypointoverlap) | +| What ships unused | [`unusedReexports`](#performanceunusedreexports), [`missingSideEffects`](#performancemissingsideeffects), [`dynamicExports`](#performancedynamicexports), [`scopeHoistingBailouts`](#performancescopehoistingbailouts), [`legacyJavascript`](#performancelegacyjavascript) | +| How chunks load | [`asyncChunkWaterfalls`](#performanceasyncchunkwaterfalls), [`redundantDynamicImports`](#performanceredundantdynamicimports), [`tinyChunks`](#performancetinychunks), [`unsplitVendors`](#performanceunsplitvendors), [`splitChunksCapped`](#performancesplitchunkscapped), [`conflictingResourceHints`](#performanceconflictingresourcehints) | +| What weighs a chunk | [`largeModules`](#performancelargemodules), [`inlinedAssets`](#performanceinlinedassets), [`embeddedSourceMaps`](#performanceembeddedsourcemaps), [`broadContexts`](#performancebroadcontexts) | +| Code hazards | [`evalUsage`](#performanceevalusage), [`pureAnnotations`](#performancepureannotations), [`topLevelThis`](#performancetoplevelthis), [`mixedExports`](#performancemixedexports) | +| Configuration | [`unusedRules`](#performanceunusedrules), [`unusedAliases`](#performanceunusedaliases), [`unusedDefines`](#performanceunuseddefines), [`unusedExternals`](#performanceunusedexternals), [`osDependentRules`](#performanceosdependentrules) | +| Build itself | [`cacheEffectiveness`](#performancecacheeffectiveness), [`hotspots`](#performancehotspots), [`circularDependencies`](#performancecirculardependencies) | + +Most of them are reported through [`performance.hints`](#performancehints), so they are silent while `hints` is `false`. + +The checks that look at your configuration are not gated on `hints`, since a rule nothing matches or a misspelled external is a configuration mistake rather than a size: [`unusedRules`](#performanceunusedrules), [`unusedAliases`](#performanceunusedaliases), [`unusedDefines`](#performanceunuseddefines), [`unusedExternals`](#performanceunusedexternals), [`osDependentRules`](#performanceosdependentrules) and [`conflictingResourceHints`](#performanceconflictingresourcehints) are reported as warnings whenever the check itself is on. + +### performance.all + + + +`boolean = false` + +Fallback value for every check that is not set individually. It takes precedence over webpack's own defaults, so `all: true` enables the whole set and any check you set explicitly still wins: + +```js +export default { + // ... + performance: { + hints: "warning", + all: true, + // enabled by `all`, but this one stays off + hotspots: false, + }, +}; +``` + +`all` does not apply to [`hints`](#performancehints), [`maxAssetSize`](#performancemaxassetsize) or [`maxEntrypointSize`](#performancemaxentrypointsize). + +T> Turning everything on at once is a good way to audit a project, but several checks walk the module graph and cost build time. Once you have read the report, keep the handful that matter to you enabled and leave the rest off. + ### performance.assetFilter `function(assetFilename) => boolean` @@ -46,9 +91,117 @@ export default { The example above will only give you performance hints based on `.js` files. +### performance.asyncChunkWaterfalls + + + +`boolean = false` + +Report chains of `import()` calls where each chunk can only be requested once the one before it has arrived and run, so every level of the chain costs a round trip in series before anything below it starts. + +Importing the deeper modules from the entry, or giving them a single [`webpackPrefetch`](/api/module-methods/#magic-comments) hint, lets them be fetched together instead. + +### performance.broadContexts + + + +`boolean = false` + +Report [`require.context`](/api/module-methods/#requirecontext) calls with no filter, which bundle every file under a directory, including the ones nothing ever requests. A sync context bundles them all; a lazy one gives each of them its own chunk. + +Narrowing the pattern, or using [`ContextReplacementPlugin`](/plugins/context-replacement-plugin/), limits the context to what is actually reachable. + +### performance.cacheEffectiveness + + + +`boolean = false` + +Report how much of the module graph the [cache](/configuration/cache/) reused, and which modules can never be reused. The warning names how many modules were rebuilt although the cache was warm, and the reasons why, so you can tell a cold cache apart from one that is being invalidated on every build. + +### performance.circularDependencies + + + +`boolean = false` + +Report groups of modules that import each other synchronously. A cycle makes at least one module in the group observe a partially initialized binding at evaluation time, and it prevents some export inlining. + +The scan runs in `mode: "production"` regardless of this option, since [export inlining](/configuration/optimization/#optimizationinlineexports) needs it; this option only decides whether the cycles it finds are reported. + +### performance.conflictingResourceHints + + + +`boolean = false` + +Report chunks asked for as both prefetch and preload from the same place. The two directives say opposite things: a preload fetches the chunk at high priority right away, while a prefetch asks for it at idle priority in case it is needed later. Keep [`webpackPreload`](/guides/code-splitting/#prefetchingpreloading-modules) for what the page needs now and `webpackPrefetch` for what it may need later, not both. + +This check is not gated on [`hints`](#performancehints). + +### performance.duplicateModules + + + +`boolean = false` + +Report modules emitted into more than one chunk, and the bytes the extra copies cost. Usually a sign that [`optimization.splitChunks`](/plugins/split-chunks-plugin/) could move the shared modules into a chunk of their own. + +### performance.duplicatePackages + + + +`boolean = false` + +Report packages that are included more than once, either in different versions or as several copies of the same version. Both cost the bundle a full extra copy, and different copies of a package that keeps state (a React or a store instance, for example) also break at runtime. + +```js +export default { + // ... + performance: { + hints: "warning", + duplicatePackages: true, + }, +}; +``` + +T> [`resolve.alias`](/configuration/resolve/#resolvealias) or your package manager's dependency resolution can force a single copy once the report tells you which package is duplicated. + +### performance.dynamicExports + + + +`boolean = false` + +Report modules whose exports cannot be read statically (a CommonJS module assigning to `module.exports` behind a condition, for example), which stops anything importing them from being tree-shaken. + +### performance.embeddedSourceMaps + + + +`boolean = false` + +Report a production build whose [`devtool`](/configuration/devtool/) writes the source map into the JavaScript itself. The map is then downloaded by everyone who loads the page, at several times the size of the code it describes. A separate `.map` file is fetched only by whoever opens the devtools, and `hidden-source-map` keeps it off the client entirely while still producing a map to upload to an error reporter. + +### performance.entrypointOverlap + + + +`boolean = false` + +Report modules shipped by more than one entrypoint, which every page that loads them downloads again, along with the bytes the overlap costs. + +### performance.evalUsage + + + +`boolean = false` + +Report modules that call `eval` directly. A direct eval reads and writes any name in scope, so nothing the module declares can be renamed or dropped: minification, [scope hoisting](/configuration/optimization/#optimizationconcatenatemodules) and tree shaking all stop at it. `new Function` takes no local scope and does not have this effect. + ### performance.hints -`string: 'error' | 'warning'` `boolean: false` +`string: 'error' | 'warning' | 'stats'` `boolean: false` Turns hints on/off. In addition, tells webpack to throw either an error or a warning when hints are found. @@ -95,6 +248,51 @@ export default { An error will be displayed notifying you of a large asset. We recommend using `hints: "error"` during production builds to help prevent deploying production bundles that are too large, impacting webpage performance. +```js +export default { + // ... + performance: { + hints: "stats", + }, +}; +``` + + + +The hints are collected and exposed through [`stats`](/configuration/stats/) only. They are not counted as warnings or errors, so the build stays green and nothing fails a CI step that treats warnings as failures. This is the value to use when you want the report from the checks listed above without turning every finding into build output. + +### performance.hotspots + + + +`boolean = false` + +Report the loaders, plugins and hooks that hold the main thread, timing each one's own code rather than what it waited for. Only synchronous stretches count, so work resumed after an `await` is not attributed. When the ordering matters rather than the totals, [`ProfilingPlugin`](/plugins/profiling-plugin/) records the same work as a trace. + +### performance.inlinedAssets + + + +`boolean = false` + +Report assets inlined as data urls that are large enough for the base64 cost and the lost caching to outweigh the request they save. A data url costs about a third more than the file it replaces, cannot be cached on its own, and is downloaded again whenever the code around it changes. [`Rule.parser.dataUrlCondition.maxSize`](/guides/asset-modules/#general-asset-type) decides which files are small enough to be worth that. + +### performance.largeModules + + + +`boolean = false` + +Report a single module that makes up most of the chunk it is in. Everything else in the chunk together weighs less than that one module, so splitting it out with [`optimization.splitChunks`](/plugins/split-chunks-plugin/), loading it on demand, or replacing it is what actually changes the size. + +### performance.legacyJavascript + + + +`boolean = false` + +Report polyfill packages that emulate language features the [target](/configuration/target/) already supports natively, along with the bytes they cost. + ### performance.maxAssetSize `number = 250000` @@ -110,6 +308,8 @@ export default { }; ``` +Since webpack 5.110.0 the warning also names the largest modules inside the oversized asset, so the report points at what to split rather than only at the file. + ### performance.maxEntrypointSize `number = 250000` @@ -124,3 +324,137 @@ export default { }, }; ``` + +Since webpack 5.110.0, when the entrypoint that goes over the limit is also the one carrying the runtime, the hint recommends [`optimization.runtimeChunk`](/configuration/optimization/#optimizationruntimechunk) so the runtime stops being re-downloaded with it. + +### performance.missingSideEffects + + + +`boolean = false` + +Report packages that keep unused code in the bundle because their `package.json` does not declare [`sideEffects`](/guides/tree-shaking/#mark-the-file-as-side-effect-free), together with the bytes that costs. + +### performance.mixedExports + + + +`boolean = false` + +Report an entry that exports a default beside named exports for a CommonJS [library](/configuration/output/#outputlibrary), where a consumer calling `require()` gets the namespace object and therefore receives the default as `.default` rather than as the value itself. Exporting only a default, or only named exports, leaves no ambiguity, and [`output.library.export`](/configuration/output/#outputlibraryexport) can also pick one. + +### performance.osDependentRules + + + +`boolean = false` + +Report conditions in [`module.rules`](/configuration/module/#modulerules) that hardcode a path separator, so they only match on one operating system (a `test: /src\/components\//` that matches on Linux and macOS but not on Windows, for example). + +This check is not gated on [`hints`](#performancehints). See also the [`glob`](/configuration/module/#ruleglob) condition, which matches OS-independently. + +### performance.pureAnnotations + + + +`boolean = false` + +Report `/*#__PURE__*/` annotations that sit somewhere the parser does not read them. The annotation is only read directly before a call, a `new`, or a tagged template; anywhere else it is a plain comment, and the code it was meant to make droppable is kept. + +### performance.redundantDynamicImports + + + +`boolean = false` + +Report `import()` calls whose module is already loaded where the call runs, so they defer nothing while still costing a promise and a chunk boundary. + +### performance.scopeHoistingBailouts + + + +`boolean = false` + +Report modules that could not be merged into their importer's scope by [`optimization.concatenateModules`](/configuration/optimization/#optimizationconcatenatemodules), and why, so each keeps its own wrapper. The reasons are grouped and counted rather than listed one module at a time. + +### performance.splitChunksCapped + + + +`boolean = false` + +Report splits [`optimization.splitChunks`](/plugins/split-chunks-plugin/) refused because `maxInitialRequests` or `maxAsyncRequests` was already reached. The modules stayed where they were, so the cache group did not take effect; raising the limit lets the split happen, at the cost of more parallel requests. + +### performance.tinyChunks + + + +`boolean = false` + +Report chunks that are loaded on demand but carry less than [`optimization.splitChunks.minSize`](/plugins/split-chunks-plugin/#splitchunksminsize), where the request costs more than the bytes it defers. + +### performance.topLevelThis + + + +`boolean = false` + +Report modules that read `this` at the top level of an ES module, where it is `undefined` rather than the module object or the global one. A single `import` or `export` is enough for `javascript/auto` to decide a file is an ES module, so a file that worked as CommonJS can silently read nothing once it is bundled that way. Use `globalThis` where the global object was meant, [`import.meta`](/api/module-variables/#importmeta) for anything about the module, or give the file a `.cjs` extension to keep it CommonJS. + +### performance.unsplitVendors + + + +`boolean = false` + +Report initial chunks that mix `node_modules` code with application code. The dependencies then get a new hash on every application change, so returning visitors download them again; [`optimization.splitChunks`](/plugins/split-chunks-plugin/) can move them into a chunk of their own. + +### performance.unusedAliases + + + +`boolean = false` + +Report [`resolve.alias`](/configuration/resolve/#resolvealias) entries that no request matched, which usually means the alias is misspelled and the real request resolved somewhere else. + +This check is not gated on [`hints`](#performancehints). + +### performance.unusedDefines + + + +`boolean = false` + +Report keys defined by [`DefinePlugin`](/plugins/define-plugin/) that no module ever referenced. Each one costs a parser hook per module and invalidates the build whenever its value changes. The keys webpack defines itself (`process.env.NODE_ENV` and the `import.meta.env` defaults) are marked internal and are never reported against you. + +This check is not gated on [`hints`](#performancehints). + +### performance.unusedExternals + + + +`boolean = false` + +Report requests listed in [`externals`](/configuration/externals/) that no module ever imported, which usually means the request is misspelled and the real one got bundled instead. + +This check is not gated on [`hints`](#performancehints). + +### performance.unusedReexports + + + +`boolean = false` + +Report modules that are bundled although nothing uses what they export, pulled in by a re-export. This is the classic barrel file cost: `export * from "./x"` in an `index.js` drags `./x` into the bundle even when only its neighbour is imported. + +### performance.unusedRules + + + +`boolean = false` + +Report rules in [`module.rules`](/configuration/module/#modulerules) that never matched a module, which cost condition evaluation on every build and usually mean the `test` does not describe the files you thought it did. + +This check is not gated on [`hints`](#performancehints). + +W> Plugins add rules too, so a reported rule is not necessarily one you wrote. From 9136bf40bed4f8edc1a0b13146f319edd672e95c Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 20:56:44 -0500 Subject: [PATCH 02/10] docs(module): document Rule.glob and Rule.descriptionRelativePath Both conditions land in webpack 5.110.0: glob matches the resource OS-independently and is also accepted inside a Condition object, and descriptionRelativePath matches a module by its path inside its own package. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/configuration/module.mdx | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/content/configuration/module.mdx b/src/content/configuration/module.mdx index dc1d63bb9669..3b2e9aa08b90 100644 --- a/src/content/configuration/module.mdx +++ b/src/content/configuration/module.mdx @@ -1718,6 +1718,8 @@ There are two input values for the conditions: In a Rule the properties [`test`](#ruletest), [`include`](#ruleinclude), [`exclude`](#ruleexclude) and [`resource`](#ruleresource) are matched with the resource and the property [`issuer`](#ruleissuer) is matched with the issuer. +Since webpack 5.110.0 the resource can also be matched with [`glob`](#ruleglob), which is OS-independent, and with [`descriptionRelativePath`](#ruledescriptionrelativepath), which is the resource's path inside its own package. + When using multiple conditions, all conditions must match. W> Be careful! The resource is the _resolved_ path of the file, which means symlinked resources are the real path _not_ the symlink location. This is good to remember when using tools that symlink packages (like `npm link`), common conditions like `/node_modules/` may inadvertently miss symlinked files. Note that you can turn off symlink resolving (so that resources are resolved to the symlink path) via [`resolve.symlinks`](/configuration/resolve/#resolvesymlinks). @@ -1809,6 +1811,35 @@ export default { }; ``` +## Rule.descriptionRelativePath + + + +A [`Condition`](#condition) matched against the path of the module relative to the directory of its description file (usually the closest `package.json`), for example `./lib/button.js`. The path always uses forward slashes, so the same rule matches on every operating system. + +This is what you want when a rule should target a file by its place inside a package rather than by its absolute location, which changes with the install layout (a hoisted `node_modules`, a pnpm store, a workspace symlink): + +**webpack.config.js** + +```js +export default { + // ... + module: { + rules: [ + { + descriptionData: { + name: "some-package", + }, + descriptionRelativePath: /^\.\/src\//, + // ... + }, + ], + }, +}; +``` + +The condition does not apply when a [match resource](/api/loaders/#inline-matchresource) replaced the resource, since the description file of the original request no longer describes it. + ## Rule.enforce `string` @@ -1855,6 +1886,66 @@ Exclude all modules matching any of these conditions. If you supply a `Rule.excl Include all modules matching any of these conditions. If you supply a `Rule.include` option, you cannot also supply a `Rule.resource`. See [`Rule.resource`](#ruleresource) and [`Condition.include`](#condition) for details. +## Rule.glob + + + +`string` `[string]` + +Match the module resource against one or more glob patterns. Unlike a regular expression, a glob matches the same way on every operating system: `/` and `\` are both read as a path separator, in the pattern as well as in the tested path, so a rule written on macOS keeps matching on Windows. See [`performance.osDependentRules`](/configuration/performance/#performanceosdependentrules) for a check that reports the regexp conditions that do not. + +**webpack.config.js** + +```js +export default { + // ... + module: { + rules: [ + { + glob: "src/**/*.css", + type: "css/module", + }, + ], + }, +}; +``` + +Several patterns are OR-ed together, and a `!` prefix subtracts what it matches. A list that only contains `!` patterns subtracts from everything, so it reads as an exclusion list: + +```js +export default { + // ... + module: { + rules: [ + { + // every .ts file except the tests and the generated ones + glob: ["**/*.ts", "!**/*.test.ts", "!**/generated/**"], + loader: "ts-loader", + }, + ], + }, +}; +``` + +A relative pattern matches at any depth, so `"src/**/*.css"` also matches `packages/ui/src/theme.css`. Start the pattern with an absolute path when you want to pin it to one directory. + +`glob` combines with the other conditions: [`test`](#ruletest), [`include`](#ruleinclude) and [`exclude`](#ruleexclude) still have to match too. It is also available inside a [`Condition`](#condition) object, which lets you use it wherever a condition is accepted: + +```js +export default { + // ... + module: { + rules: [ + { + test: /\.js$/, + include: { glob: "src/**" }, + loader: "babel-loader", + }, + ], + }, +}; +``` + ## Rule.issuer A [`Condition`](#condition) to match against the module that issued the request. In the following example, the `issuer` for the `a.js` request would be the path to the `index.js` file. @@ -2643,6 +2734,8 @@ Conditions can be one of these: `{ not: [Condition] }`: All Conditions must NOT match. +`{ glob: string | [string] }`: The input must match the glob pattern, or one of them. See [`Rule.glob`](#ruleglob) for the pattern syntax. + **Example:** ```js From b5717813e69b9026e33135e0f09d5dab757333b0 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 20:59:41 -0500 Subject: [PATCH 03/10] docs(externals): document nodeModules preset and external sideEffects Covers externalsPresets.nodeModules and its allowlist, which replace the webpack-node-externals plugin, plus the { external, sideEffects } value form, both added in webpack 5.110.0. Also fills in the bun and deno preset rows, which the table was missing. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/configuration/externals.mdx | 106 +++++++++++++++++++++--- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/src/content/configuration/externals.mdx b/src/content/configuration/externals.mdx index 8458cb1ccd40..2b1fc360086f 100644 --- a/src/content/configuration/externals.mdx +++ b/src/content/configuration/externals.mdx @@ -20,6 +20,7 @@ contributors: - anshumanv - SaulSilver - fi3ework + - bjohansebas --- The `externals` configuration option provides a way of excluding dependencies from the output bundles. Instead, the created bundle relies on that dependency to be present in the consumer's (any end-user application) environment. This feature is typically most useful to **library developers**, however there are a variety of applications for it. @@ -193,6 +194,31 @@ export default { }; ``` +### object with options + + + +An external value can also be given as an object carrying the target under `external` plus options describing how webpack should treat it: + +```js +export default { + // ... + externals: { + "@scope/icons": { + external: "commonjs @scope/icons", + sideEffects: false, + }, + }, +}; +``` + +- `external` - the target, in any of the forms above (a string, an array, or an object per externals type). +- `sideEffects` - whether importing the external has side effects, the same idea as the [`sideEffects` flag](/guides/tree-shaking/#mark-the-file-as-side-effect-free) in a `package.json`. + +webpack cannot analyze an external, so it has to assume that importing one does something observable and keeps the import even when nothing reads its exports. `sideEffects: false` states the opposite, and lets webpack drop the external entirely when none of its exports are used. This matters most for a large external imported by a barrel file, where the request would otherwise survive into every chunk that touches the barrel. + +W> Only set it when the package really is free of side effects. Getting it wrong removes an import that the runtime was relying on for its effect, and the failure shows up at runtime rather than at build time. + ### function - `function ({ context, request, contextInfo, getResolve }, callback)` @@ -1053,16 +1079,19 @@ jq(".my-element").animate(/* ... */); Enable presets of externals for specific targets. -| Option | Description | Input Type | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| `electron` | Treat common electron built-in modules in main and preload context like `electron`, `ipc` or `shell` as external and load them via `require()` when used. | boolean | -| `electronMain` | Treat electron built-in modules in the main context like `app`, `ipc-main` or `shell` as external and load them via `require()` when used. | boolean | -| `electronPreload` | Treat electron built-in modules in the preload context like `web-frame`, `ipc-renderer` or `shell` as external and load them via require() when used. | boolean | -| `electronRenderer` | Treat electron built-in modules in the renderer context like `web-frame`, `ipc-renderer` or `shell` as external and load them via `require()` when used. | boolean | -| `node` | Treat node.js built-in modules like `fs`, `path` or `vm` as external and load them via `require()` when used. | boolean | -| `nwjs` | Treat `NW.js` legacy `nw.gui` module as external and load it via `require()` when used. | boolean | -| `web` | Treat references to `http(s)://...` and `std:...` as external and load them via `import` when used. **(Note that this changes execution order as externals are executed before any other code in the chunk)**. | boolean | -| `webAsync` | Treat references to `http(s)://...` and `std:...` as external and load them via `async import()` when used **(Note that this external type is an `async` module, which has various effects on the execution)**. | boolean | +| Option | Description | Input Type | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| `electron` | Treat common electron built-in modules in main and preload context like `electron`, `ipc` or `shell` as external and load them via `require()` when used. | boolean | +| `electronMain` | Treat electron built-in modules in the main context like `app`, `ipc-main` or `shell` as external and load them via `require()` when used. | boolean | +| `electronPreload` | Treat electron built-in modules in the preload context like `web-frame`, `ipc-renderer` or `shell` as external and load them via require() when used. | boolean | +| `electronRenderer` | Treat electron built-in modules in the renderer context like `web-frame`, `ipc-renderer` or `shell` as external and load them via `require()` when used. | boolean | +| `bun` | Treat bun built-in modules like `bun`, `bun:sqlite` or `bun:ffi`, and node.js built-in modules, as external and load them via `import` when used (for the Bun runtime). | boolean | +| `deno` | Treat node.js built-in modules like `fs`, `path` or `vm` as external and load them via the required `node:` specifier when used (for the Deno runtime). | boolean | +| `node` | Treat node.js built-in modules like `fs`, `path` or `vm` as external and load them via `require()` when used. | boolean | +| `nodeModules` | Treat installed packages (requests resolving into a `node_modules` directory) as external and load them via `require()`/`import` at runtime instead of bundling them. See [`externalsPresets.nodeModules`](#externalspresetsnodemodules). | boolean, object | +| `nwjs` | Treat `NW.js` legacy `nw.gui` module as external and load it via `require()` when used. | boolean | +| `web` | Treat references to `http(s)://...` and `std:...` as external and load them via `import` when used. **(Note that this changes execution order as externals are executed before any other code in the chunk)**. | boolean | +| `webAsync` | Treat references to `http(s)://...` and `std:...` as external and load them via `async import()` when used **(Note that this external type is an `async` module, which has various effects on the execution)**. | boolean | Note that if you're going to output ES Modules with those node.js-related presets, webpack will set the default `externalsType` to [`node-commonjs`](#externalstypenode-commonjs) which would use `createRequire` to construct a require function instead of using `require()`. @@ -1080,3 +1109,60 @@ export default { }, }; ``` + +### externalsPresets.nodeModules + + + +`boolean` `object` + +Treat every request that resolves into a `node_modules` directory as external and load it with `require()` or `import` at runtime, instead of bundling it. This is what a server-side build usually wants: the dependencies are already installed next to the output, so bundling them only makes the build slower and the output bigger. + +**webpack.config.js** + +```js +export default { + // ... + target: "node", + externalsPresets: { + nodeModules: true, + }, +}; +``` + +The preset looks at where the request resolves, not at how it is written, so a request that resolves through a symlink into `node_modules` (a pnpm store, a linked workspace package) is externalized as well. A few things are never externalized, so you do not have to list them: + +- relative and absolute requests, and `#` subpath imports, which are never installed packages; +- anything that does not resolve to a file the runtime can load on its own, that is anything other than `.js`, `.mjs`, `.cjs`, `.json` and `.node`, so a package's CSS or assets imported from JavaScript stay bundled and webpack keeps processing them; +- CSS `@import` and `url()` references, which are handled by their own presets; +- a request that [`resolve.alias`](/configuration/resolve/#resolvealias) sends to a different package, since the external would keep the original request and load the wrong one. + +The external is emitted as [`node-commonjs`](#externalstypenode-commonjs), or as [`module-import`](#externalstypemodule-import) when [`output.module`](/configuration/output/#outputmodule) is enabled; a `require()` dependency stays `node-commonjs` either way, so its `require()` semantics are preserved. + +T> This preset replaces the [`webpack-node-externals`](https://github.com/liady/webpack-node-externals) plugin, which did the same thing from outside webpack, and it has the resolver's answer instead of guessing from the request string. + +#### externalsPresets.nodeModules.allowlist + +Some installed packages still have to be bundled: one that only ships ESM while the output is CommonJS, a workspace package that is not published next to the output, or a package you want processed by your loaders. Pass them in `allowlist` to keep them bundled: + +```js +export default { + // ... + externalsPresets: { + nodeModules: { + allowlist: [ + // an exact request + "some-esm-only-package", + // everything under a scope + /^@my-company\//, + // or decide per request + (request) => request.startsWith("internal-"), + ], + }, + }, +}; +``` + +Each entry is a string matched exactly, a `RegExp` tested against the request, or a function returning `true` for the requests that should stay bundled. + +T> Turn on [`performance.unusedExternals`](/configuration/performance/#performanceunusedexternals) while you are tuning the list: it reports the requests listed in `externals` that no module imported, which is what a misspelled allowlist entry looks like. From 4600369c15442b4a52b362e16143349d718a9057 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 21:02:42 -0500 Subject: [PATCH 04/10] docs(output): document contained path placeholders, umdAmdContainer, resourceHints.dedupe and per-entry html Adds the [containedfile]/[containedpath] template placeholders, output.library.umdAmdContainer and output.resourceHints.dedupe from webpack 5.110.0, plus the entry descriptor's html option, which now accepts an object overriding output.html per entry. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/configuration/entry-context.mdx | 47 ++++++++++++ src/content/configuration/output.mdx | 84 ++++++++++++++++++--- 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/content/configuration/entry-context.mdx b/src/content/configuration/entry-context.mdx index 5066301fd63e..fe8bb07aa9ed 100644 --- a/src/content/configuration/entry-context.mdx +++ b/src/content/configuration/entry-context.mdx @@ -98,6 +98,53 @@ Descriptor syntax might be used to pass additional options to an entry point. T> The `worker` flag (added in webpack 5.108.0) marks an entry as a worker so its output file uses [`output.workerChunkFilename`](/configuration/output/#outputworkerchunkfilename) instead of the regular chunk filename. webpack sets it automatically for entries it creates from `new Worker(new URL(...))`, so you rarely set it by hand. +### HTML per entry + + + +When [`output.html`](/configuration/output/#outputhtml) is enabled, every non-HTML entrypoint gets a generated page. The descriptor's `html` option decides that per entry: `false` leaves the entry without a page even though `output.html` is on, and `true` gives it one when `output.html` is off. + +```js +export default { + experiments: { html: true }, + entry: { + app: "./app.js", + // no page for this one + embed: { import: "./embed.js", html: false }, + }, + output: { + html: true, + }, +}; +``` + +Since webpack 5.110.0 it also accepts an object, which overrides [`output.html`](/configuration/output/#outputhtml) option by option for that entry. Anything the object does not name keeps the value from `output.html`: + +```js +export default { + experiments: { html: true }, + entry: { + app: "./app.js", + admin: { + import: "./admin.js", + html: { + // only these two differ from output.html + title: "Admin", + scriptLoading: "blocking", + }, + }, + }, + output: { + html: { + title: "My App", + scriptLoading: "defer", + }, + }, +}; +``` + +T> [`output.html.inline`](/configuration/output/#outputhtmlinline) is resolved once per generated page, so it can only be set on `output.html` and not per entry. + ### Output filename By default, the output filename for the entry chunk is extracted from [`output.filename`](/configuration/output/#outputfilename) but you can specify a custom output filename for a specific entry: diff --git a/src/content/configuration/output.mdx b/src/content/configuration/output.mdx index 05d85f49f2d2..a83828d3e9f9 100644 --- a/src/content/configuration/output.mdx +++ b/src/content/configuration/output.mdx @@ -670,16 +670,18 @@ Substitutions available on Module-level: Substitutions available on File-level: -| Template | Description | -| ---------- | --------------------------------------------------------------------------------- | -| [file] | Filename and path, without query or fragment | -| [query] | Query with leading `?` | -| [fragment] | Fragment with leading `#` | -| [base] | Only filename (including extensions), without path | -| [filebase] | Same, but deprecated | -| [path] | Only path, without filename | -| [name] | Only filename without extension or path | -| [ext] | Extension with leading `.` (not available for [output.filename](#outputfilename)) | +| Template | Description | +| --------------- | --------------------------------------------------------------------------------- | +| [file] | Filename and path, without query or fragment | +| [query] | Query with leading `?` | +| [fragment] | Fragment with leading `#` | +| [base] | Only filename (including extensions), without path | +| [filebase] | Same, but deprecated | +| [path] | Only path, without filename | +| [containedfile] | Same as `[file]`, kept inside `output.path` | +| [containedpath] | Same as `[path]`, kept inside `output.path` | +| [name] | Only filename without extension or path | +| [ext] | Extension with leading `.` (not available for [output.filename](#outputfilename)) | Substitutions available on URL-level: @@ -689,6 +691,21 @@ Substitutions available on URL-level: T> `[file]` equals `[path][base]`. `[base]` equals `[name][ext]`. The full path is `[path][name][ext][query][fragment]` or `[path][base][query][fragment]` or `[file][query][fragment]`. +`[containedfile]` and `[containedpath]` are `[file]` and `[path]` rewritten so the result always stays under [`output.path`](#outputpath): a leading absolute root and every `../` segment become `_/`. A module resolved outside the [`context`](/configuration/entry-context/#context), a linked package or a file above the project root, has a `[path]` starting with `..`, and using it in a filename template writes the asset outside the output directory. The contained placeholders keep the same directory structure inside it instead: + +```js +// a module at ../shared/logo.png, with context at ./src +export default { + // ... + output: { + assetModuleFilename: "[containedpath][name][ext]", // _/shared/logo.png + // "[path][name][ext]" would emit ../shared/logo.png, outside output.path + }, +}; +``` + +With [`experiments.futureDefaults`](/configuration/experiments/#experimentsfuturedefaults) enabled, `[path]` and `[file]` already behave this way for asset and HTML modules, so the contained placeholders are what those templates resolve to anyway. This will become the default in webpack 6. + The length of hashes (`[hash]`, `[contenthash]` or `[chunkhash]`) can be specified using `[hash:16]` (defaults to 20). Alternatively, specify [`output.hashDigestLength`](#outputhashdigestlength) to configure the length globally. Since webpack 5.108.0, the hash digest encoding can also be set inline as `[::]`, for example `[contenthash:base64:8]`. The digest defaults to [`output.hashDigest`](#outputhashdigest) (`hex`), and the same form works in CSS module [`localIdentName`](/loaders/css-loader/#localidentname) placeholders. @@ -1372,6 +1389,31 @@ Which will result in the following bundle: globalThis.clientContainer.define(/* define args */); // or 'amd-require' window['clientContainer'].require(/*require args*/); ``` +### output.library.umdAmdContainer + + + +`string` + +Add a branch to the UMD wrapper for an AMD-style loader that exposes `define` on a container object rather than as a global, given as a dot-separated path. The branch is emitted right after the standard `define.amd` one, so a plain AMD loader still wins and the container is only used where it is the loader present: + +```js +export default { + // ... + output: { + library: { + name: "MyLibrary", + type: "umd", + umdAmdContainer: "myContainer.amdLoader", + }, + }, +}; +``` + +The value must be a dot-separated identifier path (`myContainer.amdLoader`), not an arbitrary expression. + +W> This is not [`output.library.amdContainer`](#outputlibraryamdcontainer), which applies to the `amd` and `amd-require` library types and calls `define`/`require` through the container. `umdAmdContainer` only adds a branch to the `umd` wrapper's detection chain. + ### output.library.name ```js @@ -2810,6 +2852,28 @@ export default { }; ``` +### output.resourceHints.dedupe + + + +`boolean = false` + +Skip the runtime-injected `` for a chunk that the document already preloads or prefetches. Some browsers, Chrome among them, fetch the chunk twice when both links are present. + +```js +export default { + // ... + output: { + resourceHints: { + initial: true, + dedupe: true, + }, + }, +}; +``` + +T> [`performance.conflictingResourceHints`](/configuration/performance/#performanceconflictingresourcehints) reports the other half of the problem: a chunk asked for as both prefetch and preload from the same place, where the two directives contradict each other. + ### output.resourceHints.manifest `string` From b52556c4677ec33e05dac34b2bed9b21e795c2d0 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 21:05:24 -0500 Subject: [PATCH 05/10] docs(optimization): document minimize per asset type webpack 5.110.0 makes optimization.minimize accept an object that configures the built-in minimizer per asset type, and the default minimizer now handles CSS and HTML too. Documents the javascript, css and html option sets and when each minifier runs. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/configuration/optimization.mdx | 139 ++++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/src/content/configuration/optimization.mdx b/src/content/configuration/optimization.mdx index 91e979568ae2..e93d8759dc2f 100644 --- a/src/content/configuration/optimization.mdx +++ b/src/content/configuration/optimization.mdx @@ -406,7 +406,7 @@ export default { ## optimization.minimize -`boolean` +`boolean` `object` Tell webpack to minimize the bundle using the [MinimizerPlugin](/plugins/minimizer-webpack-plugin/) or the plugin(s) specified in [`optimization.minimizer`](#optimizationminimizer). @@ -431,6 +431,143 @@ export default { T> Learn how [mode](/configuration/mode/) works. +### Minimizing per asset type + + + +Since webpack 5.110.0 the default minimizer also handles CSS and HTML assets, not only JavaScript, and `optimization.minimize` accepts an object that configures it per asset type: + +```js +export default { + // ... + optimization: { + minimize: { + javascript: { compress: { passes: 3 } }, + css: { rewriteCustomProperties: true }, + html: false, + }, + }, +}; +``` + +An asset type the object does not name is minimized with its defaults, and `false` disables minimizing that type while the others keep running. `minimize: true` is the same as `minimize: {}`. + +The CSS and HTML minifiers only run when the corresponding built-in support is enabled ([`experiments.css`](/configuration/experiments/#experimentscss), [`experiments.html`](/configuration/experiments/#experimentshtml)), and webpack steps aside for a minimizer you already configured for those assets, so an existing `css-minimizer-webpack-plugin` setup keeps owning its assets. + +W> The object configures the minimizer webpack installs by default. Replacing [`optimization.minimizer`](#optimizationminimizer) with your own plugins replaces that minimizer, and the options here no longer apply. + +### optimization.minimize.javascript + + + +`object = { compress: { passes: 2 } }` `false` + +Options handed as-is to the JavaScript minimizer, which is terser-compatible. `false` leaves JavaScript unminified while CSS and HTML are still minimized. + +```js +export default { + // ... + optimization: { + minimize: { + javascript: { + compress: { passes: 2, drop_console: true }, + format: { comments: false }, + }, + }, + }, +}; +``` + +### optimization.minimize.css + + + +`object` `false` + +What the built-in CSS minifier may do beyond the transforms that always apply. The options apply wherever that minifier runs: on `.css` assets and on the inline `${html}`; +} +``` + +The registry is keyed by style identifier and namespaced with [`output.uniqueName`](/configuration/output/#outputuniquename), so several bundles rendering in the same process do not read each other's styles. + ## \_\_webpack_get_script_filename\_\_ (webpack-specific) `function (chunkId)` diff --git a/src/content/api/node.mdx b/src/content/api/node.mdx index f5a6124bc1d0..5b977878ca82 100644 --- a/src/content/api/node.mdx +++ b/src/content/api/node.mdx @@ -289,6 +289,36 @@ W> Multiple configurations will **not be run in parallel**. Each configuration is only processed after the previous one has finished processing. +### MultiCompiler hooks + +A `MultiCompiler` exposes the child compilers' hooks as one, so a plugin can tap the set instead of every child: + +| Hook | Type | Description | +| ------------------- | --------------------------------------- | -------------------------------------------------------------------------------- | +| `done` | `SyncHook<[MultiStats, Compiler[]]>` | Called once every child compilation has finished. Aggregated, not a `MultiHook`. | +| `shutdown` | `AsyncSeriesHook<[Compiler]>` | Called when the compilers are closing, once per child. | +| `invalid` | `SyncHook<[string \| null, number]>` | Called when a watched file changes in any of the children. | +| `run` | `AsyncSeriesHook<[Compiler]>` | Called before a child starts a non-watch build. | +| `watchRun` | `AsyncSeriesHook<[Compiler]>` | Called before a child starts a watch build. | +| `watchClose` | `SyncHook<[]>` | Called when watching stops. | +| `infrastructureLog` | `SyncBailHook<[string, string, any[]]>` | Infrastructure logging for any child. | + +Since webpack 5.110.0 the `done` hook is also handed the compilers whose build actually ran, in configuration order. In watch mode a change usually invalidates only some of the children, and the others are reported from their previous stats, so this is how a plugin tells what is new: + +```js +import webpack from "webpack"; + +const compiler = webpack([config1, config2]); + +compiler.hooks.done.tap("MyPlugin", (multiStats, changedCompilers) => { + for (const child of changedCompilers) { + console.log(`${child.name} rebuilt`); + } +}); +``` + +On the first build every child is reported as changed. `shutdown` was added in the same release and lets a plugin release resources once, for the whole set, rather than tapping each child's own [`shutdown`](/api/compiler-hooks/#shutdown). + ## Error Handling For good error handling, you need to account for these three types of errors: From eb821fba838301341093d483165a9cda0dc1b113 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 26 Aug 2026 21:11:08 -0500 Subject: [PATCH 08/10] docs: document renderEmbeddedSource and HTML head hot patching Adds the renderEmbeddedSource and embeddedSourceHash compilation hooks, and updates the HTML HMR note: 5.110.0 patches the head in place instead of falling back to a full page reload. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/api/compilation-hooks.mdx | 36 +++++++++++++++++++++++ src/content/configuration/experiments.mdx | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/content/api/compilation-hooks.mdx b/src/content/api/compilation-hooks.mdx index 5cd2376ba54d..77f863897b09 100644 --- a/src/content/api/compilation-hooks.mdx +++ b/src/content/api/compilation-hooks.mdx @@ -303,6 +303,42 @@ Store chunk info to the records. This is only triggered if [`shouldRecord`](#sho - Callback Parameters: `chunks` `records` +## renderEmbeddedSource + + + +`AsyncSeriesWaterfallHook` + +Called with source written in one language that a module emits inside another, before it is embedded. Today that is CSS or HTML reaching the bundle as a JavaScript string literal. Return the source to embed, possibly transformed. + +- Callback Parameters: `source`, `info` + +`info` carries `type` (the embedded source's type, `"css"` or `"html"`), `hostType` (the type it is embedded in, `"javascript"`) and `module` (the module being generated), so one tap can serve every pair. + +```js +compilation.hooks.renderEmbeddedSource.tapPromise( + "MyPlugin", + async (source, info) => { + if (info.type !== "css") return source; + return new RawSource(await minifyCss(source.source())); + }, +); +``` + +No asset ever carries this source, so an asset-level minimizer cannot reach it. That is what the hook is for. + +W> Module hashes are taken before code generation, so a tap must write whatever it varies on into the hash through [`embeddedSourceHash`](#embeddedsourcehash). Otherwise the code generation cache replays output produced before your options changed. + +## embeddedSourceHash + + + +`SyncHook` + +Called while hashing a module that embeds a source of another language. Tap it to add whatever your [`renderEmbeddedSource`](#renderembeddedsource) tap varies on to the module hash. + +- Callback Parameters: `module`, `hash` + ## beforeModuleHash `SyncHook` diff --git a/src/content/configuration/experiments.mdx b/src/content/configuration/experiments.mdx index 926972a44416..a03373d0c113 100644 --- a/src/content/configuration/experiments.mdx +++ b/src/content/configuration/experiments.mdx @@ -516,7 +516,7 @@ When [`experiments.css`](#experimentscss) is enabled, `.css` is likewise appende HTML modules support [Hot Module Replacement](/concepts/hot-module-replacement/). No extra configuration is needed. It activates automatically when HMR is enabled (for example via [`devServer.hot`](/configuration/dev-server/#devserverhot)). -For a page extracted to a real `.html` file, each hot update patches `document.body.innerHTML` and `document.title` in place instead of triggering a full reload. Changes to `` beyond the `` (a new `<meta>`, a swapped `<link rel="icon">`, …) cannot be safely DOM-patched, so the shim falls back to a full page reload. +For a page extracted to a real `.html` file, each hot update patches `document.body.innerHTML` and `document.title` in place instead of triggering a full reload. Since webpack 5.110.0 the `<head>` is patched in place as well, so a new `<meta>`, a swapped `<link rel="icon">` or a removed `<script>` that never executed no longer costs a full page reload. T> [Module concatenation](/configuration/optimization/#optimizationconcatenatemodules) is disabled for HTML modules while HMR is active, because each module needs its own `module.hot` scope to self-accept updates. From dbb5041d49bf0aefa8127b134f04eda3f5925b8e Mon Sep 17 00:00:00 2001 From: Sebastian Beltran <bjohansebas@gmail.com> Date: Wed, 26 Aug 2026 21:13:06 -0500 Subject: [PATCH 09/10] docs(performance): fix dataUrlCondition link target Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- src/content/configuration/performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/configuration/performance.mdx b/src/content/configuration/performance.mdx index ebbf0995f290..ee590294b1fb 100644 --- a/src/content/configuration/performance.mdx +++ b/src/content/configuration/performance.mdx @@ -275,7 +275,7 @@ Report the loaders, plugins and hooks that hold the main thread, timing each one `boolean = false` -Report assets inlined as data urls that are large enough for the base64 cost and the lost caching to outweigh the request they save. A data url costs about a third more than the file it replaces, cannot be cached on its own, and is downloaded again whenever the code around it changes. [`Rule.parser.dataUrlCondition.maxSize`](/guides/asset-modules/#general-asset-type) decides which files are small enough to be worth that. +Report assets inlined as data urls that are large enough for the base64 cost and the lost caching to outweigh the request they save. A data url costs about a third more than the file it replaces, cannot be cached on its own, and is downloaded again whenever the code around it changes. [`Rule.parser.dataUrlCondition.maxSize`](/configuration/module/#ruleparserdataurlcondition) decides which files are small enough to be worth that. ### performance.largeModules From ffbe0209539f501f6160b2f52ef2e399306b2b28 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran <bjohansebas@gmail.com> Date: Thu, 27 Aug 2026 22:48:28 -0500 Subject: [PATCH 10/10] docs(optimization): keep main's inline comment formatting The local prettier (3.8.3) reformatted two of main's inline block comments onto their own lines; the declared version (3.9.6), which CI installs, wants them inline, which is how they are committed on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- src/content/configuration/optimization.mdx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/content/configuration/optimization.mdx b/src/content/configuration/optimization.mdx index 0dec8b3d795c..bea4ab5ab3a6 100644 --- a/src/content/configuration/optimization.mdx +++ b/src/content/configuration/optimization.mdx @@ -446,9 +446,7 @@ export default { // ... optimization: { minimize: { - javascript: { - /* handed to the JavaScript minimizer as-is */ - }, + javascript: {/* handed to the JavaScript minimizer as-is */}, css: { comments: false, }, @@ -574,9 +572,7 @@ export default { optimization: { minimizer: [ (compiler) => { - new MinimizerPlugin({ - /* your config */ - }).apply(compiler); + new MinimizerPlugin({/* your config */}).apply(compiler); }, ], },