Pandora E-Commerce · Architecture · Webpack-Native

One Bundler. Everything.

① Webpack compiles everything
② Output: static files
③ Only files deploy
① Build Machine / CI
Developer laptop · CI server · runs before deployment
📦Webpack 5
One bundler handles everything — Next.js app build, HMR, SSR, and Module Federation in a single process. No second runner needed.
next dev · next build · single process
⏱ HMR ~2–5s · Cold start ~10–30s
// next.config.js — one file, no extras const { ModuleFederationPlugin } = require('webpack').container; const BUILD_ID = process.env.BUILD_ID; // injected in CI module.exports = { webpack(config) { config.plugins.push( new ModuleFederationPlugin({ name: 'home_remote', filename: `remoteEntry.${BUILD_ID}.js`, // → .next/static/chunks/remoteEntry.2292.js shared: { react: { singleton: true } } }) ); return config; } };
✓ One process — output lands in .next/ Versioned filename per deploy → immutable CDN caching safe
⛔ Webpack stays on this machine Never installed on the production server
output
② Compiled Output
Static artifacts — the only things that move to production
Single Webpack process — everything in .next/
~2.4 MB
📁.next/static/chunks/
Everything Webpack emits — app chunks, SSR, and MF manifest
📄 page-xxx.js app chunks
📄 react-abc123.js shared deps
📄 __federation_expose_PLPPage.js MF expose
📄 remoteEntry.2292.js MF manifest ✓
Versioned filename — new BUILD_ID each deploy = new URL = immutable caching safe. No public/ folder needed.
Just compiled
One folder. One process. All CDN benefits.
.next/ handles everything. Webpack stays behind.
Deploy Gate
📁Files
✓ Deploys
vs
Tools
📦
✗ Blocked
LIVE
③ Production Server
Cloud · CDN · No bundlers installed
Node.js Runtime
JavaScript engine on your server
Makes JavaScript run on the server — same language as the browser, but in the cloud. Handles every visitor's HTTP request.
Receives browser HTTP requests
Runs SSR from .next/
Serves remoteEntry.{BUILD_ID}.js from .next/
Does NOT run Webpack
No bundlers installed here
.next/static/chunks/
App chunks · shared deps · remoteEntry.2292.js — all in one folder
Cache-Control: immutable · CDN · Brotli · HTTP/2 ✓
Not installed on server
📦Webpack — build tool only
Pandora E-Commerce · Architecture · Webpack-Native

Pure Webpack vs Turbopack Hybrid — Know the Trade-off

scroll
Same Runtime Outcome — Different Developer Experience
React is shared correctly in both approaches. The choice is about build speed, setup complexity, and team friction.
Pure Webpack
This approach · one bundler, everything native
HMR speed
2–5s
per change · full rebuild
Cold start
10–30s
initial compilation
Processes
1
single next dev
Config files
1
next.config.js only
HMR latency
~3s
Cold start
~20s
Single process — no coordination between two tools
Native ModuleFederationPlugin — built into Webpack 5
One config file — next.config.js, nothing else
Battle-tested — Webpack MF has years of production use
__webpack_share_scopes__ works natively on both sides
Slow HMR — every change triggers full JS rebuild
Slow cold start — large apps can take 20–30s to boot
No Rust-speed benefits — Turbopack is 10–100× faster
Turbopack + Webpack
Current POC approach · hybrid, two parallel processes
HMR speed
~50ms
instant · Rust-based
Cold start
~3s
Turbopack fast init
Processes
2
Turbopack + Webpack
Config files
2+
next.config + federation.js
HMR latency
~50ms
Cold start
~3s
Instant HMR — Turbopack recompiles in ~50ms
Fast cold start — ~3s vs 20s with pure Webpack
Next.js default — officially recommended by Vercel
Future-ready — Turbopack adding native MF support
Two parallel processes — coordination required
Two config files — more surface area for bugs
MF workaround — Turbopack has no native MF today
🔗
Runtime behaviour is identical — React is shared correctly in both approaches
Both use Module Federation runtime negotiation. The host registers React via lib:()=>React. The remote's get():i.e(86) fallback is never triggered. 86.js is absent from the Network tab in both setups. The only difference is developer experience — build speed and configuration complexity.
Verified in POC
Pandora E-Commerce · Architecture · Webpack-Native

React Shared Scope — Native Webpack MF

