Code Splitting Strategies Beyond the Basics
AI generated
JS
() =>
JavaScript · Code Splitting · Bundling · Web Performance
Code Splitting Strategies Beyond the Basics
Combining route, component and vendor splits correctly

Route splitting is the standard entry point, but the noticeable improvement often sits one level deeper. Component level splits, deliberate vendor chunking and preload hints together decide whether code splitting merely looks good in the bundle report or actually shortens load time for users.

18 min read Code Splitting · Vendor Chunking · Preload · Prefetch Vite · Webpack · Rollup 2026

1. Why route splitting alone is often disappointing

Code splitting at the route level is the usual first step: every page of an application gets its own bundle that is only loaded on navigation. That noticeably reduces the initial bundle, but quickly hits limits once individual routes themselves grow large, for example a dashboard with charts, a rich text editor or a map component. Route based code splitting treats an entire page as an atomic unit, even though in reality only a fraction of the code is actually needed on first render.

The problem shows up especially in applications with few but functionally extensive routes. A single product page in a shop can easily contain several hundred kilobytes of JavaScript for reviews, recommendations, zoom functionality and variant selection, even though only the title, price and image need to be visible on first load. Pure route based code splitting misses exactly this opportunity, because it does not consider the internal structure of a page.

The solution is to understand code splitting as a layered strategy: route splitting as the coarse first layer, component level splitting for individual expensive areas within a route, and vendor chunking for shared dependencies across routes. Only the interplay of these three layers turns code splitting into a technique that actually lowers perceived load time, instead of just increasing the number of bundle files.

2. Component level splitting: targeted instead of blanket

Component level code splitting only loads individual, clearly scoped components once they are actually needed, independent of the route they appear in. Typical candidates are modals that only appear after a user interaction, rich text editors that are only needed in edit mode, or complex chart libraries that are only visible for certain user roles. Dynamic import with the import function instead of a static import is the technical foundation for this pattern.

The difference to route splitting: component level splitting reacts to user behavior, not to navigation. A modal opened via a button only needs to load its bundle once the click actually happens, not already on first render of the page. This delay is mostly invisible to users, since a short interaction time passes anyway between click and visible modal, in which the small, targeted bundle can be loaded.

A common mistake in component level splitting is splitting too many small components separately, without considering the resulting number of network requests. Every additional dynamic import creates its own request, and on HTTP/1.1 or under poor connection quality, the per request overhead adds up noticeably. Component level code splitting pays off especially for components that are themselves at least several dozen kilobytes in size and are rarely needed immediately.


// Component level splitting: only load the rich text editor
// when the user actually opens the edit mode
import { lazy, Suspense, useState } from "react";

const RichTextEditor = lazy(() => import("./RichTextEditor.jsx"));

function ProductDescription({ text }) {
  const [isEditing, setIsEditing] = useState(false);

  if (!isEditing) {
    return (
      <div>
        <p>{text}</p>
        <button onClick={() => setIsEditing(true)}>Edit</button>
      </div>
    );
  }

  return (
    <Suspense fallback={<div>Loading editor...</div>}>
      <RichTextEditor initialValue={text} />
    </Suspense>
  );
}

3. Vendor chunking: bundling shared dependencies strategically

Vendor chunking groups third party dependencies used by multiple parts of the application into their own, separately cacheable chunks. Without deliberate vendor chunking, a large library like a date processing library or a state management framework ends up in every chunk that imports it, leading to massive code duplication across multiple bundle files. Modern bundlers like Vite and Rollup offer the manualChunks configuration for this, which precisely controls which modules move into which chunk.

A proven vendor chunking strategy separates by change frequency: libraries that rarely change, such as React or a UI library, go into a dedicated, long term cacheable chunk, while the application code itself, which changes with every deployment, stays in separate chunks. This pattern maximizes the cache hit rate, because users can load the vendor chunk from the browser cache across many deployments without downloading it again.

A risk in vendor chunking is over granularity: if too many small vendor chunks appear, the number of requests grows without the cache benefit offsetting the additional overhead. Practice shows that two to four vendor chunks, grouped by thematic proximity, usually offer the best balance between cache efficiency and request count, compared to a single giant vendor bundle or dozens of tiny chunks per individual library.


