Home / Blog / JavaScript bundle splitting: when dynamic import actually pays off

JavaScript bundle splitting: when dynamic import actually pays off

Splitting the bundle is the universal advice, but it doesn't help in every situation. With real measurements, when it's actually worth it.

On one SPA project I opened webpack-bundle-analyzer and saw the main bundle at 2.8 MB. “Let’s code-split it”, and we added lazy loading on every route. The bundle went from 2.8 MB down to 480 KB plus 12 chunks. First load was 4 seconds faster. Nice story. Except on the next project the same approach helped nothing, and actually made things worse. Here’s what was different.

What code splitting actually does

Bundlers like webpack, Rollup, and Vite, when they hit import() syntax, split that module into a separate chunk. The first load ships only critical code, the rest is fetched when needed.

When does it pay off?

  1. Route-based splitting. In an SPA, each page is its own chunk. A user on /dashboard doesn’t download the /admin chunk. With lots of pages this is a real win.
  2. Large third-party libraries. Chart.js, PDF.js, a rich text editor, a syntax highlighter. Not every user touches these. Load them lazily.
  3. Modal or overlay content. Rarely opened, not needed on the main flow.
  4. A/B test variants. Bundle all variants together and the package bloats. Lazy load so only the user’s variant ships.

When it doesn’t pay off?

  1. Small bundles. If the whole package is under 100 KB, splitting just creates more requests. HTTP overhead eats the bundle win.
  2. Single-page apps, literally. On a landing page all the code is needed for the first paint anyway. No point splitting.
  3. Splitting on the critical path. If you split the homepage into three chunks and all three are needed for first paint, you’re slower than a single large file. Two extra HTTP round-trips.
  4. Heavy chunk interdependency. If chunk A needs B and B needs C, you get a waterfall. Slower than a single bundle.

What went wrong on the second project

It was a dashboard, eight pages. First-load bundle was 520 KB, 180 KB gzipped. We added splitting, main chunk down to 95 KB, each page chunk 40 to 80 KB.

The numbers looked good, but LCP got worse. Reason: every navigation now pulled a new chunk, and the user felt the delay. With one bundle, 180 KB downloaded once, every navigation was instant. After splitting, each transition added 50ms of lag.

The fix was prefetch. Using webpack’s magic comments, we fetched the likely next chunk during idle time:

const Dashboard = lazy(() => import(/* webpackPrefetch: true */ './Dashboard'));

With prefetch, chunks download in idle time after the main bundle lands. By the time the user navigates, the chunk is cached.

How to measure

Don’t just look at bundle size. The real metrics:

  • FCP (First Contentful Paint): when does the first content show up?
  • LCP (Largest Contentful Paint): when is the main content done?
  • TTI (Time to Interactive): when can the user interact?
  • Route transition time: how fast are navigation changes?

Splitting usually improves FCP and LCP, but can hurt route transitions. Work out the tradeoff for your site.

Keep an eye on chunk count

Very small chunks are bad. Webpack’s splitChunks has a minSize: 30000 (30 KB) threshold for a reason. A 5 KB chunk is not worth it, the HTTP overhead exceeds the payload.

Import packages like lodash and moment granularly. If you use one function, don’t drag in the whole library:

// bad
import _ from 'lodash';

// good
import debounce from 'lodash/debounce';

Better: date-fns instead of moment (tree-shakable), lodash-es instead of lodash (tree-shakable).

Vite vs Webpack

In Vite dev mode, splitting is natural because of ESM. In production it uses Rollup, which supports splitting but configures differently.

In webpack you have to define your chunk strategy:

splitChunks: {
  chunks: 'all',
  cacheGroups: {
    vendor: { test: /[\/]node_modules[\/]/, name: 'vendors' },
    common: { minChunks: 2, name: 'common' }
  }
}

Pulling out a vendor chunk is usually a win because it changes less than app code and caches well.

Takeaway

Code splitting is a tool, not a solution. Measure, apply it where it actually wins. On small projects don’t waste your time. On big projects splitting plus prefetch keeps both users and metrics happy.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch