ESC

Type to search the knowledge base.

Code Splitting Strategies

Route, component, and vendor splits: dynamic import(), magic comments, and avoiding over-splitting waterfalls.

intermediate3 min read
  • performance
  • code-splitting
  • bundling
  • javascript
  • lcp

Code splitting ships less JavaScript before first interaction by cutting the bundle into async chunks loaded on demand. Done well, it improves LCP/TBT/INP. Done poorly, it creates request waterfalls and blank screens waiting on nested import().

Docs: web.dev code splitting, dynamic import — MDN.

Core mechanism

// static — in main graph
import { heavy } from './heavy';

// dynamic — separate chunk
const { heavy } = await import('./heavy');

Bundlers (webpack, Vite, Parcel) emit extra files and rewrite imports.

Strategy 1 — Route-based

// React example
const Settings = lazy(() => import('./pages/Settings'));

<Suspense fallback={<Spinner />}>
  <Settings />
</Suspense>

Each major route owns its page chunk. Highest ROI for multi-page apps.

Strategy 2 — Component-based

Modals, charts, editors, emoji pickers — load when opened:

button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor');
  openEditor();
});

Prefetch on hover/focus if the interaction is likely:

link.addEventListener('pointerenter', () => {
  import('./editor');
}, { once: true });

Strategy 3 — Vendor splitting

Large stable libraries in a separate chunk for long-term caching. With fingerprinting, granular caching still works without manual vendor.js — measure before forcing.

Waterfalls kill wins

// BAD: sequential discovery
const a = await import('./a');
const b = await import('./b'); // starts only after a finishes
// BETTER: parallel
const [{ a }, { b }] = await Promise.all([import('./a'), import('./b')]);

SSR/framework routers should preload route chunks when a link is visible (many frameworks do this automatically).

What not to split

  • Tiny utilities (overhead > savings)
  • Above-the-fold critical UI that always shows
  • The LCP path’s essential JS (if any — prefer less JS overall)

Measuring

  • Bundle analyzer for chunk sizes
  • Network waterfall for sequential chunks
  • Field LCP/INP after deploy

Pair with JS bundle budget and tree shaking.

Interview out-loud

“I split by route and heavy components via dynamic import, preload likely next chunks, and avoid sequential import waterfalls. Splitting is worthless if critical path still downloads everything up front.”

How this shows up in interviews

Be ready to define the metric or technique in one sentence, name one measurement approach (DevTools panel, web-vitals, or headers), and cite a concrete fix you would try first. Walk through a before/after: what the waterfall or flame chart showed, what you changed, and which percentile moved. Mention a tradeoff (complexity, caching correctness, or third-party business constraints) so the answer doesn’t sound like a blog checklist.

Production guardrails

Ship behind a flag when the change is risky, watch field p75 for the affected template for at least a few days, and keep a rollback path. Pair lab verification (throttled Performance/Network) with RUM so you don’t celebrate a Lighthouse-only win. Document the owner of any ongoing budget or third-party exception.

Further depth

Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.

Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.

Further reading

Related guides