Lazy Loading and Code Splitting Done Right in 2026
Every website loads code the user does not need at this moment: the checkout script while they read the homepage, or the chat widget they never click. This ballast delays the start. According to the Web Almanac, a median of 44 percent (Web Almanac, 2024) of delivered JavaScript is unused during load -- that equals 206 KB (Web Almanac, 2024) of dead code on mobile alone. Lazy loading and code splitting flip the principle: only what is needed for the first interactive rendering is loaded, everything else follows on demand. The effect is directly business-relevant -- a mobile load time just 0.1 seconds faster increased retail conversions by 8.4 percent (Google/Deloitte, 2020). This article shows how to implement image and component lazy loading, route-based splitting and dynamic imports cleanly, without harming your Core Web Vitals.
Why the Initial Payload Decides Success
The size of the initially loaded JavaScript determines how quickly a page becomes interactive. JavaScript is more expensive than any other resource: it must be downloaded, decompressed, parsed, compiled and executed -- and each of these steps blocks the main thread. During this blocking, the page responds to no click and no tap. Today, median home pages load 664 KB (Web Almanac, 2025) of JavaScript, which uncompressed often amounts to two to three times that.
User patience is limited. Google field data shows the probability of a bounce rises by 32 percent (Google/SOASTA, 2017) when load time grows from one to three seconds, and by 90 percent (Google/SOASTA, 2017) when it climbs from one to five seconds. 53 percent (Google, 2018) of mobile page visits are abandoned if loading takes longer than three seconds. Since around 57 percent (Statista, 2024) of worldwide e-commerce revenue now happens on mobile devices, a heavy payload hits exactly where revenue is made.
Lazy loading and code splitting address the root cause. Instead of preloading the entire application, delivery is divided into a critical path and deferred units. This not only reduces bytes but, above all, shortens main thread processing time -- and with it Total Blocking Time, Interaction to Next Paint and Largest Contentful Paint at once. A structured frontend optimization therefore almost always begins with the question: what really needs to load immediately?
Lazy Loading vs. Code Splitting -- the Difference
Lighten the Load with Native Image Lazy Loading
Images are often the largest item in page weight. Native lazy loading defers loading images outside the visible area until the user scrolls near them. In most cases a single attribute suffices: loading="lazy" on the img or iframe element. Support is broad -- all modern browsers from Chrome 77, Firefox 75 and Safari 15.4 handle the technique, together over 95 percent (web.dev, 2024) of global browser usage. It is thus one of the most resource-friendly optimizations there is, because it requires no additional JavaScript.
Never Lazy-Load the LCP Image
The rule is clear: images in the first visible area are loaded eagerly, ideally with fetchpriority="high" for the LCP image. All images below the fold receive loading="lazy". In addition, width and height should be set so the browser reserves the space and no layout shift (Cumulative Layout Shift) occurs. Anyone already working on image optimization with modern formats combines both: smaller files and deferred loading complement each other. Encouragingly, adoption is growing -- 34 percent (Web Almanac, 2024) of websites used native lazy loading on mobile in 2024, up from 27 percent a year earlier.
Route-Based Code Splitting as the Foundation
The most effective form of code splitting is route-based splitting. Each page or route gets its own bundle that is only loaded when the user actually visits that route. Someone landing on the homepage does not also load the code for the product detail page, cart and account management. Modern frameworks like SvelteKit, Next.js and Nuxt generate this division automatically along the route structure -- the initial payload often drops considerably without developers having to manage individual imports by hand.
For server-side rendered shops based on Shopware CE, splitting must be configured more deliberately, because the storefront bundle has historically grown monolithic. Here it helps to break plugin functionality into separate entry points and move rarely used areas -- such as the wishlist or product comparison -- into their own chunks. Our article on Shopware performance explores the details. What matters is keeping the critical path (layout, navigation, first rendering) small.
| Approach | What loads | Suitable for | Typical effect |
|---|---|---|---|
| Monolithic bundle | Entire app code at once | Very small sites | High blocking time |
| Route-based splitting | Only current route code | Multi-page apps, shops | Much smaller initial JS |
| Component splitting | Component on demand | Heavy widgets, modals | Relieves individual routes |
| Vendor splitting | Libraries in own chunk | Stable dependencies | Better cache hit rate |
An often underestimated side effect: route-based chunks improve caching. If only the code of a single page changes, the browser only needs to reload that chunk -- the rest stays cached. The same applies to vendor chunks with stable libraries: they rarely change and remain valid across deployments. This caching advantage is especially strong in combination with CDN and edge caching, which brings the chunks additionally closer to the user.
Use Dynamic Imports Deliberately
While route-based splitting mostly happens automatically, dynamic imports are the tool for fine-grained control. The import() syntax returns a promise and instructs the bundler to place the imported code in a separate chunk that is only loaded at runtime. This lets you couple heavy functionality precisely to the moment the user needs it -- such as a chart renderer that loads only when the dashboard opens, or a map widget that appears only when the location is clicked.
// Static import: always ends up in the initial bundle
// import { Chart } from './chart';
// Dynamic import: own chunk, loaded only on demand
button.addEventListener('click', async () => {
const { Chart } = await import('./chart');
new Chart(container).render(data);
});The most common use case is interactions not every user triggers: a chat widget that loads only on click, a review form initialized only when scrolled into view, or a heavy editor that appears only on certain pages. Since a median of 22 third-party scripts (Web Almanac, 2024) are loaded per page, the savings potential is large precisely here. How to deliberately slim down external scripts is covered in our article on third-party scripts and performance.
Practical Tip: Plan for Loading State and Error Case
Tree Shaking: Automatically Remove Unused Code
Code splitting distributes code, tree shaking removes it. During build optimization, the bundler analyzes the import chain and excludes all exports used nowhere. This is so effective because a large share of delivered bytes is dead anyway: Coverage data from the Web Almanac shows the median website delivers 45 percent (Web Almanac, 2024) unused JavaScript. Notable is the origin -- 82.7 percent (Web Almanac, 2024) of wasted bytes come from own code, not third parties. So cleaning up is largely within your own control.
For tree shaking to take effect, dependencies must be in ES module format (import/export). CommonJS modules with require cannot be reliably removed because their exports are only determined at runtime. When choosing npm packages, it is therefore worth checking the package.json: a module or exports field signals ES modules. A second pitfall is the sideEffects marking. Modules that intentionally trigger side effects on import -- such as injecting CSS -- must not be removed; an incorrect marking is, in our experience (project experience), one of the most common reasons tree shaking saves less than expected.
Prefer ES modules
Choose libraries with native import/export. Only then can the bundler safely remove unused parts. CommonJS prevents effective tree shaking.
Import granularly
Import individual functions instead of entire packages. A blanket default import often pulls the complete library into the bundle even if only one function is used.
Analyze the bundle
A treemap analysis shows which modules take up the most space. Duplicates, oversized imports and unnecessary polyfills become immediately visible.
Prefetching: Lazy Loading Without Noticeable Wait
The valid objection to aggressive lazy loading is: does the reload not delay the interaction? This is exactly where prefetching comes in. The browser loads chunks the user will likely need shortly in the background -- at low priority, without disturbing the current page. When a user hovers over a link, the target page's code can already be loaded, so the switch feels instant. The directive and the Intersection Observer API enable this predictive loading.
The gradation of resource hints matters. preload applies to resources required on the current page (such as a critical font). prefetch applies to likely next steps. Too much prefetching, however, can waste bandwidth and burden weaker connections -- especially on mobile. The art lies in preloading only what is needed with high probability. Many frameworks solve this elegantly by automatically prefetching links in the viewport and only over Wi-Fi.
Lazy loading without prefetching merely shifts the problem to the moment of interaction. Only the combination of on-demand loading and predictive preloading produces an experience that both starts fast and feels fast.
Common Mistakes and How to Avoid Them
Lazy loading and code splitting are powerful but error-prone when applied schematically. The most serious mistake is lazy-loading the LCP image or other critical content -- this worsens exactly the metric one wanted to improve. Equally problematic is over-splitting: dividing code into dozens of tiny chunks, where request overhead and latency add up and overall performance suffers. Sensible chunk sizes, in our experience (project experience), lie in the range of a few dozen kilobytes.
- Load the LCP image and above-the-fold content eagerly, not with loading=lazy
- Set width and height on images to avoid layout shifts (CLS)
- Do not over-split chunks -- weigh request overhead against savings
- Secure dynamically loaded areas with a skeleton placeholder and error handling
- Prefetch likely next routes deliberately instead of preloading everything
- Prefer ES modules and mark sideEffects correctly in package.json
Another classic is the lack of measurement. Anyone who splits without measuring before and after optimizes blind. We recommend observing initial JavaScript size, Total Blocking Time and Largest Contentful Paint both in the lab and in the field. A sound performance analysis reveals which chunks are truly relevant, where unused code hides and whether third-party scripts undo your own optimizations. Without this data foundation, any splitting strategy remains speculation.
Measure, Safeguard and Stay Lean for Good
A lean payload is not a permanent state but a discipline. Every new feature, every dependency and every third-party script can grow the initial JavaScript size again. That is why size limits belong in the CI/CD pipeline: build tools like Vite and webpack can annotate or abort the build as soon as a chunk exceeds a defined limit. This makes performance regressions visible before they reach production.
We combine synthetic measurements under controlled conditions with Real User Monitoring from actual sessions. Synthetic tests detect regressions quickly and reproducibly; field data shows the actual experience across different devices and connections. Particularly insightful is segmentation by device type -- mid-range smartphones experience JavaScript load, in our experience (project experience), considerably more severely than desktop devices. This continuous observation is part of our performance services.
The Core in One Sentence
The effort pays off multiple times. Smaller initial bundles improve Largest Contentful Paint and Total Blocking Time, which directly benefits the Core Web Vitals and strengthens organic visibility. Faster interactivity reduces bounce rate and lifts conversion. And an established performance budget preserves the result across releases. This turns a one-time optimization into a sustainably fast website -- the lever a specialized frontend optimization consistently operates.