Webpack 5.111

Webpack 5.111 is here! This release takes a few more tasks off your hands: copying public files, preparing CSS for older browsers, and finding code that no longer needs to ship. It also makes ES module output stable, so you can remove the experimental flag from your configuration.

You'll also find a smaller chunk-loading runtime, two fewer dependencies, fixes for hot updates and caching, and a few new tools for plugin authors. Let's look at what changes for your project and what to update if you're coming from 5.110.

Explore what's new:

ES Module Output Without an Experiment

Shipping your app or library as ES modules? You can now remove experiments.outputModule and keep output.module. ESM output is stable, and the module and modern-module library types and externalsType: "module" are available without the experiment, too:

 export default {
-  experiments: {
-    outputModule: true,
-  },
   output: {
     module: true,
   },
 };

One detail to check when upgrading: if you only had experiments.outputModule: true, add output.module: true. Webpack now ignores the old flag, so keeping it alone won't enable ESM output. The universal, deno, and bun targets still use ESM by default; other targets still use classic output by default.

A module or modern-module library type now turns output.module on by itself. That library only exists as an ECMAScript module, so you no longer have to ask for the same thing twice:

export default {
  output: {
    library: {
      type: "module",
    },
  },
};

Want webpack 6's defaults today? experiments.futureDefaults now includes output.module, so the build emits an ECMAScript module wherever your target can read one. Three things keep the classic script output there: a target that states it cannot read a module (a browserslist query resolving to engines such as ie 11, or a version below it like node10), a library type read out of a script such as var or umd, and setting output.module yourself, which wins in both directions.

Async startup gets an improvement as well. If your entry uses top-level await to load configuration, code that imports your bundle now waits until that setup finishes and receives an error if it fails. This works when your target supports top-level await. Webpack detects that support for you; output.environment.topLevelAwait lets you override it in the output environment settings.

ES Module Libraries and Externals

If you publish an ES module library that re-exports from a package you keep external, several things behave correctly now:

export * from "lodash";
export { render } from "react-dom";
  • Live bindings survive. A value the external package reassigns later is seen by your consumers, because webpack emits a native re-export instead of copying the value once.
  • Your externals mapping is used. A star re-export writes the request you configured rather than the one spelled in your source.
  • Import attributes are kept, so export * from "./data.json" with { type: "json" } keeps them in the emitted statement.
  • Cycles no longer overflow the stack, which they did when two modules star re-exported each other through an external.
  • Specifiers are escaped, so a request containing an unusual character reaches the output as a valid string literal.

One case is now reported instead of being emitted incorrectly: an external declared as an array is a module specifier plus a property path, and export * from that external would re-export the module rather than the property it names. Webpack reports it as a build error, so you can name the export you meant.

File URLs in Configuration

If you write your webpack configuration as an ES module, you can now pass file: URL strings directly to path options such as context and output.path. That saves the extra fileURLToPath() conversion:

-import { fileURLToPath } from "node:url";
-
 export default {
   output: {
-    path: fileURLToPath(new URL("./dist/", import.meta.url)),
+    path: new URL("./dist/", import.meta.url).href,
   },
 };

You can use the same approach in rule conditions such as test and include. Just remember the .href: webpack expects the URL as a string here, rather than a URL object.

Copying Static Files

Your robots.txt, public images, and license files need to reach the output directory even if nothing imports them. With output.copy, you can ask webpack to copy a whole folder as part of the build:

export default {
  output: {
    copy: "public",
  },
};

That copies the contents of public/ into your output directory, keeping their folder structure. You no longer need a separate copy step for this setup. Edit a file while watch mode is running and webpack copies the updated version for you. The files also appear in build stats and stay in place when output.clean cleans the output.

To copy from several places or choose where the files land, use patterns:

export default {
  output: {
    copy: [
      "public",
      { from: ["licenses/*.txt", "vendor/licenses/*.txt"], to: "licenses" },
      { from: "images", to: "img", filename: "[name].[contenthash][ext]" },
    ],
  },
};

You can also filter files, transform their contents, or preserve their permissions and timestamps. Webpack warns if a pattern finds nothing and reports an error if a copy would overwrite a generated asset, helping you catch a wrong path early. For more control over when copying happens and how many files are processed at once, use the built-in CopyPlugin.

CSS for Your Browser Targets

You can write modern CSS while webpack handles more of the work needed for older browsers. When you use a browserslist target, the built-in CSS minimizer checks which browsers you're supporting. lowerUnsupported rewrites supported cases of newer syntax into forms those browsers understand, and colorFallbacks adds a compatible color declaration before a newer one.

Both options are on by default. This release covers more syntax, including inset, media ranges, and CSS nesting, and improves how the minimizer simplifies math and colors. You can adjust the individual settings through optimization.minimize.css:

export default {
  target: "browserslist",
  optimization: {
    minimize: {
      css: {
        lowerUnsupported: true,
        colorFallbacks: true,
      },
    },
  },
};

