Zum Inhalt springen
Core Web Vitals specialists
All Articles Frontend-Performance

Lazy Loading and Code Splitting Done Right in 2026

14 min read
Lazy LoadingCode-SplittingJavaScriptCore Web Vitals

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.

Loading Timeline: Monolithic Bundle vs. On-Demand ChunksWithout SplittingWith Lazy Loading and Code Splittingapp.bundle.js -- 880 KB (blocks main thread)Parse and compileExecuteFirst Paintlate interactivityEverything loads upfront -- even code for pagesthe user never visits.core.js -- 140 KBParseExec.Paintearly interactivityroute: product.js 28 KBroute: checkout.js 34 KBchat-widget.js (click)image (viewport)On demandOnly the critical path starts immediately.Chunks follow on navigation or interaction.Initial JavaScript (compressed)before880 KBafter140 KB44%JS unused at median34%sites using lazy loadingplus 8.4%conversions per 0.1 s2.5 sLCP targetAt the median, 44 percent of delivered JavaScript is unused during load (Web Almanac 2024) --a 0.1 s faster mobile load lifted retail conversions by 8.4 percent (Google/Deloitte).

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

Code splitting is a build technique: the bundler divides the code into multiple files (chunks). Lazy loading is a loading strategy: a resource is requested only when it is needed. The two work together -- splitting creates the chunks, lazy loading decides when they load.

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

If the largest visible image (the LCP candidate) is given loading="lazy", the browser defers its fetch until after layout -- the LCP measurably worsens. The 75th percentile LCP for pages with lazy loading was 3,546 milliseconds versus 2,922 milliseconds without (web.dev, 2024). This very mistake is widespread: 9.5 percent of mobile pages wrongly lazy-load their LCP image (Web Almanac, 2024).

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.

ApproachWhat loadsSuitable forTypical effect
Monolithic bundleEntire app code at onceVery small sitesHigh blocking time
Route-based splittingOnly current route codeMulti-page apps, shopsMuch smaller initial JS
Component splittingComponent on demandHeavy widgets, modalsRelieves individual routes
Vendor splittingLibraries in own chunkStable dependenciesBetter 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.

lazy-component.js
// 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

A dynamically loaded chunk takes a moment. Show a lean placeholder or skeleton during that time instead of letting the UI freeze. Also plan for the error case -- if the load fails (for example on an unstable network), a clear message should appear instead of an empty area.

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.

Project experience from 50+ performance projects

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

Lazy loading and code splitting only work if the critical path stays small, the LCP image loads eagerly, prefetching masks the wait and size budgets safeguard the result permanently. Deferring alone is not enough -- the goal is to actually reduce the initial payload.

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.

This article is based on data from: Web Almanac 2024 and 2025 (HTTP Archive), web.dev (Browser-Level Image Lazy Loading), Google/Deloitte (Milliseconds Make Millions), Google/SOASTA Research, Statista (Mobile Commerce). All cited statistics were verified at the time of publication.