Lazy loading images: what to lazy-load and what never to

Lazy loading images cuts bytes without hurting speed. The rule is simple: never lazy-load anything above the fold, always lazy-load the rest. Here is how to do it with images, iframes and backgrounds, and how to avoid the hero image trap that ruins LCP.

By , founder of Porteur · Updated 14 September 2026 · Markdown

What to lazy-load and what never to

Lazy-load every image that starts below the fold. Never lazy-load anything visible at first paint.

  • Do not lazy-load your hero image on "/" or "/pricing".
  • Do lazy-load thumbnails below the first viewport on "/blog".
  • Do lazy-load product gallery images that sit under the fold on "/product/widget".
  • Do not lazy-load the logo, nav icons or any image in the header.
  • Do lazy-load user avatars that appear down the page on "/community".

This rule protects Largest Contentful Paint. LCP counts the largest image, video poster, background image or text in the first viewport. A lazy hero delays it.

A fixed page looks like this: the hero image on "/" has no lazy flag and uses fetchpriority=high. Every image below the fold has loading=lazy.

Native loading: how browsers handle it

Use the native attribute on images and iframes. Browsers support it across the board as of 2026. They start loading a little before the viewport, not exactly at the edge.

<img src="/images/guide-card.jpg" alt="Guide card" loading="lazy" width="640" height="360">

Do not overthink the threshold. The browser has more context than you. It knows the connection, device and scroll speed. Let it decide when to fetch.

Always set width and height or aspect-ratio. That prevents layout shifts when the image arrives. CLS should stay under 0.1 for a good score.

Add decoding=async on images that do not block the first paint. The browser can decode them off the critical path.

<img src="/images/review-avatars.jpg" alt="Customer avatars" loading="lazy" decoding="async" width="320" height="320">

Avoid the LCP trap: the hero image

A lazy hero image ruins LCP. The browser will not fetch it early, so the largest element renders late. Keep LCP under 2.5 s for a good field score.

  1. Remove lazy on the hero

    On "/" replace loading="lazy" with nothing. Keep width and height.

  2. Give it priority

    Add fetchpriority=high. This hints the browser to fetch it sooner.

  3. Use responsive sources

    Serve WebP or AVIF with srcset and sizes so phones do not download desktop sizes.

  4. Preload when it is CSS

    If the hero is a background-image, preload the file. Then keep it out of lazy logic.

  5. Render the HTML server-side

    Name the hero image in the HTML. Do not insert it later with JavaScript.

<!-- Good hero on / -->
<link rel="preload" as="image" href="/images/hero-1280.webp" imagesrcset="/images/hero-640.webp 640w, /images/hero-1280.webp 1280w" imagesizes="100vw">
<img src="/images/hero-1280.webp"
     srcset="/images/hero-640.webp 640w, /images/hero-1280.webp 1280w"
     sizes="100vw"
     alt="Plan your roadmap"
     width="1280" height="720"
     fetchpriority="high">

A fixed page shows LCP as that hero image, fetched with high priority, not lazy. Other images on "/" below the fold keep loading=lazy and decoding=async.

Iframes: embeds, videos and maps

Lazy-load iframes that appear below the fold. The native attribute works here too. Embeds are heavy, so this saves a lot of work on first paint.

<!-- YouTube embed below the fold on /guides/getting-started -->
<iframe
  loading="lazy"
  width="560" height="315"
  src="https://www.youtube.com/embed/VIDEO_ID"
  title="Product demo"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen>
</iframe>

Above the fold, replace the iframe with a static facade and load the iframe on interaction. This reduces third-party script cost and improves INP and TBT in lab runs.

<!-- Facade pattern for a hero video on /pricing -->
<button id="play-demo" aria-label="Play demo">
  <img src="/images/demo-poster.webp" width="800" height="450" alt="Watch the demo">
  <span>Play</span>
</button>
<script type="module">
  document.getElementById('play-demo').addEventListener('click', () => {
    const iframe = document.createElement('iframe');
    iframe.width = 800; iframe.height = 450;
    iframe.src = 'https://www.youtube.com/embed/VIDEO_ID?autoplay=1';
    iframe.title = 'Product demo';
    iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
    iframe.referrerPolicy = 'strict-origin-when-cross-origin';
    document.getElementById('play-demo').replaceWith(iframe);
  });
</script>

A fixed page uses loading=lazy on a below-the-fold map iframe on "/contact". The hero has a static image until you click Play. INP stays under 200 ms for clicks that do not mount the iframe.

Background images and JavaScript-inserted content

CSS backgrounds do not support loading=lazy. The browser must discover them from CSS, which often happens late. If the image is below the fold, load it with IntersectionObserver.

<!-- Mark elements that need a background only when near viewport -->
<section class="feature" data-bg="/images/charts.webp"></section>

