Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.
ImagesHigh

Lazy load offscreen images

Images below the visible viewport use loading="lazy" to defer download until the user scrolls near them, reducing initial page load time.

Utilities
Quick take
Typical fix time 10 min
  • Add `loading="lazy"` to all `<img>` elements below the fold
  • Never lazy-load the LCP image (hero, first product image)—use `fetchpriority="high"` instead
  • Always pair `loading="lazy"` with `width` and `height` attributes to prevent CLS
  • Native `loading="lazy"` has universal support in all modern browsers—no JavaScript polyfill needed
  • If fold position is unclear from the snippet, do not invent a lazy-loading defect
Why it matters: Lazy loading eliminates unnecessary image downloads on initial page load. A page with 20 images below the fold may transfer several megabytes of data that users who don't scroll will never see. Deferring these downloads reduces Time to Interactive, improves LCP for above-fold content, and saves bandwidth—particularly impactful for users on metered mobile connections.

Rule Details

Native browser lazy loading (loading="lazy") defers image downloads until the user scrolls near them, reducing initial page weight with a single attribute.

Code Example

<!-- ❌ Bad: All images load immediately regardless of position -->
<img src="product-1.jpg" alt="Product 1" width="400" height="300">
<img src="product-2.jpg" alt="Product 2" width="400" height="300">
<!-- ... 20 more product images -->
 
<!-- ✅ Good: Above-fold hero loads eagerly, rest defer -->
<!-- Hero image — above fold, must load immediately -->
<img
  src="hero.jpg"
  alt="Hero image"
  width="1200"
  height="600"
  fetchpriority="high"
>
 
<!-- Below-fold product images — defer until near viewport -->
<img src="product-1.jpg" alt="Product 1" width="400" height="300" loading="lazy">
<img src="product-2.jpg" alt="Product 2" width="400" height="300" loading="lazy">

Why It Matters

Lazy loading eliminates unnecessary image downloads on initial page load. A page with 20 images below the fold may transfer several megabytes of data that users who don't scroll will never see. Deferring these downloads reduces Time to Interactive, improves LCP for above-fold content, and saves bandwidth—particularly impactful for users on metered mobile connections.

Critical: Do Not Lazy-Load the LCP Image

The Largest Contentful Paint (LCP) element is usually the first large image visible on the page. Lazy-loading it delays the most important metric.

<!-- ❌ Bad: Lazy-loading the LCP image hurts LCP score -->
<img
  src="hero.jpg"
  alt="Hero image"
  loading="lazy"
  width="1200"
  height="600"
>
 
<!-- ✅ Good: LCP image loads immediately with high priority -->
<img
  src="hero.jpg"
  alt="Hero image"
  width="1200"
  height="600"
  fetchpriority="high"
  decoding="async"
>

Pair with Explicit Dimensions

Without width and height, lazy-loaded images cause layout shift (CLS) when they eventually load.

<!-- ❌ Bad: No dimensions → layout shifts when image loads into view -->
<img src="article-photo.jpg" alt="Article photo" loading="lazy">
 
<!-- ✅ Good: Dimensions reserved, no shift when loading triggers -->
<img
  src="article-photo.jpg"
  alt="Article photo"
  width="800"
  height="450"
  loading="lazy"
>

picture Element with Lazy Loading

Add loading="lazy" to the <img> element inside <picture>, not the <source> elements.

<picture>
  <source
    type="image/avif"
    srcset="photo-400.avif 400w, photo-800.avif 800w"
    sizes="(max-width: 600px) 100vw, 50vw"
  >
  <source
    type="image/webp"
    srcset="photo-400.webp 400w, photo-800.webp 800w"
    sizes="(max-width: 600px) 100vw, 50vw"
  >
  <img
    src="photo-800.jpg"
    srcset="photo-400.jpg 400w, photo-800.jpg 800w"
    sizes="(max-width: 600px) 100vw, 50vw"
    alt="Photo description"
    width="800"
    height="600"
    loading="lazy"       <!-- Goes on the <img>, not <source> -->
    decoding="async"
  >
</picture>

Framework Examples

interface ImageProps {
  src: string
  alt: string
  width: number
  height: number
  priority?: boolean
}
 
