EdgeCases Logo
Nov 2025
SEO
Deep
7 min read

Image SEO: Lazy Loading and Modern Formats

loading=lazy on hero images delays LCP; WebP needs JPEG fallbacks—optimize without breaking SEO

seo
images
srcset
optimization
edge-case

Add loading="lazy" to all images for performance—watch your Largest Contentful Paint (LCP) tank when the hero image delays download until after layout. Use WebP for smaller file sizes—discover that Google indexes WebP but some crawlers and older browsers still need JPEG fallbacks. Image optimization promises faster pages and better SEO, but implementation details determine whether you improve Core Web Vitals or sabotage them.

The Lazy Loading LCP Paradox

Lazy loading defers image downloads until they're needed, reducing initial page weight. The browser's native loading="lazy" attribute delays requests for off-screen images until the user scrolls near them. This improves Time to Interactive (TTI) by reducing bandwidth competition during page load.

The problem: If you lazy load an above-the-fold image—especially the LCP element—you delay its download until after layout completes. The browser can't start fetching the image until it knows where it's positioned. This adds hundreds of milliseconds to LCP.

<!-- WRONG: Lazy loading hero image (LCP element) -->
<img src="/hero.jpg"
     alt="Hero image"
     loading="lazy" />

/* Timeline:
   1. HTML parsed, browser sees img tag
   2. Browser must complete layout to determine if image is in viewport
   3. Layout complete at ~500ms
   4. Image download starts at 500ms (delayed!)
   5. Image displays at 1200ms
   LCP: 1200ms ❌
*/

<!-- CORRECT: Eager loading hero image -->
<img src="/hero.jpg"
     alt="Hero image"
     loading="eager"
     fetchpriority="high" />

/* Timeline:
   1. HTML parsed, browser sees img tag
   2. Image download starts immediately (high priority)
   3. Image displays at 700ms
   LCP: 700ms ✅
*/

Google's 2025 Warning

In August 2025, Google's Martin Splitt warned: "If you are using lazy loading on an image that is immediately visible, that is most likely going to have an impact on your largest contentful paint."

The rule: Never lazy load above-the-fold images. If an image is visible without scrolling, use loading="eager" (or omit the attribute—eager is the default).

How Googlebot Handles Lazy Loading

Googlebot doesn't scroll. When rendering pages, it simulates a very tall viewport (~10,000px) to capture above-the-fold and some below-fold content in a single snapshot. Scroll events never fire.

Native Lazy Loading (Safe)

<img src="/product.jpg"
     alt="Product"
     loading="lazy" />

/* How it works:
   - Browser manages loading based on viewport distance
   - src attribute always contains real URL
   - Googlebot sees full URL in src attribute
   - Googlebot's tall viewport loads images within ~10,000px
   Result: Images indexed correctly ✅
*/

Custom Lazy Loading (Risky)

<!-- Old lazy loading pattern (pre-2019) -->
<img data-src="/product.jpg"
     src="placeholder.gif"
     class="lazy" />

<script>
  // JavaScript swaps data-src → src on scroll
  document.addEventListener('scroll', () => {
    document.querySelectorAll('.lazy').forEach(img => {
      img.src = img.dataset.src;
    });
  });
</script>

/* Problem for Googlebot:
   1. Googlebot doesn't scroll → scroll event never fires
   2. JavaScript never swaps data-src to src
   3. Googlebot sees: src="placeholder.gif"
   4. Real image (/product.jpg) never indexed
   Result: Images missing from Google Images ❌
*/

Google's recommendation (2025): Use native loading="lazy" or IntersectionObserver API. Avoid relying on scroll events or storing URLs in non-standard attributes (data-src, data-lazy-src).

IntersectionObserver (SEO-Safe Alternative)

<img src="/product.jpg"
     alt="Product"
     class="lazy" />

<script>
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        // Image is already in src, just trigger load
        observer.unobserve(img);
      }
    });
  });

  document.querySelectorAll('.lazy').forEach(img => {
    observer.observe(img);
  });
</script>

/* Why this works:
   - src attribute contains real URL (Googlebot sees it)
   - IntersectionObserver triggers when image enters viewport
   - Googlebot's tall viewport triggers intersection for most images
   - Images load for both users and crawlers
*/

Modern Image Formats: WebP and AVIF

JPEG and PNG have dominated the web for 25+ years. WebP (2010) and AVIF (2020) offer superior compression—50% smaller file sizes with equivalent visual quality. Smaller images improve LCP, a Core Web Vitals metric directly impacting SEO.