<style>
.feature { min-height: 480px; background-color: #f6f7f9; }
.feature.bg-loaded { background-image: url('/images/charts.webp'); background-size: cover; background-position: center; }
</style>

<script type="module">
  const io = new IntersectionObserver(entries => {
    for (const e of entries) if (e.isIntersecting) {
      const url = e.target.dataset.bg;
      if (url) {
        const img = new Image();
        img.decoding = 'async';
        img.src = url;
        img.onload = () => e.target.classList.add('bg-loaded');
        e.target.removeAttribute('data-bg');
      }
      io.unobserve(e.target);
    }
  }, { rootMargin: '200px 0px' });

  document.querySelectorAll('[data-bg]').forEach(el => io.observe(el));
</script>

For content inserted by JavaScript, the same pattern applies. Use IntersectionObserver to load and mount when near the viewport. Avoid scroll handlers for this job.

A fixed page moves a below-the-fold hero variant that used background-image to a proper img with preload if it was above the fold. This protects LCP and keeps CLS low with width and height in HTML.

How Google sees lazy-loaded content

As of 2026, Googlebot uses a smartphone crawler. It can index lazy-loaded images and content that appear without a click or gesture. IntersectionObserver is supported by Googlebot rendering.

  • Content that loads as it nears the viewport is fine.
  • Content that needs a true user action is not indexed until it exists in the DOM.
  • A scroll handler that only loads on scroll may not run in rendering. Use IntersectionObserver instead.
  • Server-rendered HTML wins: include the key content in the document.

Check your pages with PageSpeed Insights and the URL Inspection tool. If an image is missing in the rendered HTML, Google may not index it. Do not gate it on scroll events or clicks.

Prevent layout shifts when images load

Lazy-loading often exposes weak layout. Images arrive late and push content down. Reserve space so the layout does not jump. Aim for CLS under 0.1.

  • Set width and height on every img. Modern browsers use them to set the aspect ratio.
  • If you cannot, set aspect-ratio in CSS on the image box.
  • Give ad and embed slots a min-height so banners and iframes do not push content.
  • Avoid loading bars that insert above existing content. Use overlays for notices.
  • Do not animate layout properties. Use transform for motion.
<!-- Thumbnails on /blog, space reserved -->
<img
  src="/images/thumb-article.webp"
  alt="How we shipped faster"
  loading="lazy" decoding="async"
  width="384" height="216">

A fixed page keeps the grid stable when images arrive. The scroll feels calm, and the field CLS assessment passes on mobile and desktop in Search Console.

Measure the effect: field and lab data

Judge success with field data first. The Core Web Vitals assessment uses the 75th percentile of real Chrome users over 28 days. A URL passes when LCP, CLS and INP are all good.

  • Good LCP is up to 2.5 s, poor above 4 s.
  • Good CLS is up to 0.1, poor above 0.25.
  • Good INP is up to 200 ms, poor above 500 ms.

Use PageSpeed Insights. The top section shows field data if there is enough traffic. That is what Google Search uses for the ranking signal, with content and relevance well ahead of it.

Use the lower section for lab checks. Lighthouse simulates a mid-range phone on throttled 4G. Scores vary between runs. Total Blocking Time weighs 30 percent there and is the proxy for INP in lab tests.

Expect the two to disagree. Field data wins. If a page has too few visits, the reports fall back to origin-level or show nothing. Keep testing as traffic grows.

A simple lazy loading example that works

Here is a full pattern for images below the fold on "/blog". It uses srcset, sizes, lazy loading and decoding=async, and reserves space with width and height.

<article class="post-card">
  <a href="/blog/how-we-ship-faster">
    <img
      src="/images/thumb-ship-640.webp"
      srcset="/images/thumb-ship-320.webp 320w, /images/thumb-ship-640.webp 640w, /images/thumb-ship-960.webp 960w"
      sizes="(max-width: 640px) 100vw, 384px"
      alt="How we ship faster"
      width="384" height="216"
      loading="lazy"
      decoding="async">
    <h2>How we ship faster</h2>
    <p>What we cut to release weekly.</p>
  </a>
</article>

Use the same pattern in product grids. Keep the primary product image above the fold eager, with fetchpriority=high, and lazy-load the rest of the gallery below the fold on the PDP.

Common mistakes and how to fix them

  • Lazy-loading the hero image. Fix: remove loading=lazy, add fetchpriority=high, and preload when it is a CSS background.
  • Relying on scroll events to load content. Fix: use IntersectionObserver so Google can see it and it loads near the viewport.
  • No width and height on images. Fix: set them or use aspect-ratio to stop layout shifts.
  • Lazy-loading icons and logos in the header. Fix: keep critical UI eager and small. Sprite or inline tiny SVGs.
  • Huge background images discovered late. Fix: preload above-the-fold backgrounds, or switch to <img> where you can.
  • JavaScript inserts the image after hydration. Fix: server-render the <img> in HTML so the browser can fetch it sooner.
  • Too many third-party embeds above the fold. Fix: use a facade, lazy-load the iframe, and load scripts on interaction.
  • Overusing lazy on short pages. Fix: if your page fits one screen, lazy adds no value and can delay paint.

After fixes, your homepage LCP should be your hero image, fetched early, and your long pages should save requests until the user scrolls. Scrolling feels smooth and inputs stay snappy.

Questions

Sources

Check my site, free

Paste your homepage URL to see which images to lazy-load, which to keep eager and where LCP is delayed, in about thirty seconds, free, with three findings you can ship.

  • Free check, no card
  • Read-only, your own accounts
  • Readable by your agent

Read next