Webpack 5.109
Webpack 5.109 is out, and it is one of the largest minor releases in recent memory. The headline: the built-in CSS, HTML, TypeScript, and async WebAssembly support now defaults to "auto", enabled out of the box unless a loader is already registered for those files, so existing setups keep working unchanged while new projects get the zero-config experience.
Beyond that, the native HTML support takes a big leap toward html-webpack-plugin parity, a full resource-hints system lands, and a set of Vite-compatible module APIs arrives (import.meta.glob, import.meta.env defaults, import.meta.resolve, and asset query suffixes), plus CommonJS scope hoisting, a built-in progress bar, and another large batch of performance work.
Explore what's new:
- Built-in Support Goes
"auto" - Closing the Gap with
html-webpack-plugin - Resource Hints
- Vite-Compatible Module APIs
- CommonJS Module Concatenation
- Built-in Build Progress
- Bundling Worklets
- CSS Improvements
- Externals
- Other Improvements
- webpack-dev-server 6
- Bug Fixes and Performance
Built-in Support Goes "auto"
The last few releases built native support for CSS, HTML, TypeScript, and WebAssembly into webpack core, all behind opt-in flags. Webpack 5.109 flips those flags to a new "auto" default that enables each feature unless a loader is already registered for those files:
experiments.cssstays off when a rule with a loader matches.cssor.module.css(yourcss-loader/mini-css-extract-pluginsetup wins).experiments.htmlstays off when a rule matches.html(yourhtml-loadersetup wins).experiments.typescriptenables the built-in type-stripping when Node.js supports it (>= 22.6) and no rule matches.ts/.mts/.cts(sots-loaderandswc-loaderwin).experiments.asyncWebAssemblyenables the async Wasm support unless.wasmis claimed by a rule or bysyncWebAssembly.
The result: existing configurations behave exactly as before, while a fresh project can import CSS, HTML, TypeScript, and Wasm with zero loaders and zero configuration. You can still pin any of these explicitly with true or false, and experiments.futureDefaults resolves them all to true.
Closing the Gap with html-webpack-plugin
Webpack 5.107 made HTML importable and 5.108 made it an entry point. This release is about getting closer to html-webpack-plugin: most of what the plugin is used for day to day, from head generation to CSP, is now built into webpack core.
Head Generation: title, meta, and base
output.html can now scaffold the document head for you. title sets the page <title>, meta injects <meta> tags (keys starting with og: become property attributes), and base injects a <base> element. Anything the page already declares is left untouched.
// webpack.config.js
module.exports = {
entry: { main: "./src/main.js" },
output: {
html: {
title: "My App",
meta: {
viewport: "width=device-width, initial-scale=1",
"og:title": "My App",
},
base: "https://example.com/",
},
},
};Controlling Tag Injection
output.html.inject decides where the chunk <script> / <link> tags land: 'body' (the default for classic output), 'head' (the default for ESM output), or false to suppress sibling-chunk injection entirely. When injecting into <head>, stylesheets are placed after defer / module scripts and ahead of blocking ones, the same order Vite emits.
Inlining Chunks into HTML
output.html.inline inlines the content of matching chunks directly into the HTML instead of emitting a separate tag, useful for critical CSS or a tiny runtime chunk. true inlines everything, 'script' only JavaScript, 'style' only CSS, and an array of regular expressions matches chunk names:
// webpack.config.js
module.exports = {
output: {
html: {
inline: [/^runtime/, /critical/],
},
},
};Individual references in an authored page can opt in or out with the webpackInline magic comment (<!-- webpackInline: true --> before the tag), and the page's [contenthash] accounts for the inlined content.
Favicons and Web App Manifest
output.html.favicon links favicons on generated pages, emitting every icon as a hashed asset. It scales from favicon: "./favicon.svg" to multiple icons per rel with per-icon attributes (sizes, media, color, type, crossorigin), for example light/dark variants or several sizes.
output.html.manifest generates and links a web app manifest: pass a path to an existing .webmanifest, or an object that is serialized, emitted hashed, and linked with <link rel="manifest">; its icons / screenshots paths are resolved and emitted as hashed assets too.
Content-Security-Policy and Subresource Integrity
output.html.csp injects a <meta http-equiv="Content-Security-Policy"> into every generated page. true applies a strict baseline and appends a hash of every inline <script> / <style> to script-src / style-src; the object form merges custom directives and supports a server-side nonce placeholder.
output.html.integrity adds SRI integrity attributes to the injected <script> / <link> tags, with configurable hash algorithms.
// webpack.config.js
module.exports = {
output: {
html: {
csp: true,
integrity: true,
},
},
};Hooks for Plugins
The HTML pipeline is now extensible: webpack.html.HtmlModulesPlugin exposes compilation hooks for working with the emitted pages.
Tag Hooks: injectTags and transformTags
injectTags adds structured tag descriptors with injectTo placement, and transformTags mutates, removes, or moves the page's existing <script> / <link> / <style> / <meta> tags. Tags injected through injectTags are hashed by the CSP support like the page's own inline content.
Page Hooks: transformHtml and htmlEmitted
transformHtml transforms the final HTML string just before it is written (e.g. to minify it), and htmlEmitted fires after each page is finalized.
A Smarter HTML Parser
The parser side grew as well:
- A new
htmlsource type treats a URL as a link to another HTML file bundled as its own emitted page, like Parcel's<a href="page.html">, and<link rel="preload">/<link rel="prefetch">references to scripts and styles are bundled as chunks and rewritten to the built URLs. - Built-in sources now cover the
twitter:player:streammeta content, legacy SVG references (font-face-uri,cursor,altGlyph,tref,glyphRef), and the icon / screenshot / shortcut URLs inside a<link rel="manifest">Web App Manifest. module.parser.html.asparses a source as a full document or as an element fragment (e.g.as: "tbody"keeps a bare<tr>that a document parse would drop).- A built-in source can be disabled per
tag/attributewithtype: falseinsources.
Resource Hints
Webpack can now emit resource hints (<link rel="preload">, prefetch, modulepreload, and preconnect) natively through output.resourceHints. For ES module output it is on by default, auto-emitting <link rel="modulepreload"> for each entry's initial chunks so native import() doesn't waterfall, the same behavior Vite ships. Classic output stays opt-in.
Hints land in the extracted HTML <head> for HTML entries and are wired through a chunk-startup runtime otherwise. The option accepts a shorthand (true, 'prefetch', 'preload', 'none', an array of custom descriptors, or a function receiving the auto hints) or an object form with:
urlHints: project-wide rules that hint URL-referenced assets (JavaScriptnew URL(...), CSSurl(...), HTML attributes) without magic comments, e.g. preload every.woff2.preconnect: warm the origin of a cross-originoutput.publicPath(your CDN).modulePreloadPolyfill: an inlinemodulepreloadpolyfill for targets without native support.manifest: a JSON manifest of the resolved hints per entrypoint for SSR servers, webpack's analogue of Vite'sbuild.ssrManifest.
// webpack.config.js
module.exports = {
output: {
resourceHints: {
initial: true,
urlHints: [{ test: /\.woff2$/, preload: true, as: "font" }],
},
},
};The parser side pairs with it: module.parser.<type>.urlHints scopes hint rules to one parser, module.parser.css.fontPreload auto-preloads the primary URL of each @font-face reachable from an HTML entry's initial CSS, and module.parser.javascript.dynamicImportCssPreload preloads the CSS of dynamically imported chunks so stylesheets fetch in parallel with the chunk's JavaScript.
The resolved hints are exposed on stats.entrypoints[name].resourceHints.
Vite-Compatible Module APIs
import.meta.glob
Webpack now supports import.meta.glob, compatible with Vite's glob imports. It maps each matched file to a dynamic import function (split into on-demand chunks), or to the module itself with eager: true:
const modules = import.meta.glob("./pages/*.js");
// {
// "./pages/about.js": () => import("./pages/about.js"),
// "./pages/home.js": () => import("./pages/home.js"),
// }The supported options are eager, import (pick a single export for better tree shaking), query, base, exhaustive, and a webpack-specific caseSensitive.
import.meta.env Defaults
import.meta.env now ships Vite's standard constants by default: MODE, DEV, PROD, SSR, and BASE_URL (derived from mode, the target platform, and output.publicPath). Code written against Vite's conventions, and libraries that branch on import.meta.env.DEV, work out of the box.
import.meta.resolve
import.meta.resolve("./asset") is resolved at build time to the emitted asset URL, matching the native semantics while flowing through the asset pipeline. It is controlled by the importMeta.resolve parser option.
Asset Query Suffixes
Under experiments.futureDefaults, the asset query suffixes ?raw, ?url, ?inline, and ?no-inline work with no configuration, mapping to asset/source, asset/resource, and asset/inline:
import source from "./file.txt?raw";
import dataUri from "./icon.svg?inline";
import url from "./image.png?url";They are plain default rules, so your own module.rules can still override them.
Fine-Grained import.meta Options
The importMeta parser option now accepts an object to enable or disable the evaluation of individual fields (url, env, resolve, webpackContext, custom fields, and more). As part of this, the standalone importMetaContext option is deprecated in favor of importMeta.webpackContext.
CommonJS Module Concatenation
Module concatenation (scope hoisting) was an ESM-only optimization since webpack 3. Webpack 5.109 extends it to CommonJS: modules with statically analyzable exports are concatenated into their consumers, and "weird" CommonJS modules that previously bailed the whole group out are now wrapped into the concatenation instead. Large, CommonJS-heavy dependency trees get flatter bundles with less module-boundary overhead.
This is on wherever concatenation already applies (production mode by default). If you hit an edge case, you can restrict concatenation to ES modules again:
// webpack.config.js
module.exports = {
optimization: {
concatenateModules: { commonjs: false },
},
};Built-in Build Progress
A progress bar is now built into webpack itself via infrastructureLogging.progress. Set it to 'auto' to show the interactive bar only in real terminals (and stay quiet in CI logs); under experiments.futureDefaults that is the default. This supersedes third-party progress plugins such as WebpackBar.
// webpack.config.js
module.exports = {
infrastructureLogging: {
progress: "auto",
},
};ProgressPlugin itself also learned progressBar: "auto", a configurable bar width, an estimatedTime display, and a phaseTimings breakdown printed when the build completes.
Bundling Worklets
The new module.parser.javascript.worklet option bundles Worklet entries the way workers already are:
await audioContext.audioWorklet.addModule(
new URL("./processor.js", import.meta.url),
);Audio, paint, layout, and animation worklets are recognized by default (custom syntax lists are supported). Since worklets, unlike workers, cannot load additional chunks at runtime, ESM output links a worklet's split chunks via native import, while classic script output pre-adds them through addModule from the calling scope.
CSS Improvements
Native CSS resolves @custom-media (including media-type values) and @custom-selector at-rules at build time, so these draft-spec features work without PostCSS:
@custom-media --narrow-window (max-width: 30em);
@custom-selector :--heading h1, h2, h3;
@media (--narrow-window) {
:--heading {
font-size: 1.1em;
}
}CSS Modules also now scope view-transition-name, view-transition-group, and view-transition-class names, along with ::view-transition-*() pseudo-element references, under the existing customIdents behavior, so View Transitions get the same collision-free names as your classes.
Externals
- A new
amd-asyncexternals type loads AMD-only externals at runtime through the asynchronousrequire([...])API as an async module, so the bundle itself no longer needs an AMD library wrapper. - The object form of an external accepts an
interophint ('esModule'or'default') that pins how adefaultimport of a non-ESM external behaves, independent of the importer's strictness, mirroring Rollup'soutput.interop. - An object external with no entry for the used externals type now fails the build with an actionable error instead of silently resolving to
undefinedat runtime.
Other Improvements
cache.compression: 'zstd'compresses the filesystem cache with Zstandard on Node.js >= 22.15.output.wasmStreamingFallbackfalls back to non-streaming WebAssembly instantiation when the server serves.wasmwith the wrong MIME type.- The new
strictModeViolationsparser option warns (or errors) on constructs that break at runtime in strict mode when emitted as ES module output:with, octal literals,arguments.callee, assigning to read-only globals, and more. - ESM output now emits
new URL(..., import.meta.url), worker/worklet URLs, andimport()with literal specifiers, so downstream bundlers and runtimes can statically analyze webpack's output. - Async modules compile to generator-based code on targets without native
async/awaitsupport. - Dynamic
import(specifier, options)evaluates and validates its second argument. - Custom parsers (via
module.parser.javascript.parse) no longer need to providelocdata or collect inserted semicolons; webpack derives both from node offsets and the source text. - Stats output now explains why a module was marked as not cacheable.
ChunkLoadErrorandScriptExternalLoadErrorcarry the original DOM event aserror.eventfor easier debugging.MultiCompilerexposes the activeMultiWatchingonmultiCompiler.watching.
webpack-dev-server 6
Alongside this release, webpack-dev-server@6.0.0 is out. The highlights:
- The source is now native ES modules, with both ESM and CommonJS builds exposed via
exports. - It can be used as a webpack plugin, integrating with the compiler lifecycle (no explicit compiler passing, clean shutdown, and
MultiCompilersupport). - Express 5,
http-proxy-middlewarev4,webpack-dev-middlewarev8, andchokidarv5 (with glob support inwatchFiles.options.ignored). - Requires Node.js >= 22.15.0 and webpack >= 5.101.0. SockJS,
spdy(usenode:http2via theserveroption), the proxybypassoption, and the standalone CLI flags were removed.
Check the release notes for the full list of breaking changes and the migration path.
Bug Fixes and Performance
Beyond the features, a long list of bugs has been fixed since version 5.108.
The release also continues the parser performance campaign: JavaScript, CSS, and HTML parsing are measurably faster and allocate less memory, snapshot creation and stats generation speed up on large builds, and updated webpack-sources / enhanced-resolve dependencies cut peak memory. Check the changelog for all the details. A dedicated blog post diving into all this performance work is coming soon.
As a first look at what's next: the coming version will double down on performance, moving the JavaScript syntax parser to a custom Structure-of-Arrays AST (the same design the CSS and HTML parsers already use), which in early measurements cuts end-of-build heap significantly.
Thanks
A big thank you to all our contributors and sponsors who made Webpack 5.109 possible. Your support, whether through code contributions, documentation, or financial sponsorship, helps keep Webpack evolving and improving for everyone.



