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:
- Prefer ES-module (tree-shakeable) dependencies over CommonJS.
- Lazy-load heavy, conditional components.
- Mark render-once components
static. - 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:
- Find your biggest dependencies. Your compiled client bundles are in
.fusion/dist/components/after a local build. - Check whether an ESM build or ESM-native alternative exists for each.
- Import specific entry points instead of the package root, where supported.
// Before — CommonJS lodash ships the entire libraryimport _ from "lodash";export default ({ items }) => <ul>{_.uniq(items).map(...)}</ul>;
// After — ESM lodash-es ships only `uniq` and its internalsimport { uniq } from "lodash-es";export default ({ items }) => <ul>{uniq(items).map(...)}</ul>;// Before — pulls all of date-fns via CJSconst dateFns = require("date-fns");
// After — tree-shakeable named importimport { 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:
- 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.
- 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 bundleconst 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 startrebuilds, 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.jsonexportsmaps 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
- Build locally (
npx fusion startor your normal flow) on Engine 7.1.0+. - Compare bundle sizes under
.fusion/dist/components/combinations/<output-type>.jsbefore and after your change. - Check the build output for the two warning types from section 4.
- For
import()changes, confirm the new*.chunk.jsfiles exist and load in the network tab only when the feature renders.