Skip to content
Innovialabs

Code Splitting and Lazy Loading in JavaScript Apps

Practice3 min

In short

Code splitting breaks your bundle into chunks loaded only when needed: routes separately, heavy components via dynamic import, with prefetching to load likely next steps ahead of time.

What it is and why it matters

Code splitting means dividing your JavaScript bundle into several files (chunks). Instead of one large script, users first get only the code for the current page and the rest on demand. That is lazy loading.

The result: less JavaScript at startup, faster parsing and execution, better responsiveness. The cost is extra network requests later, so split thoughtfully.

The foundation: dynamic import

Everything is built on import() — a function that loads a module asynchronously and returns a Promise. Bundlers (webpack, Vite, Turbopack, Rollup) detect it and automatically move the module into its own chunk.

button.addEventListener("click", async () => {
  const { exportToPdf } = await import("./export-pdf.js");
  exportToPdf(data);
});

The PDF export code stays out of the main bundle and loads only on click.

Route-based splitting

This is the most valuable level: a visitor on the home page should not download the account dashboard.

  • Next.js does it automatically: every page in app/ or pages/ is its own entry point. Nothing to configure.
  • React with a router (for example, React Router) — wrap pages in React.lazy:
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./pages/Dashboard"));

export function App() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <Dashboard />
    </Suspense>
  );
}

Component-based splitting

Within a page, split out what is heavy and not needed immediately:

  • modals and forms opened on click;
  • charts, maps, rich text editors, video players;
  • sections below the fold.

Next.js provides next/dynamic for this:

import dynamic from "next/dynamic";

const Chart = dynamic(() => import("@/components/chart"), {
  loading: () => <p>Loading chart…</p>,
  ssr: false,
});

ssr: false is for components that only work in the browser (they use window). Note that in the App Router it can only be used inside client components — check the docs for your version.

Do not split tiny things. A button or icon in its own chunk adds a request with no benefit.

Prefetching: removing the delay

Lazy loading adds a pause at the moment code is needed. Prefetching loads it in advance, in the background.

StrategyWhen to loadGood for
Link enters the viewportLink is visible on screenPage navigation (what next/link does by default in production)
On hover / focusUser hovers the triggerHeavy modals, menus
During idle timeBrowser is free (requestIdleCallback)The likely next step in a flow
No prefetchOnly on actionRare features, admin tools

Prefetch on hover:

const loadEditor = () => import("./editor");
button.addEventListener("mouseenter", loadEditor, { once: true });
button.addEventListener("click", async () => (await loadEditor()).open());

Calling import() again for the same module does not re-download it — the cached module is reused.

How to measure the effect

  1. Before changes, record a baseline: initial JS size (bundle analyzer), Lighthouse, LCP and INP.
  2. Check the Network panel in DevTools: new chunks should appear only on the intended action.
  3. The Coverage panel shows how much loaded JS actually runs at startup.
  4. Compare field metrics after release — a lab run may not reflect real devices.

Common mistakes

  • Too many tiny chunks — network overhead eats the gains.
  • Lazy-loading what is visible above the fold — users see a placeholder instead of content.
  • No fallback and no handling of chunk load errors (for example, old chunks may disappear after a new deploy).
  • A heavy shared library imported statically in a common module still ends up in the main bundle.

FAQ

Next.js splits by page on its own. You only need to manually split heavy components inside pages with next/dynamic or import().

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.