Skip to content

Adopting Webpack 5 in Your Bundle

Engine 7.1.0 compiles your bundle with webpack 5, replacing webpack 4. The upgrade is fully backward compatible: your bundle compiles and renders exactly as before, no changes required.

This guide is for teams who want to go further. Webpack 5 rewards a few patterns much more than webpack 4 did, and your bundle likely has easy wins.

Four changes, in order of impact:

  1. Prefer ES-module (tree-shakeable) dependencies over CommonJS.
  2. Lazy-load heavy, conditional components.
  3. Mark render-once components static.
  4. Keep Node-only code out of client components.

Without any changes, you already get smaller compiled bundles (up to ~20% on large bundles), faster local dev rebuilds, and leaner build logs.

1. Prefer tree-shakeable (ESM) dependencies

Webpack 5’s dead-code elimination analyzes nested exports and drops unused module internals, on a large production bundle we saw ~1,000 modules eliminated that webpack 4 used to ship. Tree-shaking only works on ES modules; a CommonJS require is opaque, so the whole package ships. See Optimize your client-side bundle size by tree shaking your dependencies for the fundamentals.

Steps:

  1. Find your biggest dependencies. Your compiled client bundles are in .fusion/dist/components/ after a local build.
  2. Check whether an ESM build or ESM-native alternative exists for each.
  3. Import specific entry points instead of the package root, where supported.
// Before — CommonJS lodash ships the entire library
import _ from "lodash";
export default ({ items }) => <ul>{_.uniq(items).map(...)}</ul>;
// After — ESM lodash-es ships only `uniq` and its internals
import { uniq } from "lodash-es";
export default ({ items }) => <ul>{uniq(items).map(...)}</ul>;
// Before — pulls all of date-fns via CJS
const dateFns = require("date-fns");
// After — tree-shakeable named import
import { formatDistance } from "date-fns";

The same applies to @wpmedia/* blocks and your own shared packages. Packages that declare "sideEffects": false get shaken hardest. If you maintain internal packages, shipping an ESM build with sideEffects: false is the highest-leverage change you can make.

2. Lazy-load heavy components

Good candidates: anything most page views don’t render or interact with, modals, embeds, below-the-fold widgets, admin-only UI. Dependencies shared across lazy components get deduplicated into shared vendors-*.chunk.js files.

2a. Component.lazy = true (SSR-safe, preferred)

The page still server-renders the full component HTML, no SEO or layout impact, while the client defers downloading the component’s JS until it renders.

import HeavyChart from "@org/charting-library";
const StockChart = ({ symbol }) => <HeavyChart symbol={symbol} />;
StockChart.lazy = true; // or an array of output types, e.g. ["default", "article"]
export default StockChart;

2b. import() / React.lazy (finer-grained, has an SSR caveat)

Use this to split part of a component, e.g. loading a library only after a user interaction.

import { lazy, Suspense } from "react";
const HeavyChart = lazy(() =>
import(/* webpackChunkName: "stock-chart" */ "@org/charting-library"),
);
const StockChart = ({ symbol }) => (
<Suspense fallback={<div className="chart-placeholder" />}>
<HeavyChart symbol={symbol} />
</Suspense>
);

With React.lazy, the server renders the Suspense fallback, not the real content, so:

  • Don’t use it for SEO-critical or above-the-fold content, use Component.lazy (2a) there instead.
  • Make the fallback match the real content’s size to avoid layout shift.
  • import() from an event handler (load-on-click) has no SSR involvement and is always safe.

3. Mark render-once components static

A static component is server-rendered and its HTML preserved on the client, so its code and imports ship zero client-side JavaScript. Combined with tree-shaking, a static component’s dependencies drop out of the client graph entirely.

Good candidates: mastheads, static promos, footers, anything with no client-side interactivity.

import { massiveIconSet } from "@org/brand-assets"; // stays out of client JS
const Footer = () => <footer>{/* ... */}</footer>;
Footer.static = true;
export default Footer;

4. Keep Node-only code out of client components

Webpack 4 silently bundled browser polyfills whenever client-side code imported a Node builtin (crypto, stream, http, etc). Engine 7.1.0 still compiles your bundle the same way, but now warns you when you’re paying for a polyfill. crypto-browserify alone is several hundred KB of client JS, most imports of it are accidental.

Steps:

  1. After a build, check for these warnings:
    • HeavyPolyfillWarning — a Node builtin resolved to a browser polyfill and pulled a large dependency tree into the client bundle.
    • BuiltinFallbackGuardWarning — a server-only builtin (fs, net, …) reached the client. It resolves to an empty module and will throw at runtime. This usually means server-only code leaked into a client component.
  2. Follow the from <module> path in the warning to the offending import and fix it at the source.
// Before — pulls crypto-browserify into the client bundle
const crypto = require("crypto");
const hash = crypto.createHash("sha256").update(id).digest("hex");
// After — the browser's native Web Crypto API, zero bundle cost
// (call from a client-only code path, e.g. useEffect)
useEffect(() => {
window.crypto.subtle
.digest("SHA-256", new TextEncoder().encode(id))
.then(setDigest);
}, [id]);

Also check your dependency choices: if a “browser” flavor exists (e.g. jwt-decode instead of jsonwebtoken for reading tokens), it avoids the polyfill tree entirely.

What you get automatically

  • Smaller bundles from tree-shaking, even unchanged bundles shrink.
  • Faster local dev: a persistent filesystem cache across fusion start rebuilds, and watch mode that coalesces multi-file saves into a single rebuild.
  • Cleaner build logs: unactionable noise is gone, real errors and warnings remain.

What hasn’t changed

Some webpack 5 features are intentionally pinned to webpack 4 behavior in this release for compatibility. Changing your bundle to target these won’t do anything yet:

  • Code-splitting thresholds and entry chunks are fixed by the platform; there’s no configuration surface for this in arc.config.json.
  • package.json exports maps are ignored (webpack 4 behavior), so deep imports keep working.
  • Emitted runtime code remains ES5-compatible.
  • Filename-based long-term caching (contenthash) works exactly as before.

These are candidates for future opt-in improvements. If one matters to your team, tell us, that’s what prioritizes the roadmap.

Verifying your improvements

  1. Build locally (npx fusion start or your normal flow) on Engine 7.1.0+.
  2. Compare bundle sizes under .fusion/dist/components/combinations/<output-type>.js before and after your change.
  3. Check the build output for the two warning types from section 4.
  4. For import() changes, confirm the new *.chunk.js files exist and load in the network tab only when the feature renders.