// vite.config.js — deliberate vendor chunking by update frequency
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            if (id.includes("react") || id.includes("react-dom")) {
              return "vendor-react"; // rarely changes, cache long term
            }
            if (id.includes("chart.js") || id.includes("d3")) {
              return "vendor-charts"; // only needed on dashboard routes
            }
            return "vendor-misc"; // remaining smaller dependencies
          }
        },
      },
    },
  },
};

4. Chunk granularity: avoiding too many and too few chunks

The right chunk granularity in code splitting is a balancing act between two opposing risks. Too few, too large chunks mean users keep loading code they do not need for the current view at all, undermining the original purpose of code splitting. Too many, too small chunks, on the other hand, create request overhead that, while lower under HTTP/2 than under HTTP/1.1, never fully disappears, since every chunk still has to be parsed and executed.

A practical rule of thumb: chunks under roughly ten kilobytes compressed rarely provide a net benefit, because the overhead for connection, parsing and module registration outweighs the benefit of the smaller download size. Bundlers like Webpack offer splitChunks minSize and maxSize as direct configuration options to enforce this limit instead of leaving it to chance based on module structure.

It is also important that chunk granularity does not apply statically to the entire application. Critical paths like the checkout process in a shop benefit from larger, less fragmented chunks to minimize latency, while rarely used administrative areas benefit from more aggressive code splitting, since load time there is less critical, but reducing the initial bundle matters more.

5. Preload and prefetch: combining splitting with foresight

Code splitting without preload and prefetch merely shifts load time instead of eliminating it: the user loads less on first page visit, but has to wait for a new chunk on every navigation instead. Preload hints with rel="preload" for chunks that are highly likely to be needed next, and prefetch hints with rel="prefetch" for probable future navigation targets, balance this effect by starting the download in the background before the user actually clicks.

Modern frameworks and routers often integrate these hints automatically: a link element appearing in the visible viewport can automatically request the associated route chunk as a prefetch via Intersection Observer, so that the bundle is already in cache once the actual click happens. This technique combines the benefits of code splitting, a smaller initial bundle, with the benefits of a monolithic bundle, no perceived wait time on navigation.

Caution is warranted with aggressive prefetching on connections with limited bandwidth. The Network Information API allows adjusting behavior based on navigator.connection.effectiveType and disabling prefetching on slow connections, to save data volume and not slow down the resource actually being requested through competing prefetch requests.


// Prefetch a route chunk when its link enters the viewport,
// but skip it on slow connections to avoid wasting bandwidth
function prefetchOnVisible(linkElement, chunkImporter) {
  const connection = navigator.connection;
  if (connection && ["slow-2g", "2g"].includes(connection.effectiveType)) {
    return; // skip prefetching on slow connections
  }

  const observer = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        chunkImporter(); // triggers the dynamic import, browser caches it
        observer.disconnect();
      }
    }
  });

  observer.observe(linkElement);
}

prefetchOnVisible(
  document.querySelector("a[href='/checkout']"),
  () => import("./routes/Checkout.jsx")
);

6. Conditional loading: features on demand instead of by route

Conditional loading goes one step beyond pure component level splitting and loads code based on feature flags, user roles or device characteristics, independent of route or interaction. One example: a complex accessibility extension loaded only for users with enabled screen reader settings, or an admin toolbar included in the bundle only for users with the appropriate permission at all.

This form of code splitting requires the condition for loading to be known early enough in the application lifecycle, usually right after authentication or on initial feature flag retrieval. A common pattern is a central feature loader that decides, based on a configuration, which dynamic imports are executed at all, instead of scattering this decision across the entire codebase.

The advantage of conditional loading over static code splitting: bundle size adapts exactly to actual usage instead of prescribing an identical structure for all users. A user without admin rights never loads even a single byte of the admin toolbar, while pure route splitting cannot make this distinction without additional logic.

7. Request waterfalls: the invisible risk of too much splitting

An often overlooked risk of aggressive code splitting is request waterfalls: chunk A imports chunk B, which in turn imports chunk C, and each of these imports triggers a sequential network request instead of loading in parallel. With deeply nested dynamic imports, actual load time can increase despite smaller individual files, because the chain of sequential requests multiplies network latency instead of reducing it.

Modern bundlers partially detect such waterfalls automatically and merge deeply nested chunks, but the most reliable countermeasure remains a deliberate analysis of the import graph. Tools like the Rollup Bundle Visualizer or Webpack Bundle Analyzer graphically display the dependency structure and show exactly where deep nesting occurs that is prone to request waterfalls.