For example, if a browser in your target list doesn't understand inset, webpack writes out the four sides instead. Here's the change, formatted for readability:

 .panel {
-  inset: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
 }

If your project needs more control, there are a few options you can turn on:

  • unusedSymbols lets you remove classes, ids, keyframes, and custom properties you've identified as unused. You supply the names after checking your project; webpack doesn't find them automatically.
  • pseudoClasses helps if you use a script to provide a pseudo-class behavior. For example, { "focus-visible": "focus-visible" } replaces :focus-visible with .focus-visible, which your script can apply.
  • resolveCustomAtRules lets the minimizer handle @custom-media and @custom-selector in stylesheets that haven't gone through webpack's CSS parser.
  • rewriteDirSelector replaces an unsupported :dir() with an approximation using attribute selectors. Enable it only if that approximation fits your styles.
  • mergeDistantRules combines rules with matching declarations even when other rules sit between them. It's off by default because changing their order can affect which styles win.

Minification also preserves colors more carefully when another CSS expression reads their channels, and keeps CSS layer blocks in the right order. These fixes help your production styles behave as you wrote them.

CSS Modules get a fix worth knowing about too: a @value used in an at-rule prelude, such as a @keyframes name, is scoped exactly once now. It used to be rewritten a second time by the generic value pass, which emitted a doubled name that no animation matched.

HTML and Embedded Content

If you use webpack's native HTML support, this release tidies up more of your generated pages. The minimizer trims whitespace around URLs in attributes such as href and src. You can also opt into joining adjacent bare script elements with mergeScripts, or shortening more boolean attributes with collapseBooleanAttributes: "all". See optimization.minimize.html for the settings.

Attributes are also minified by what their value says rather than by how your source spelled it. A quoted value was rewritten while an unquoted one was echoed as written, so method="GET" folded and method=GET did not. Both take the same path now, including boolean collapsing (checked=checked) and character reference decoding.

If you use a custom minimizer, you can now give it inline CSS and JSON through renderEmbeddedSource, keeping those parts of your pages under the same control as standalone files. Webpack can handle the content your minimizer leaves to it. Event handler attributes can also be sent to a minimizer with the context it needs to read them correctly.

Building a plugin? You can reuse webpack's own CSS and HTML minifiers through webpack.css.syntax.cssMinify and webpack.html.syntax.htmlMinify.

Plugins can also do asynchronous work in NormalModule's processResult hook. For example, an image plugin can convert a PNG to WebP and set module.buildInfo.assetResource to the new path. Webpack then uses the matching filename and inline media type, so references keep pointing to the right file, even when the build uses the cache.

HTML entries also produce output that's easier to navigate: page chunks use names based on your entry or source file, and redundant JavaScript copies are removed. If your HTML contains template delimiters in attribute names or lists, those are now preserved during minification.

More Useful Performance Hints

Ever removed an image from a page and wondered why it still appears in your output? Or opened a source map that points to the wrong lines? The new checks help you track down problems like these:

  • sourceMaps helps you find maps adding weight to production bundles and loaders that change code without returning a map, making debugging harder.
  • unusedAssets points out files still being emitted for imports you no longer use, so you can clean up the imports keeping them around.
  • unusedModules helps answer "why is this module still in my bundle?" by identifying the re-export or side effect keeping it there.
  • analyzableBailouts helps when you pass ESM output to another tool. It explains what prevents a reference from becoming a literal import() or new URL() that the tool can follow.

If several pages include copies of the same module, duplicate-module reporting now catches those copies even when webpack has merged modules together during optimization.

These reports also survive the cache. The bailouts webpack notices while parsing a module are stored with that module now, so a build that restores it from the filesystem cache reports the same reasons as the build that parsed it, rather than a shorter list.

Updating existing checks

If you enabled checks in 5.110, take a moment to update their names. We've grouped several related checks together, and webpack will report a configuration error if you keep the removed names:

Previous optionUse in 5.111
unusedAliases, unusedDefines, unusedExternals, unusedRulesunusedConfig
unusedReexportsunusedModules
embeddedSourceMapssourceMaps
entrypointOverlapduplicateModules

unusedConfig groups the alias, define, external, and rule checks. It also reports Module Federation shared entries and remotes that nothing imports, helping you clean up configuration left behind as your app changes. For example:

export default {
  performance: {
    hints: "warning",
    unusedConfig: true,
    sourceMaps: true,
    unusedAssets: true,
  },
};

You choose which checks to enable. If you'd like to explore their findings without adding warnings to your build, use performance.hints: "stats". This includes unusedConfig. One distinction to keep in mind: hints: false silences bundle checks, but enabled configuration checks still warn. Turn off their individual options to silence those too. The performance documentation has the full list of renamed options.

Smaller Output and Less Memory

Every build gets a little lighter here, with nothing to configure.