Browser Support (2025)

  • WebP: 96% browser support (all modern browsers + Safari 14+)
  • AVIF: 93% browser support (Chrome 85+, Firefox 93+, Safari 16+)
  • JPEG XL: Experimental (limited support, not recommended for production)

Google indexing: Google Search fully supports WebP and AVIF. Both formats are indexed and eligible for Google Images results.

The Fallback Problem

While 93-96% support sounds high, the remaining 4-7% of users (and some bots) can't decode WebP/AVIF. Without a fallback, they see broken images.

<!-- WRONG: No fallback -->
<img src="/hero.avif" alt="Hero" />

/* Result for Safari 15 users (no AVIF support):
   Broken image icon
   Poor user experience
*/

<!-- CORRECT: picture element with fallbacks -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero" />
</picture>

/* How it works:
   1. Browser checks: Can I decode AVIF? (type="image/avif")
   2. If yes: Use hero.avif (smallest file)
   3. If no: Check WebP support
   4. If yes: Use hero.webp
   5. If no: Fallback to hero.jpg (universal support)
*/

What Google Indexes

When you use the <picture> element, Google indexes the image in the <img> tag's src attribute—the fallback JPEG. Google can index AVIF and WebP from <source> tags, but prioritizes the img src for compatibility.

<picture>
  <source srcset="/product.avif" type="image/avif" />
  <source srcset="/product.webp" type="image/webp" />
  <img src="/product.jpg" alt="Wireless headphones" />
</picture>

/* Google indexes:
   - Primary: product.jpg (from img src)
   - Secondary: May also index product.avif and product.webp
   - Alt text: "Wireless headphones" (used for ranking)
   - File size served to users: product.avif (smallest)
*/

SEO benefit: Googlebot sees the JPEG fallback (guaranteed compatibility), but real users get AVIF (50% smaller, faster LCP).

Combining Lazy Loading + Modern Formats

<!-- Hero image: Eager loading + modern formats -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg"
       alt="Hero image"
       loading="eager"
       fetchpriority="high" />
</picture>

/* Optimizations:
   - Modern formats reduce file size by ~50%
   - Eager loading starts download immediately
   - fetchpriority="high" gives image top priority
   - Fallback ensures compatibility
   Result: Fast LCP ✅
*/

<!-- Below-fold image: Lazy loading + modern formats -->
<picture>
  <source srcset="/product.avif" type="image/avif" />
  <source srcset="/product.webp" type="image/webp" />
  <img src="/product.jpg"
       alt="Product image"
       loading="lazy" />
</picture>

/* Optimizations:
   - Lazy loading defers download until needed
   - Modern formats reduce bandwidth when loaded
   - Fallback ensures older browsers work
   Result: Reduced initial page weight ✅
*/

Responsive Images and srcset

The srcset attribute provides multiple image sizes for different viewport widths. The browser downloads the most appropriate size, reducing bandwidth waste.

<img src="/hero-800w.jpg"
     srcset="/hero-400w.jpg 400w,
             /hero-800w.jpg 800w,
             /hero-1200w.jpg 1200w"
     sizes="(max-width: 600px) 400px,
            (max-width: 1000px) 800px,
            1200px"
     alt="Hero" />

/* How sizes works:
   - Viewport ≤ 600px: Browser downloads hero-400w.jpg
   - Viewport 601-1000px: Browser downloads hero-800w.jpg
   - Viewport > 1000px: Browser downloads hero-1200w.jpg
*/

Combining picture + srcset

<picture>
  <source
    type="image/avif"
    srcset="/hero-400w.avif 400w,
            /hero-800w.avif 800w,
            /hero-1200w.avif 1200w"
    sizes="(max-width: 600px) 400px,
           (max-width: 1000px) 800px,
           1200px" />

  <source
    type="image/webp"
    srcset="/hero-400w.webp 400w,
            /hero-800w.webp 800w,
            /hero-1200w.webp 1200w"
    sizes="(max-width: 600px) 400px,
           (max-width: 1000px) 800px,
           1200px" />

  <img src="/hero-800w.jpg"
       srcset="/hero-400w.jpg 400w,
               /hero-800w.jpg 800w,
               /hero-1200w.jpg 1200w"
       sizes="(max-width: 600px) 400px,
              (max-width: 1000px) 800px,
              1200px"
       alt="Hero" />
</picture>

