/ Image Optimisation Fixes / Lazy Load

How to Fix Image Lazy Load

Lazy-loading delays image downloads until they're near the viewport — saves bandwidth, speeds up initial render, improves LCP. Native browser support has been universal since 2022; you don't need JS libraries any more. The catch: the LCP (hero) image should NOT be lazy-loaded — it should be eager with fetchpriority=high. This guide covers the loading attribute, decoding hints, the LCP exception, and replacing legacy JS lazy-load libraries. For related fixes, see the Image Optimisation Fixes index.

1. Audit current lazy-load coverage

Step 1
Run the Image Optimisation audit
Findings categorise as:
  • Below-fold images without loading=lazy — wasted bandwidth
  • LCP image with loading=lazy — delayed first paint
  • All images marked lazy (including hero) — major LCP impact
  • Old data-src lazy-load pattern still in use — switch to native

2. Add loading=lazy to below-fold images

<!-- All images that aren't visible on initial page load -->
<img src="/gallery-photo.jpg"
     loading="lazy"
     decoding="async"
     width="800" height="600"
     alt="...">

Browser delays download until the image gets close to the viewport. Bandwidth saved for images the user never scrolls to. Initial render finishes faster.

Default to lazy across templates

// React example: Image component defaults to lazy
function ResponsiveImage({ src, alt, width, height, priority = false }) {
  return <img
    src={src}
    alt={alt}
    width={width}
    height={height}
    loading={priority ? 'eager' : 'lazy'}
    decoding="async"
    fetchpriority={priority ? 'high' : 'auto'}
  />;
}

// Usage
<ResponsiveImage src="/hero.jpg" alt="..." width={1200} height={675} priority />  // LCP
<ResponsiveImage src="/below-fold.jpg" alt="..." width={800} height={600} />  // lazy default

3. The LCP exception

⚠️ The LCP image should NEVER have loading=lazy. Mark it eager AND high priority.
<!-- Hero / LCP image -->
<img src="/hero.jpg"
     loading="eager"
     fetchpriority="high"
     decoding="async"
     width="1200" height="675"
     alt="...">

Preload for even faster LCP

<link rel="preload"
      as="image"
      href="/hero.jpg"
      imagesrcset="/hero-400.jpg 400w, /hero-1200.jpg 1200w, /hero-2400.jpg 2400w"
      imagesizes="(max-width: 800px) 100vw, 1200px"
      fetchpriority="high">

Goes in <head> before any CSS. Browser starts downloading the hero image as soon as it parses the head, before discovering the img tag in body.

4. Identify the LCP image

Step 1
DevTools Performance recording
DevTools → Performance → record page load → stop. Timings panel shows LCP marker pointing at the largest content element. Usually a hero image, sometimes a large block of text.
Step 2
Web Vitals Chrome extension
Install Web Vitals extension. Real-time LCP, CLS, INP displayed per page. Click LCP to highlight which element is being measured.

5. Add decoding=async to all images

<img src="..." decoding="async" alt="...">

Tells browser to decode the image off the main thread. Once downloaded, decoding doesn't block other JavaScript or paint operations. Universally safe to add to every image (lazy, eager, all of them).

6. Remove old JS lazy-load libraries

Native lazy-loading has been supported since 2020 in all major browsers. Libraries like lazysizes are typically obsolete.

Migration: data-src pattern → native

<!-- OLD: lazysizes pattern -->
<img data-src="/photo.jpg" class="lazyload" alt="...">
<script src="lazysizes.min.js"></script>

<!-- NEW: native -->
<img src="/photo.jpg" loading="lazy" alt="...">
<!-- No JS needed -->

Savings

7. Background images (CSS) lazy-load

Native loading=lazy applies only to <img> and <iframe> elements. CSS background images don't have a native lazy-load.

Workaround 1: convert to img where possible

<!-- Was background-image, now img -->
<img src="/section-bg.jpg" alt="" loading="lazy" class="full-bleed">

<style>
.full-bleed {
  width: 100%;
  height: 400px;
  object-fit: cover;
}
</style>

Workaround 2: IntersectionObserver for CSS

<div class="lazy-bg" data-bg="/section-bg.jpg"></div>

<script>
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.style.backgroundImage = `url(${entry.target.dataset.bg})`;
      observer.unobserve(entry.target);
    }
  });
});
document.querySelectorAll('.lazy-bg').forEach(el => observer.observe(el));
</script>

8. Iframes also benefit from lazy

<iframe src="https://www.youtube.com/embed/..."
        loading="lazy"
        width="560" height="315"
        title="...">
</iframe>

Same loading attribute. Browser delays iframe content load until near viewport. Massive savings on pages with embedded YouTube, Vimeo, Twitter widgets, maps.

9. Verify improvements

Step 1
Network tab: deferred downloads
DevTools → Network → reload. Below-fold images should NOT appear in the initial download list. Scroll the page; they appear as you approach them.
Step 2
Lighthouse LCP improvement
Re-run Lighthouse. Expected:
  • "Defer offscreen images" audit: pass
  • "Largest Contentful Paint image was not lazily loaded" audit: pass
  • LCP: 200-800ms faster if hero was previously lazy-loaded
  • Initial page weight: smaller
💡 The single biggest mistake: applying loading=lazy uniformly to all images including the hero. That makes LCP worse, not better. Audit your LCP image specifically — it gets eager + fetchpriority=high. Everything else gets lazy.

🖼️ Re-run the Image Optimisation audit

Verify lazy-load findings are cleared and measure LCP improvement.

Run Image Audit →
Related Guides: Image Optimisation Fixes  ·  Fix Image Size  ·  Core Web Vitals Fixes  ·  Image Optimisation Guide
💬 Got a problem?