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.
Table of Contents
- 1. Why route splitting alone is often disappointing
- 2. Component level splitting: targeted instead of blanket
- 3. Vendor chunking: bundling shared dependencies strategically
- 4. Chunk granularity: avoiding too many and too few chunks
- 5. Preload and prefetch: combining splitting with foresight
- 6. Conditional loading: features on demand instead of by route
- 7. Request waterfalls: the invisible risk of too much splitting
- 8. Measuring impact: bundle analyzer and real load times
- 9. Splitting strategies compared
- 10. Summary
- 11. FAQ
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.