Tags: web-dev concept

Lazy Loading

Date: 2026-08-16


Defer work until it’s needed. It’s nearly free for anything below the fold and actively harmful applied to the LCP element — which is the most common way a performance “fix” makes a page measurably slower.


What it is

Lazy loading is deferring the fetch or initialisation of a resource until it’s about to be needed, rather than doing it on page load.

The point isn’t bandwidth. It’s contention: everything loading at once competes for the connection and the main thread, so deferring the non-urgent makes the urgent arrive sooner.

Images

The platform handles this natively — no library required:

<!-- below the fold -->
<img src="/product-12.jpg" loading="lazy" decoding="async"
     width="400" height="400" alt="…">
 
<!-- the LCP image, above the fold -->
<img src="/hero.avif" fetchpriority="high"
     width="1200" height="600" alt="…">

Never lazy-load anything in the initial viewport, and especially not the LCP element. It delays discovery by hundreds of milliseconds and is a direct Largest Contentful Paint regression.

This happens constantly because lazy loading gets applied as a blanket rule across a template — every image gets the attribute, including the hero. If you check one thing after adding lazy loading, check that the LCP element didn’t get it.

Always set dimensions on lazy images, or the space isn’t reserved and you trade a loading problem for a Cumulative Layout Shift problem.

Iframes and embeds

<iframe src="https://maps.example.com/…" loading="lazy" …></iframe>

Embeds are among the heaviest things on a page — a video player or map iframe can weigh more than everything else combined. Better still is the facade pattern: render a static placeholder and load the real embed on click.

<!-- costs ~20KB until someone clicks -->
<button class="video-facade" data-video-id="abc123">
  <img src="/poster.avif" width="640" height="360" alt="Play: how we source wool">
</button>

This turns a megabyte of player into an image, and most visitors never click.

Components and routes

Deferring JavaScript is Code Splitting applied on demand:

// load when it enters the viewport
const observer = new IntersectionObserver(async ([entry]) => {
  if (!entry.isIntersecting) return;
  observer.disconnect();
  const { mount } = await import('./ReviewsWidget');
  mount(entry.target);
});
observer.observe(document.querySelector('#reviews'));

IntersectionObserver is the right tool — it runs off the main thread and doesn’t require a scroll listener. See Event Handling.

Third parties

The highest-value application, because they’re the heaviest and the least urgent:

  • Chat widgets — load on click of a lightweight button
  • Review platforms — load when the reviews section approaches the viewport
  • Video players — facade, as above
  • Anything below the fold — on intersection

A chat widget that loads on interaction instead of page load removes its entire cost for the 95%+ of visitors who never open it. See Third-Party Scripts.

Getting the threshold right

Loading exactly at the viewport edge means the user sees a gap while it fetches. Load slightly early:

new IntersectionObserver(cb, { rootMargin: '200px 0px' });

Native loading="lazy" uses its own margin, which is generous and connection-aware, so it usually needs no tuning.

Where it backfires

  • The LCP element. Covered above, and worth repeating because it’s the number one cause
  • No reserved space, producing layout shift as things arrive
  • Lazy-loading everything, so scrolling is a sequence of gaps and pop-ins. It reads as a broken page rather than a fast one
  • Content that needs to exist for crawling. Text and links deferred behind JavaScript may not be indexed — Rendering and SEO
  • A JavaScript lazy-loader for images, where the native attribute would do. The library costs more than it saves and delays images behind its own execution
  • Print and accessibility. Lazy content that never loads because the user navigates by keyboard or prints the page

The rule

Above the fold: eager, prioritised, in markup. Below the fold: lazy, with reserved space. Third parties: on interaction wherever possible.

Then confirm in the field rather than the lab — lazy loading’s benefit depends on connection speed and viewport, both of which a lab run fixes at one value. See Field vs Lab Data.