Upgrading Webpack 4 → 5

Smaller bundles, faster build times, stricter checks, and fully typed

Reddit
LinkedIn

Webpack 5 was released last year in October. We had been hearing many great things about it in the Ecommerce department at Square, but had been reluctant to go through the upgrade work.

Upgrading developer tools is usually not pressing, can be time consuming, and modifying a bundler can be high risk. Since we rely on a few Webpack plugins, it also made sense to give the ecosystem time to adapt to the breaking changes. We also read it might be slower.

But last week, we finally upgraded Webpack to v5.3.7 from v4.46.0 in our largest Ecommerce codebase!

Why did we upgrade?

The upgrade was done by the Ecommerce Performance team, which is dedicated to improving website performance on Square Online stores. While looking for ways to reduce bundle size, we discovered that Webpack 5 had improved tree-shaking.

We hesitantly ran npm install webpack@latest, worried it might be a huge undertaking. But after following the migration guide, we had a working build in about an hour. Not only was it faster, but it also yielded a much smaller bundle size!

We had a few more blockers to resolve but this was enough of a reason to see the upgrade through.

Here are some of the biggest benefits we saw:

  • Smaller bundler sizes ~30% bundle size reduction, thanks to all the tree-shaking optimizations!
  • Waaay faster dev builds With persistent caching, our dev builds were as fast as 20 seconds compared to the 2~3 minutes we’re used to.
  • Stricter checks Webpack 5 has stricter adherence to spec and warns when violations are found.
  • Webpack 5 is fully typed This indicates higher quality code and helps improve 3rd party plugins.

Webpack 5 Gains

Decreased bundle size by 27-28%

Thanks to all the tree-shaking optimizations.

Minified size Minified + Gzipped size
Webpack 4

28.1 MB

8.1 MB

Webpack 5 -28%↓20.3 MB -27%↓5.9 MB

Decreased build time by 10% (2m 54s → 2m 36s)

Benchmarks were taken using hyperfine on 5 runs with these conditions:

  • Minification disabled
  • Localization builds disabled
  • Source-maps enabled

Webpack 4

Compiled without minification in ~2m 54s.

➜ hyperfine -r 5 "npm run prod"
Benchmark #1: npm run prod
  Time (mean ± σ):     173.916 s ±  2.602 s    [User: 212.686 s, System: 15.950 s]
  Range (min … max):   170.394 s … 176.080 s    5 runs

With minification using esbuild-loader:

Compiled in ~3m 43s.

➜ hyperfine -r 5 "npm run prod"
Benchmark #1: npm run prod
  Time (mean ± σ):     222.545 s ±  6.278 s    [User: 449.745 s, System: 38.700 s]
  Range (min … max):   215.466 s … 231.612 s    5 runs

Webpack 5

Compiled without minification in ~2m 36s.

➜ hyperfine -r 5 "npm run prod"
Benchmark #1: npm run prod
  Time (mean ± σ):     156.191 s ±  4.504 s    [User: 197.464 s, System: 16.268 s]
  Range (min … max):   149.410 s … 160.245 s    5 runs

With minification using esbuild-loader:

Compiled in ~3m 13s. Build time with minification went down by 13%.

➜ hyperfine -r 5 "npm run prod"
Benchmark #1: npm run prod
  Time (mean ± σ):     192.877 s ±  4.038 s    [User: 288.959 s, System: 25.533 s]
  Range (min … max):   188.799 s … 199.448 s    5 runs

Cached dev builds take ~19s

Persistent caching is a new feature in Webpack 5.

We only enable it in dev-mode but it is a huge improvement over the 2 minute builds we're used to.

➜ hyperfine -r 5 "npm run dev"
Benchmark #1: npm run dev
  Time (mean ± σ):     18.765 s ±  2.019 s    [User: 25.258 s, System: 5.214 s]
  Range (min … max):   16.098 s … 21.166 s    5 runs

Migration process

We were able to upgrade by mostly following the Webpack 5 release blog post and migration guide.

Corrected JSON module imports

Previously, we used named imports to import properties from JSON modules.

In Webpack 5, JSON modules are implemented to spec and only use default-exports. We corrected all named imports from JSON files to default imports.

Manually polyfilled Node.js APIs

Webpack 5 no longer automatically polyfills Node.js APIs.

The required changes depend on each app and its dependencies. You can identify what your app needs by creating a build and interacting with the app.

Webpack Reference Error An error thrown when clicking into one of the panels indicates that Buffer is not available.

For us, we needed to polyfill the Node.js stream module, and the process and Buffer globals.

The stream Node.js module was polyfilled with stream-browserify.

 const webpackConfig = {
      resolve: {
          ...,
+         fallback: {
+             stream: 'stream-browserify'
+         }
      },

      ...
  }

The process and Buffer globals were polyfilled with node-process and buffer.

 const webpackConfig = {
      ...,

      plugins: [
+         new webpack.ProvidePlugin({
+             process: 'process/browser',
+             Buffer: ['buffer', 'Buffer'],
+         }),
          ...
      }
  }

Upgraded loaders & plugins

cache-loader

With Webpack 4, we used cache-loader to cache the results from some of the most active loaders to disk (eg. babel-loader, vue-loader, etc).

With persistent caching built into Webpack 5, we were able to replace it with:

 const webpackConfig = {
      ...,

+     cache: {
+         type: 'filesystem',
+         buildDependencies: {
+             config: [__filename],
+         },
+     }
  }

webpack-manifest-plugin

We use the webpack-manifest-plugin plugin to get a list of assets for our PHP backend to serve.

We upgraded to v3.0.0 for Webpack 5 support.

- const ManifestPlugin = require('webpack-manifest-plugin')
+ const { WebpackManifestPlugin } = require('webpack-manifest-plugin')

mocha-webpack

We used mocha-webpack to build and run our mocha tests. We realized mocha-webpack didn't support Webpack 5 and is no longer maintained. There was a fork maintained, but it also didn't support Webpack 5.

For Webpack 5 compatibility, we replaced mocha-webpack with instant-mocha.

i18n-webpack-plugin

We used i18n-webpack-plugin to localize the assets. However, it only supported up to Webpack 4 and the project had been deprecated with no alternative recommendations.

For Webpack 5 compatibility, we replaced i18n-webpack-plugin with webpack-localize-assets-plugin. It supports multiple locales by default, is typed, and has a few niceties such as warning on unused strings and the ability to toggle source-maps for specific locales.

Added type-checking to config (optional)

We don’t use TypeScript in this codebase, but since Webpack 5 comes with types, we can use JSDocs for IDE type-checking/IntelliSense by adding this before the Webpack config:

+ /** @type {import('webpack').Configuration} */
  const webpackConfig = {
      ...
  }

Interested in upgrading to Webpack 5?

If you're interested in seeing what kind of results you’ll get, try timeboxing an hour just to install it and to see how far you can get with the migration guide.

We're very happy with Webpack 5 and all it's improvements!

Hope the migration experience will be as smooth as ours!