/* Result:
   - Mobile user on Safari: hero-400w.webp
   - Desktop user on Chrome: hero-1200w.avif
   - Old Android browser: hero-800w.jpg
   - Googlebot: Sees hero-800w.jpg (fallback src)
*/

Image CDNs and Automatic Optimization

Image CDNs (Cloudinary, Imgix, Cloudflare Images) automatically serve optimal formats based on browser support, eliminating manual <picture> management.

<!-- Cloudinary automatic format delivery -->
<img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/hero.jpg"
     alt="Hero"
     loading="eager" />

/* f_auto: Automatically deliver AVIF to Chrome, WebP to Safari, JPEG to old browsers
   q_auto: Automatically optimize quality based on content
   Result: Browser gets optimal format without manual picture element
*/

<!-- Imgix automatic format delivery -->
<img src="https://demo.imgix.net/hero.jpg?auto=format,compress"
     alt="Hero"
     loading="eager" />

/* auto=format: Serve AVIF/WebP based on Accept header
   auto=compress: Apply optimal compression
*/

SEO consideration: Google sees the CDN URL. As long as the URL is stable (doesn't change based on user agent), Google indexes it correctly. The CDN serves JPEG to Googlebot (via Accept header detection), modern formats to users.

Core Web Vitals Impact

LCP (Largest Contentful Paint)

  • Good LCP: < 2.5 seconds
  • Common LCP element: Hero image, featured product image
  • Optimization: Use AVIF/WebP, fetchpriority="high", loading="eager", preload critical images
<!-- Preload critical LCP image -->
<link rel="preload"
      as="image"
      href="/hero.avif"
      type="image/avif" />

<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <img src="/hero.jpg" alt="Hero" fetchpriority="high" />
</picture>

/* Result: LCP image starts downloading before HTML parsing completes */

Production Checklist

  1. Identify LCP element: Use Chrome DevTools Performance tab. Never lazy load the LCP image.
  2. Use fetchpriority="high": Apply to hero images, above-the-fold featured images.
  3. Lazy load below-fold: Add loading="lazy" to images below the fold. Saves ~30-50% initial bandwidth.
  4. Implement modern formats: Use <picture> element with AVIF → WebP → JPEG fallback.
  5. Avoid custom lazy loading: Never use data-src or scroll-triggered lazy loading. Use native loading="lazy".
  6. Test with Googlebot: Use Google Search Console URL Inspection to verify images appear in rendered HTML.
  7. Monitor LCP: Use PageSpeed Insights and Search Console Core Web Vitals report. Target LCP < 2.5s.
  8. Provide alt text: All images need descriptive alt text for accessibility and Google Images ranking.
  9. Use responsive images: Implement srcset for different viewport sizes to reduce mobile bandwidth.
  10. Consider image CDN: Automate format selection, compression, and responsive sizing.

The Modern Image Stack

<!-- Above-fold hero image -->
<link rel="preload" as="image" href="/hero.avif" type="image/avif" />

<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg"
       alt="Descriptive alt text"
       width="1200"
       height="600"
       fetchpriority="high"
       loading="eager" />
</picture>

<!-- Below-fold product image -->
<picture>
  <source
    type="image/avif"
    srcset="/product-400w.avif 400w,
            /product-800w.avif 800w"
    sizes="(max-width: 600px) 400px, 800px" />

  <source
    type="image/webp"
    srcset="/product-400w.webp 400w,
            /product-800w.webp 800w"
    sizes="(max-width: 600px) 400px, 800px" />

  <img src="/product-800w.jpg"
       srcset="/product-400w.jpg 400w,
               /product-800w.jpg 800w"
       sizes="(max-width: 600px) 400px, 800px"
       alt="Product name"
       width="800"
       height="600"
       loading="lazy" />
</picture>

/* Results:
   - LCP optimized (preload + eager + modern formats)
   - Below-fold bandwidth saved (lazy loading)
   - All browsers supported (fallbacks)
   - Googlebot sees all images (src attributes)
   - Core Web Vitals improved (smaller files, faster LCP)
*/

Modern image optimization balances performance, compatibility, and SEO. The key: understand which images are critical (eager loading, preload) and which aren't (lazy loading). Use modern formats with fallbacks, and let native browser features handle the complexity.

Advertisement

Related Insights

Explore related edge cases and patterns

SEO
Deep
Canonical URLs: The Duplicate Content Paradox
7 min
SEO
Expert
JavaScript Hydration and SEO: The Googlebot Race Condition
7 min
SEO
Surface
Structured Data Validation: When Valid Schema Breaks Rich Results
5 min

Advertisement