renderpx
Theme: auto

Code Splitting & Lazy Loading

Split the bundle so the user doesn’t download code they might never need. Lazy-load routes and heavy components with lazy + Suspense (or framework equivalents) and optionally preload on hover or when likely needed.

The problem I keep seeing

A single JS bundle pulls in every route and every heavy component (charts, editors, admin UI). The initial load is slow, especially on slow networks. You want to load only what’s needed for the first screen and fetch other chunks when the user navigates or when a heavy component is about to be shown.

Naive approach

Static imports for everything. The bundler puts it all in one (or few) chunks; the user pays the cost up front even for code that runs only on a specific route or after a click.

tsx
Loading...

First improvement

Use React.lazy for components that are heavy or conditionally rendered. Wrap them in Suspense with a fallback so the user sees a placeholder while the chunk loads. Each lazy(() => import(...)) becomes a separate chunk that loads on first render.

tsx
Loading...

Remaining issues

  • Where to split: Routes are the obvious boundary (one chunk per route). Also split modals, tabs, and below-the-fold content that may never be seen.
  • Preloading: To reduce delay when the user clicks, preload the chunk on link hover or when a route is likely (e.g. after login, preload dashboard).
  • SSR: lazy doesn’t run on the server by default; use framework support (e.g. Next.js dynamic) for SSR-safe lazy loading.

Production pattern

In Next.js, use dynamic for client-only or heavy components; routes are already split. In a React SPA, lazy-load route components and wrap with Suspense. Use a skeleton or small spinner as fallback. Optionally preload: call import('./Page') on onMouseEnter of the link so the chunk is ready when the user clicks.

tsx
Loading...

When I use this

  • Routes: One chunk per route (or per section) so the initial bundle is small.
  • Heavy components: Charts, rich editors, modals that aren’t shown immediately.
  • Skip when: The component is tiny or always visible; the extra request and complexity aren’t worth it.

Gotchas

  • Default export: lazy(() => import('./X')) expects the module to have a default export; use lazy(() => import('./X').then(m => ({ default: m.Named }))) for named exports.
  • Suspense boundary: The boundary must be above the lazy component; put it at the route or layout level so the fallback shows while the chunk loads.

Memoization → · All patterns