function OptimizedImage({ src, alt, width, height, priority = false }: ImageProps) {
  return (
    <img
      src={src}
      alt={alt}
      width={width}
      height={height}
      // priority images load eagerly with high fetchpriority
      loading={priority ? 'eager' : 'lazy'}
      fetchPriority={priority ? 'high' : 'auto'}
      decoding="async"
    />
  )
}
 
// Usage
<OptimizedImage src="hero.jpg" alt="Hero" width={1200} height={600} priority />
<OptimizedImage src="product.jpg" alt="Product" width={400} height={300} />

How the Browser Determines "Near Viewport"

The browser uses a distance threshold based on network speed. On a slow connection, the threshold is larger (loading starts sooner) to compensate for slower download speeds.

Per the HTML spec (opens in new tab), the exact threshold is implementation-defined. Chromium's thresholds range from 1250px on a slow connection to 2500px on a fast connection.

Intersection Observer (Legacy Fallback)

For environments that need custom lazy loading behaviour (e.g., custom intersection margins), use the Intersection Observer API. For most modern use cases, the native loading="lazy" attribute is sufficient.

// Only needed if you need custom lazy loading behaviour
// Native loading="lazy" is preferred for standard use cases
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target
        img.src = img.dataset.src
        if (img.dataset.srcset) {
          img.srcset = img.dataset.srcset
        }
        observer.unobserve(img)
      }
    })
  },
  { rootMargin: '200px' } // Start loading 200px before entering viewport
)
 
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img))

Support Notes

  • Image format and delivery behavior can vary by browser, CDN, and device characteristics, so verify the final bytes and rendered output on the supported browser matrix.
  • Add a fallback note when a modern format or loading behavior is not available for every required target browser.

Verification

Automated Checks

  • Open Chrome DevTools → Network → filter by "Img" → reload and scroll down—images should only appear in the Network waterfall as they enter the viewport
  • Run Lighthouse — the "Defer offscreen images" audit flags lazy-loadable images
  • Use the Coverage tab in DevTools to see how much data is deferred
  • Verify the LCP image is NOT marked loading="lazy" in the Lighthouse report

Manual Checks

  • Verify the rendered or user-facing behavior manually in a representative browser or runtime flow.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Scan all <img> elements in this codebase. Identify: 1) Any <img> elements below the fold (not in the first viewport) that are missing loading="lazy". 2) Any <img> elements marked loading="lazy" that appear to be above the fold (hero images, first product images, logos in headers). 3) Any <img> with loading="eager" below the fold. Report the above-fold images that should NOT have lazy loading separately from below-fold images that should have it. If fold position cannot be inferred from the snippet, do not report missing lazy loading as a defect.

Fix

Auto-fix issues

For images that should use lazy loading: 1) Add loading="lazy" to all <img> elements that appear below the fold. 2) Remove loading="lazy" from any hero, logo, or first-visible image—these should load immediately. 3) Ensure all lazy-loaded images have explicit width and height attributes to prevent CLS. 4) For the LCP image specifically, add fetchpriority="high" and remove loading="lazy". Show the corrected HTML for each modified image.

Explain

Learn more

Explain how native browser lazy loading works. The loading="lazy" attribute tells the browser to defer image download until the image is within a browser-defined distance from the viewport (typically 1250px on a slow connection). This reduces the data transferred on initial load, lowers Time to Interactive, and saves bandwidth for users who never scroll to those images. Browser support is universal in modern browsers. The critical caveat: never lazy-load the LCP image—it must load as fast as possible.

Review

Code review

Review image assets, markup, and delivery configuration related to Lazy load offscreen images. Flag exact files or components where format choice, sizing, or loading behavior violates the rule, and describe how to confirm the fix in DevTools.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Lazy loading images — MDN Web Docs

by MDN

developer.mozilla.orgDocs
Lighthousedeveloper.chrome.comTool
Chrome DevTools Coverage tabdeveloper.chrome.comTool

Rules that often go hand-in-hand with this one.

Set explicit width and height on images

All <img> elements have explicit width and height attributes so browsers can reserve space before the image loads, preventing layout shift.

Images
Optimize largest contentful paint

The largest content element loads within 2.5 seconds for a good user experience.

Performance
Use srcset for responsive images

Images wider than 100px use the srcset attribute to offer multiple resolution variants, letting the browser download the optimal size for the user's viewport and device pixel ratio.

Images

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385