scroll
HAPPY PATH · YOUR SETUP FALLBACK · STANDALONE ONLY Browser loads host chunks React in memory · Webpack 5 build __webpack_require__.S · share scope object created MF INIT ModuleFederationPlugin init SYNC shared.react → __webpack_share_scopes__ Webpack runtime registers React before any fetch RemoteLoader mounts → loadRemote() remoteEntry.js fetch begins · t=39ms Webpack remote · declares shared candidates React in shared scope? YES ★ your setup NO scope empty init() finds React ✓ Host's React in __webpack_share_scopes__ get():i.e(86) → skipped entirely PLPPage renders r(67) = host's React object Same in-memory reference · zero cost ✓ Zero extra download · 86.js absent get():i.e(86) called Scope empty — fetching chunk Async CDN request initiated 86.js downloads from CDN ~449 kB · extra network round-trip ⚠ Two React instances possible ⚠ Fallback active · standalone mode Webpack 5 · Native Module Federation __webpack_share_scopes__ · version negotiation · singleton enforcement Webpack runtime initialises __webpack_share_scopes__ SYNC — same guarantee as createInstance() in @module-federation/enhanced REAL CONFIG · next.config.js // one file — no extra runner new ModuleFederationPlugin({ shared: { react: { singleton: true, eager: false }} native — no @mf/enhanced needed REAL SOURCE · remoteEntry.js // 24.7 kB · always fetched react: [{ get: () => i.e(86) .then(() => () => i(86)), eager: false, // lazy — if needed singleton: true identical regardless of bundler BROWSER DEVTOOLS · Network Tab · VERIFIED 86.js (React chunk) ──────────── ABSENT remoteEntry.js ── ✓ 24.7 kB loaded WHEN DOES THIS HAPPEN? Remote served standalone (no host MF init) Or: webpack share scope not initialised before load
① BUILD · CI Webpack build BUILD_ID=2292 injected at CI publicPath: /bundle/${BUILD_ID}/ next build · single process Versioned output /bundle/2292/remoteEntry.js ✓ /bundle/2292/86.js ✓ (React fallback) Every chunk shares same BUILD_ID New deploy = new URL = old cache never reused LIVE CDN · Deployed & served globally Cache-Control: public, max-age=31536000, immutable Brotli · HTTP/2 · Edge PoPs · URL is unique — safe to cache forever remoteEntry.js · 86.js · __federation_expose_PLPPage.js — all immutable ② API · RUNTIME DISCOVERY GET /get-asset-url/plp-remote.json Cache-Control: no-store · always fresh · ~200 bytes // Response JSON { "assetUrl": "https://cdn.pandora.net/bundle/2292/remoteEntry.js", "baseUrl": "https://cdn.pandora.net/bundle/2292/" } Pattern from: uk.pandora.net/get-asset-url/plp-client-remote.json Proven in production · same pattern applied here assetUrl → ③ HOST · INIT Host app · createInstance() with Promise remote // lib/mf-init.ts async function resolveRemoteEntry(): Promise<string> { const res = await fetch('/get-asset-url/plp-remote.json'); const { assetUrl } = await res.json(); return assetUrl; // versioned URL from API } createInstance({ remotes: [{ name: 'plp_remote', entry: resolveRemoteEntry() // Promise<string> accepted }] }); fetch → ④ BROWSER · LOAD Browser loads versioned remoteEntry.js https://cdn.pandora.net/bundle/2292/remoteEntry.js Resolved from API · URL is unique per deploy · CDN serves from edge Cache-Control: public, max-age=31536000, immutable ✓ All static asset benefits — unlocked ✓ CDN edge ✓ Brotli ✓ HTTP/2 ✓ immutable ✓ deploy-safe ✓ instant purge New deploy → API returns new assetUrl → old cached version never reused ✗ FIXED URL (without this pattern) /remote-components/remoteEntry.js → same filename every deploy Cache-Control: max-age=60 (short TTL forced) · immutable NOT safe CDN ✓ · Brotli ✓ · HTTP/2 ✓ · immutable ✗ · stale risk on deploy ✗ Visitors may get old remoteEntry.js for up to 60s after deploy (or until CDN purge completes — requires extra infra) ✓ API-RESOLVED URL (this pattern) /bundle/2292/remoteEntry.js → new URL every deploy Cache-Control: public, max-age=31536000, immutable ✓ CDN ✓ · Brotli ✓ · HTTP/2 ✓ · immutable ✓ · zero stale risk ✓ API response is tiny (~200 bytes) · no-store · always returns latest BUILD_ID Proven in production: uk.pandora.net/get-asset-url/plp-client-remote.json