The chunk-loading runtime shrinks. __webpack_require__.e calls a chunk's single loading handler directly instead of walking a map of every handler that might apply, and the priority queue behind __webpack_require__.O is emitted only for a build that passes a priority, which today means one using prefetching.

With ESM output, chunk imports moved into the chunk loader as well, instead of being written at each import() site. Your output stays walkable, since every emitted chunk is still reached by a literal specifier another bundler or a preload scanner can follow. Rebuilds get cheaper too: the importing module no longer carries the imported chunk's content hash, so editing a lazily loaded route stops invalidating the chunks on the path up to your initial bundle.

Webpack holds less memory, too. Parsers, generators, and the rendered sources from the last build are released from nested child compilations and while a watch build sits idle, which took 6.3 MB off an 85.6 MB idle heap on a 1700-module project. The emitted assets are unchanged.

Webpack's Own JavaScript Parser

Some improvements happen behind the scenes. Webpack parses your JavaScript with its own parser now, and we've reduced allocations in CSS and HTML output and the work involved in saving the cache. You get these changes simply by upgrading.

That parser is why acorn is no longer a webpack dependency, and neo-async is gone as well, replaced by a built-in helper. Two packages fewer to install, resolve, and audit.

There's also a useful change if your app runs in a newer environment than your build machine. Webpack can now read regular expressions that the Node.js version running the build cannot execute. The browser or runtime running your app still needs to support those expressions, but webpack can process your source without that getting in the way.

Syntax errors also keep their source locations when restored from the filesystem cache, so the next build still points you to the line you need to fix.

Optional Cache Dependencies

If you share a webpack configuration across projects, some may have a tailwind.config.js and others may not. You can now tell the filesystem cache that a build dependency is optional, so a missing file does not prevent webpack from saving the cache:

// webpack.config.cjs
const path = require("node:path");

module.exports = {
  cache: {
    type: "filesystem",
    buildDependencies: {
      config: [__filename],
      tailwind: [
        {
          dependency: path.resolve(__dirname, "tailwind.config.js"),
          optional: true,
        },
      ],
    },
  },
};

Projects without that file can still save and reuse their cache. Add or remove it later, and webpack knows to rebuild with the new setup instead of reusing an outdated result.

Module Namespace Objects

When you write import * as utils from "./utils.js", you usually just call something like utils.format(). Some libraries look more closely at the utils object itself: which keys it has, what its prototype is, or whether it can be changed. The new module.parser.javascript.specNamespaceObject option helps that code behave more like native ESM. It provides a null prototype, sorted export names, bindings that stay up to date, and protection against changes to the namespace.

The option also covers modules with no namespace of their own, such as JSON, text, and CommonJS. For those, the namespace synthesized for them gets the same shape, and every importer of one module is handed the same object rather than a look-alike. Reading a name the module doesn't export answers undefined, where it used to reach through Object.prototype and return things like toString. Deferred imports report the same shape once their exports are known.

Most apps can leave this off. If you need it, enable it on the module you're importing and make sure your runtime supports Proxy. There's a tradeoff: webpack keeps all of that module's exported names and can't merge it with other modules, so you get extra runtime code and less optimization. The modules importing it can still be merged as usual.

Other Improvements

A few smaller changes may affect your configuration:

  • resolve.fileSystem is honored. Webpack used to assign the compiler's input filesystem over whatever you configured, so a custom filesystem never reached the resolver. It is the default now, the way resolve.resolver already behaved, and resolveLoader.fileSystem works the same for loaders.
  • module.exprContextCritical says where it moved. Like its siblings, it is deprecated in favor of module.parser.javascript.exprContextCritical, and the message now tells you so.
  • Reading a property off a DefinePlugin value that is undefined produces code that throws, which is what the same expression does without the plugin. Use optional chaining where you expect the value to be missing.

Bug Fixes

This release also makes everyday development smoother:

  • More reliable rebuilds. Fixes to CSS hot updates and cache invalidation help your latest changes appear correctly.
  • More predictable runtime behavior. Fixes cover async initialization, optimized JavaScript, and CSS in server and loader workflows.
  • Clearer build errors. Missing assets and runtime code generation failures now surface as errors instead of leaving builds stuck or hiding the cause.
  • Output that reads back as written. Scope hoisting keeps a wrapped CommonJS reference a separate statement, an anonymous export default class is named before its static initializers run, and HTML text ending in </ is escaped so the tag after it can't turn the text into a comment.
  • Correct rebuilds for more setups. A DelegatedModule is rebuilt when the DLL manifest's metadata changes, a cached module takes its resolveOptions from the factory rather than the cache, and a consumer is regenerated when the value webpack inlined into it changes.

See the 5.111.0 release notes for the complete list of changes and contributors.

Thanks

Thanks to everyone who contributed code, improved the docs, tested a change, or took the time to report a bug. And thank you to our sponsors for helping make this work possible.

Edit this page·
« Previous
Blog

1 Contributor

bjohansebas