Topics
v6.0.0
On this page · sections

Dynamic Import & Code Chunks

import() creates async boundaries and separate chunk files loaded at runtime. Interviewers want route-level strategy and measurable perf, not just React.lazy syntax.

ConceptBehaviorBundler output
Static importHoisted, in initial graphParent chunk
Dynamic import()Returns Promise<Module>Separate async chunk
Magic commentswebpack chunk name, prefetchNamed chunks + resource hints
manualChunksVite/Rollup groupingVendor/feature chunks

import() Semantics

Definition

Static import ties modules into the initial graph. Dynamic import() returns a Promise resolving to the module namespace — the bundler cuts an async boundary and emits a separate chunk file.

document.getElementById('admin-btn').addEventListener('click', async () => { const { renderAdmin } = await import('./admin-panel.mjs'); renderAdmin(document.getElementById('root')); }); // Bundler emits: // - main-[hash].js (click handler + import() stub) // - admin-panel-[hash].js (loaded on first click)
Main chunk → import() executes → fetch lazy chunk → evaluate module → call exports

Literal string paths are required for static analysis. Variable paths may create a context module bundling many files. Top-level await blocks module evaluation — lazy features still use import() inside functions.

On network failure, the Promise rejects — handle with try/catch, show retry UI, pair Suspense with Error Boundaries.

Interview: Say async boundary + separate chunk file before mentioning React — shows bundler contract understanding.

Chunk Naming & Grouping

Definition

Webpack reads magic comments on the import() call:

const loadCharts = () => import( /* webpackChunkName: "charts" */ /* webpackPrefetch: true */ './features/charts.mjs' );
  • webpackChunkName — human-readable label for analysis
  • webpackPrefetch: true — idle-time fetch (rel=prefetch)
  • webpackPreload: true — high-priority fetch for current navigation
  • webpackMode: "eager" — includes in parent chunk but returns async module object — rarely wanted for route splitting

Vite/Rollup equivalent:

// vite.config.js // build: { // rollupOptions: { // output: { // manualChunks(id) { // if (id.includes('node_modules')) return 'vendor'; // if (id.includes('/features/admin/')) return 'admin'; // }, // }, // }, // },

Prod URLs include content hash — chunk name is for debugging and RUM, not stable URL.

Interview: Magic comments sit inside import() parentheses — bundlers parse them as leading comments.

Avoiding Chunk Waterfalls

Definition

A waterfall happens when chunk A loads, executes, then triggers import() for chunk B, then B triggers C — serial round-trips on the critical path.

// Waterfall — bad async function loadDashboardBad() { const layout = await import('./layout.mjs'); const charts = await import('./charts.mjs'); // waits for layout return layout.mount(charts); } // Parallel — good when independent async function loadDashboardGood() { const [layout, charts] = await Promise.all([ import('./layout.mjs'), import('./charts.mjs'), ]); return layout.mount(charts); }

Fixes:

  • Hoist shared deps into vendor chunk via splitChunks / manualChunks
  • Bundle feature folders together when always used together
  • Prefetch next route on hover or after idle
  • Promise.all for independent dynamic imports

React.lazy per child on the same route creates nested Suspense waterfalls — colocate one lazy boundary per route.

splitChunks.chunks: 'all' dedupes shared node_modules across async chunks.

Interview: More chunks does not always mean faster — each adds HTTP overhead; merge correlated code.

React.lazy & Suspense

Definition

React.lazy wraps a function returning import('./Component') — React suspends until the chunk resolves. Suspense shows fallback UI while pending.

'use client'; import { lazy, Suspense } from 'react'; const AdminPage = lazy(() => import('./AdminPage').then((mod) => ({ default: mod.AdminPage })) ); export function AppRouter({ route }: { route: string }) { if (route !== 'admin') return <div>Home</div>; return ( <Suspense fallback={<div>Loading admin…</div>}> <AdminPage /> </Suspense> ); }
Route match → React.lazy triggers import() → chunk ready? → no: Suspense fallback → yes: render component

Put one lazy boundary per route segment, not per leaf component. Pair with ErrorBoundary — chunk load failures reject the Promise. Named exports need .then(m => ({ default: m.Named })).

Suspense handles pending state; boundaries handle thrown errors — compose them.

Interview: Nested lazy per component creates serial Suspense waterfalls on the same route.

Prefetch vs Preload Strategy

Definition

preload (rel=preload, fetchpriority=high) — resources needed on the current navigation.

prefetch (rel=prefetch) — low-priority idle fetch for likely next navigation.

const prefetched = new Set(); function prefetchRoute(chunkLoader) { if (prefetched.has(chunkLoader)) return; prefetched.add(chunkLoader); chunkLoader(); } document.querySelectorAll('[data-prefetch-route]').forEach((el) => { el.addEventListener('mouseenter', () => { prefetchRoute(() => import(/* webpackPrefetch: true */ './checkout.mjs')); }); });
<link rel="modulepreload" href="/assets/vendor-abc123.js" />

Staff approach: intent-based prefetch with hit rate caps. Skip when navigator.connection.saveData is true. Service workers version cache keys on build hash to avoid stale chunk mismatch after deploy.

Next.js router.prefetch prefetches route JS and RSC payload for linked pages.

Interview: Preloading all lazy routes in index.html negates code splitting — preload only current-nav critical chunks.

Senior Interview Summary

import()

Async boundary · Promise<Module> · separate hashed chunk file loaded on demand.

Waterfalls

Serial A → B → C on critical path. Fix with Promise.all, vendor split, intent prefetch.

React integration

One lazy boundary per route · Suspense for pending · ErrorBoundary for rejected imports.

Prefetch vs preload

Preload = current nav critical. Prefetch = likely next route on idle. Measure hit rate.

Verification

Per-route chunk budgets in analyzer. Heavy routes must not live in main chunk.

Best mental model

Static import → pay bytes upfront import() → pay bytes + round-trip when needed Waterfall → multiply round-trips on critical path

Practical rules

Route splitting → one lazy boundary per route Independent chunks → Promise.all, not serial await Hover prefetch → cap concurrent prefetches Deploy → immutable hashed chunks + SW cache versioning

What to say in interviews

Explain what the bundler emits, how to fix a waterfall, where to place lazy boundaries, and when prefetch beats preload. Mention Chrome Performance initiator chain for debugging serial loads.

Last updated on