# Find above the fold lazy loaded images

> Source: <https://webperf-snippets.net/Loading/Find-Above-The-Fold-Lazy-Loaded-Images>

Detect images with lazy loading that are incorrectly placed above the fold (in the viewport). Lazy loading above-the-fold images is a common performance anti-pattern that can significantly harm your Largest Contentful Paint (LCP) score.

The snippet identifies images with `loading="lazy"` or `[data-src]` in the viewport, detects the LCP candidate, and provides detailed performance metrics.

> **Note:** **Looking for images that should have lazy loading?** Use the [Find non Lazy Loaded Images outside of the viewport](https://webperf-snippets.net/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.md) snippet instead.

### Snippet

```js
// Detect lazy loaded images above the fold
// https://webperf-snippets.net

(function() {
  // @shared isOwnRequest
  // True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
  // did not make it, so no snippet counts it.
  function isOwnRequest(entry) {
    return entry.name.endsWith("#webperf-snippets");
  }
  // @end-shared isOwnRequest

  function getSelector(node) {
    let sel = "";
    try {
      while (node && node.nodeType !== 9) {
        const el = node;
        const name = el.nodeName.toLowerCase();
        const part = el.id
          ? "#" + el.id
          : name +
            (el.classList && el.classList.value && el.classList.value.trim()
              ? "." + el.classList.value.trim().split(/\s+/).slice(0, 2).join(".")
              : "");
        if (sel.length + part.length > 80) return sel || part;
        sel = sel ? part + ">" + sel : part;
        if (el.id) break;
        node = el.parentNode;
      }
    } catch {}
    return sel;
  }

  function isInViewport(element) {
    const rect = element.getBoundingClientRect();
    return (
      rect.top < window.innerHeight &&
      rect.bottom > 0 &&
      rect.left < window.innerWidth &&
      rect.right > 0 &&
      rect.width > 0 &&
      rect.height > 0
    );
  }

  function getDistanceFromViewportEdge(element) {
    const rect = element.getBoundingClientRect();
    const distances = [
      rect.top,
      window.innerHeight - rect.bottom,
      rect.left,
      window.innerWidth - rect.right
    ];
    return Math.min(...distances.map(d => Math.abs(d)));
  }

  function getFileSize(src) {
    if (!src) return null;
    try {
      const entries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
      const entry = entries.find(e => e.name === src || e.name.includes(src.split("/").pop()));
      if (entry && entry.transferSize) {
        return (entry.transferSize / 1024).toFixed(1) + " KB";
      }
    } catch {}
    return null;
  }

  function getLazyType(element) {
    const hasLoadingLazy = element.getAttribute("loading") === "lazy";
    const hasDataSrc = element.hasAttribute("data-src") || element.hasAttribute("data-lazy");

    if (hasLoadingLazy && hasDataSrc) return "both";
    if (hasLoadingLazy) return "loading-lazy";
    if (hasDataSrc) return "data-src";
    return null;
  }

  function getElementInfo(img) {
    const rect = img.getBoundingClientRect();
    const area = rect.width * rect.height;

    return {
      selector: getSelector(img),
      lazyType: getLazyType(img),
      dimensions: `${Math.round(rect.width)}×${Math.round(rect.height)}`,
      position: {
        top: Math.round(rect.top + window.scrollY),
        left: Math.round(rect.left + window.scrollX)
      },
      distanceFromEdge: Math.round(getDistanceFromViewportEdge(img)) + "px",
      src: img.currentSrc || img.src || img.getAttribute("data-src") || "",
      srcset: img.srcset || null,
      sizes: img.sizes || null,
      alt: img.alt || "(no alt)",
      fetchPriority: img.fetchPriority || "auto",
      fileSize: getFileSize(img.currentSrc || img.src),
      area: area,
      element: img
    };
  }

  const results = {
    lazyImages: [],
    lcpCandidate: null,
    nodeArray: [],
    summary: {
      total: 0,
      withLoadingLazy: 0,
      withDataSrc: 0,
      lcpAffected: false
    }
  };

  // Find all images with lazy loading
  const lazySelectors = '[loading="lazy"], [data-src], [data-lazy]';
  const lazyElements = document.querySelectorAll(`img${lazySelectors}, picture source${lazySelectors}`);

  // Also check all images in viewport to find LCP candidate
  const allViewportImages = Array.from(document.querySelectorAll("img"))
    .filter(img => isInViewport(img) && img.getBoundingClientRect().width > 0);

  // Find LCP candidate (largest visible image)
  let lcpCandidate = null;
  let maxArea = 0;

  allViewportImages.forEach(img => {
    const rect = img.getBoundingClientRect();
    const area = rect.width * rect.height;
    if (area > maxArea) {
      maxArea = area;
      lcpCandidate = img;
    }
  });

  // Process lazy loaded images in viewport
  lazyElements.forEach(element => {
    const img = element.tagName === "SOURCE"
      ? element.closest("picture")?.querySelector("img")
      : element;

    if (!img || !isInViewport(img)) return;

    const info = getElementInfo(img);
    info.isLcpCandidate = img === lcpCandidate;

    results.lazyImages.push(info);
    results.nodeArray.push(img);

    // Update summary
    results.summary.total++;
    if (info.lazyType === "loading-lazy" || info.lazyType === "both") {
      results.summary.withLoadingLazy++;
    }
    if (info.lazyType === "data-src" || info.lazyType === "both") {
      results.summary.withDataSrc++;
    }
    if (info.isLcpCandidate) {
      results.summary.lcpAffected = true;
      results.lcpCandidate = info;
    }
  });

  // Display results
  console.group("🔍 Lazy Loading Detection - Above The Fold");

  if (results.lazyImages.length === 0) {
    console.log(
      "%c✅ Good job! No lazily loaded images found in the viewport.",
      "background: #222; color: #22c55e; padding: 0.5ch 1ch; font-weight: bold;"
    );
  } else {
    // Summary
    console.log(
      `%c⚠️ Found ${results.summary.total} lazily loaded image(s) above the fold`,
      "color: #f59e0b; font-weight: bold; font-size: 14px;"
    );
    console.log("");

    // LCP Warning
    if (results.summary.lcpAffected) {
      console.group("🚨 Critical: LCP Candidate Has Lazy Loading");
      console.log(
        "%cThe largest image in your viewport has lazy loading enabled!",
        "color: #ef4444; font-weight: bold;"
      );
      console.log("This can significantly delay your Largest Contentful Paint.");
      console.log("");
      console.log("LCP Candidate:", results.lcpCandidate.selector);
      console.log("Dimensions:", results.lcpCandidate.dimensions);
      console.log("File size:", results.lcpCandidate.fileSize || "unknown");
      console.log("");
      console.log("%cFix: Remove loading=\"lazy\" and add fetchpriority=\"high\"", "color: #22c55e;");
      console.groupEnd();
      console.log("");
    }

    // Table of all problematic images
    console.group("📋 Lazy Loaded Images in Viewport");
    const tableData = results.lazyImages.map((image) => ({
      selector: image.selector,
      lazyType: image.lazyType,
      dimensions: image.dimensions,
      top: image.position.top + "px",
      distanceFromEdge: image.distanceFromEdge,
      src: image.src,
      srcset: image.srcset,
      sizes: image.sizes,
      alt: image.alt,
      fetchPriority: image.fetchPriority,
      fileSize: image.fileSize,
      isLcpCandidate: image.isLcpCandidate ? "⚠️ YES" : "no",
    }));
    console.table(tableData);
    console.groupEnd();

    // Elements for inspection
    console.group("🔎 Elements for inspection");
    console.log("Click to expand and inspect in Elements panel:");
    results.nodeArray.forEach((node, i) => {
      const marker = node === lcpCandidate ? " 🚨 LCP" : "";
      console.log(`${i + 1}.${marker}`, node);
    });
    console.groupEnd();

    // Quick fix suggestion
    console.log("");
    console.group("📝 Suggested Fix");
    console.log("For above-the-fold images, use:");
    console.log("");
    console.log(
      '%c<img src="hero.jpg" fetchpriority="high" alt="...">',
      "font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 8px; border-radius: 4px;"
    );
    console.log("");
    console.log("Remove: loading=\"lazy\", data-src, data-lazy");
    console.log("Add: fetchpriority=\"high\" for LCP candidate");
    console.groupEnd();
  }

  console.groupEnd();

  return {
    script: "Find-Above-The-Fold-Lazy-Loaded-Images",
    status: "ok",
    count: results.lazyImages.length,
    details: {
      total: results.summary.total,
      withLoadingLazy: results.summary.withLoadingLazy,
      withDataSrc: results.summary.withDataSrc,
      lcpAffected: results.summary.lcpAffected,
    },
    items: results.lazyImages.map((image) => ({
      selector: image.selector,
      lazyType: image.lazyType,
      dimensions: image.dimensions,
      position: `${image.position.top}px, ${image.position.left}px`,
      distanceFromEdge: image.distanceFromEdge,
      src: image.src,
      srcset: image.srcset,
      sizes: image.sizes,
      alt: image.alt,
      fetchPriority: image.fetchPriority,
      fileSize: image.fileSize,
      isLcpCandidate: image.isLcpCandidate,
    })),
    issues: [
      ...(results.summary.lcpAffected ? [{ severity: "error", message: 'LCP candidate image has lazy loading: remove loading="lazy" and add fetchpriority="high"' }] : []),
      ...(results.lazyImages.filter(i => !i.isLcpCandidate).length > 0 ? [{ severity: "warning", message: `${results.lazyImages.filter(i => !i.isLcpCandidate).length} above-fold image(s) have lazy loading: remove loading="lazy"` }] : []),
    ],
  };
})();
```

### Understanding the results

The snippet scans the viewport for images that have lazy loading but shouldn't.

#### Success case

If no lazy-loaded images are found in the viewport:

```
✅ Good job! No lazily loaded images found in the viewport.
```

#### Issues found

When lazy-loaded images are detected in the viewport, you'll see:

**Summary**: Total count of problematic images

**LCP Warning** (if applicable): Critical alert when the largest image has lazy loading

**Image Table** with details:

| Field            | Description                                                       |
| ---------------- | ----------------------------------------------------------------- |
| `selector`       | CSS selector path to the element                                  |
| `lazyType`       | Type of lazy loading: `"loading-lazy"`, `"data-src"`, or `"both"` |
| `dimensions`     | Image dimensions as `"width×height"`                              |
| `src`            | Current image source URL                                          |
| `fetchPriority`  | Current fetch priority (`"high"`, `"low"`, `"auto"`)              |
| `fileSize`       | Transfer size in KB (if available via Performance API)            |
| `isLcpCandidate` | Whether this is the largest image (potential LCP)                 |

**Elements for inspection**: Direct DOM references you can click to inspect in DevTools

### Why this matters: Impact on Core Web Vitals

Lazy loading is a powerful optimization technique, but incorrect usage can severely harm your Largest Contentful Paint (LCP) score.

### The problem

When you add `loading="lazy"` to an above-the-fold image:

1. Browser defers loading until image is "near" the viewport
2. For above-fold images, this adds unnecessary delay
3. The image loads later than it would have naturally
4. LCP score increases (worse performance)

### Performance impact

| Scenario                                | Impact on LCP          |
| --------------------------------------- | ---------------------- |
| LCP image with `loading="lazy"`         | +200-500ms delay       |
| Hero image with `loading="lazy"`        | +100-300ms delay       |
| Above-fold gallery with lazy loading    | +150-400ms cumulative  |
| Correctly lazy-loaded below-fold images | -100-500ms improvement |

### The solution

| Image Location                           | Recommendation                          |
| ---------------------------------------- | --------------------------------------- |
| LCP candidate (largest above-fold image) | `fetchpriority="high"`, NO lazy loading |
| Other above-fold images                  | NO lazy loading, default priority       |
| Below-fold images                        | `loading="lazy"`                        |
| Images in hidden containers              | `loading="lazy"`                        |

### Best practices

#### When to use `loading="lazy"`

✅ **Good candidates:**

- Images below the fold
- Images in carousels (non-active slides)
- Images in tabs (non-active panels)
- Images in accordions (collapsed sections)
- Images in modals
- Footer images
- Blog post images after the first paragraph

❌ **Never lazy load:**

- The LCP candidate (largest above-fold image)
- Hero images
- Logo images in viewport
- Critical product images
- Any image visible on initial page load

#### Prioritizing critical images

For your LCP candidate, use `fetchpriority="high"`:

```html copy
<!-- LCP candidate - highest priority, no lazy loading -->
<img
  src="hero-image.jpg"
  fetchpriority="high"
  alt="Hero image description"
>

<!-- Other above-fold images - no lazy loading -->
<img src="product.jpg" alt="Product image">

<!-- Below-fold images - lazy loading -->
<img
  src="related-product.jpg"
  loading="lazy"
  alt="Related product"
>
```

#### Combining with responsive images

```html copy
<!-- LCP candidate with responsive images -->
<img
  src="hero-800.jpg"
  srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
  fetchpriority="high"
  alt="Hero image"
>

<!-- Below-fold responsive image with lazy loading -->
<img
  src="content-800.jpg"
  srcset="content-400.jpg 400w, content-800.jpg 800w"
  sizes="(max-width: 600px) 400px, 800px"
  loading="lazy"
  alt="Content image"
>
```

### Implementation examples

### Fixing an LCP image

```html copy
<!-- ❌ Before: Lazy loaded LCP candidate -->
<img
  src="hero.jpg"
  loading="lazy"
  alt="Hero"
>

<!-- ✅ After: Prioritized LCP candidate -->
<img
  src="hero.jpg"
  fetchpriority="high"
  alt="Hero"
>
```

### Picture element with proper loading

```html copy
<!-- Above-fold picture element -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img
    src="hero.jpg"
    fetchpriority="high"
    alt="Hero image"
  >
</picture>

<!-- Below-fold picture element -->
<picture>
  <source srcset="content.avif" type="image/avif">
  <source srcset="content.webp" type="image/webp">
  <img
    src="content.jpg"
    loading="lazy"
    alt="Content image"
  >
</picture>
```

### JavaScript lazy loading libraries

If using a JS library (lazysizes, vanilla-lazyload, etc.), configure it to exclude above-fold images:

```js copy
// Example: Exclude images with data-priority attribute
document.querySelectorAll('img[data-src]').forEach(img => {
  const rect = img.getBoundingClientRect();
  if (rect.top < window.innerHeight) {
    // Above fold - load immediately
    img.src = img.dataset.src;
    img.removeAttribute('data-src');
  }
});
```

### Browser support

The snippet needs these features to run.

| Feature                                                                                               | Chrome | Edge | Firefox | Safari |
| ----------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [PerformanceResourceTiming](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming)     |  ✅ 29  | ✅ 12 |   ✅ 35  |  ✅ 11  |
| [HTMLImageElement.currentSrc](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/currentSrc) |  ✅ 38  | ✅ 13 |   ✅ 38  |  ✅ 9.1 |
| All of the above                                                                                      |  ✅ 38  | ✅ 13 |   ✅ 38  |  ✅ 11  |

The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.

| Feature                                                                                                | Chrome |  Edge | Firefox | Safari |
| ------------------------------------------------------------------------------------------------------ | :----: | :---: | :-----: | :----: |
| \<img loading>                                                                                         |  ✅ 77  |  ✅ 79 |   ✅ 75  | ✅ 15.4 |
| [\<img fetchpriority>](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/fetchpriority) |  ✅ 101 | ✅ 101 |  ✅ 132  | ✅ 17.2 |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), version 8.1.4.

Browsers without support ignore these attributes safely.

### Further reading

- [Browser-level image lazy loading for the web](https://web.dev/articles/browser-level-image-lazy-loading) | web.dev
- [Lazy loading images](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) | MDN Web Docs
- [Optimize Largest Contentful Paint](https://web.dev/articles/optimize-lcp) | web.dev
- [Fetch Priority API](https://web.dev/articles/fetch-priority) | web.dev
- [The loading attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading) | MDN Web Docs
- [Don't lazy-load LCP images](https://web.dev/articles/lcp-lazy-loading) | web.dev

### Note

*Run this snippet after the page has fully loaded without scrolling for the most accurate viewport detection. The Performance API is used to retrieve file sizes when available.*