A practical trick against waterfalls is explicit preloading of nested dependencies with the modulepreload directive, which allows the browser to load all known dependencies of a chunk in parallel instead of sequentially. This technique turns a potential chain of sequential requests into a single parallel loading process, without having to change the actual code splitting structure.

8. Measuring impact: bundle analyzer and real load times

Code splitting without measurement is speculation. A bundle analyzer shows the size of each individual chunk and its dependencies, but the decisive question is how this structure affects real load times, not just the file size in the build report. Metrics like Time to Interactive and Largest Contentful Paint from real field data show whether a splitting change actually reaches users or merely improves the build report visually.

A proven approach is comparing a recording in the Performance Panel before and after a code splitting change, combined with synthetic measurement across multiple network and device profiles. A splitting strategy that looks good on a fast developer machine with a fiber connection can actually be slower than a less fragmented alternative on an average mobile device with a 4G connection, due to additional request overhead.

9. Splitting strategies compared

The table below classifies the presented strategies by their typical use case and their effect on initial load time versus navigation speed.

Strategy Use case Effect Risk
Route splitting Multi page applications Coarse reduction of initial bundle Large routes remain unaddressed
Component splitting Modals, editors, heavy widgets Targeted reduction per feature Too many chunks if overdone
Vendor chunking Shared third party dependencies High cache hit rate Over granularity increases requests
Conditional loading Feature flags, roles, devices Exact match to actual usage Requires more complex loading logic
Preload/Prefetch Likely navigation targets No perceived wait time Wasted bandwidth without throttling awareness

None of these strategies work optimally in isolation. Only the combination of route splitting as the base structure, component splitting for heavy individual pieces, vendor chunking for cache efficiency and preload for navigation speed turns code splitting into a holistic strategy instead of an isolated single measure.

Mironsoft

Bundle optimization and code splitting architecture

Getting bundle size truly under control?

We analyze your bundle structure, develop a layered code splitting strategy and measure the impact against real load times, not just the build report.

Bundle analysis

Uncover import graphs and chunk structure with a bundle analyzer

Splitting strategy

Implement component, vendor and conditional splitting combined

Preload tuning

Adapt prefetch and preload hints to actual usage patterns

10. Summary

Code splitting unfolds its full potential only as a layered strategy: route splitting forms the coarse structure, component level splitting addresses individual heavy areas within a route, and vendor chunking maximizes the cache hit rate across deployments. Preload and prefetch hints balance out the delay created by splitting, by loading likely next chunks in the background ahead of time.

Two traps remain to avoid: too many small chunks create request overhead and potential waterfalls, too few large chunks undermine the original purpose of code splitting. Anyone who measures the impact of every change with a bundle analyzer and real field data, instead of relying on the build report alone, finds the right balance for their own application and its actual users.

Code Splitting Beyond the Basics — The Essentials at a Glance

Think in layers

Combine route, component and vendor splitting instead of relying on a single layer.

Control granularity

Chunks under ten kilobytes rarely give a net benefit, actively configure minSize and maxSize.

Preload with care

Prefetch for likely navigation, but factor in network quality via the Network Information API.

Measurement over opinion

Bundle analyzer for structure, real field data for the actual impact on load times.

11. FAQ: Code Splitting Strategies

1Why is route splitting often not enough?
Route splitting treats a page as a whole, even though only part of it is needed on first render.
2What is component level splitting?
Targeted loading of individual components only when actually needed, independent of route.
3How does vendor chunking work?
Groups third party dependencies into their own long term cacheable chunks, usually by change frequency.
4How small can a chunk be?
Under ten kilobytes compressed rarely gives a net benefit due to connection and parsing overhead.
5Preload vs. prefetch?
Preload for the current view with high priority, prefetch for probable future navigation with low priority.
6What is conditional loading?
Code is loaded based on feature flags, roles or device characteristics, independent of route or interaction.
7What is a request waterfall?
Sequential instead of parallel loading of nested chunks lengthens load time despite smaller files.
8How do I avoid waterfalls?
Use modulepreload for known dependencies, review the import graph with a bundle visualizer.
9How do I measure the impact?
Bundle analyzer for structure, real field data on Time to Interactive and LCP for the actual impact.
10Enable prefetching on all connections?
No, disable on slow connections via the Network Information API to save data volume.