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.
Looking for images that should have lazy loading? Use the Find non Lazy Loaded Images outside of the viewport snippet instead.
Snippet
// Detect lazy loaded images above the fold
// https://webperf-snippets.nucliweb.net
(function() {
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 (err) {}
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");
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 (err) {}
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(({ element, area, position, ...rest }) => ({
...rest,
top: position.top + "px",
isLcpCandidate: rest.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();
})();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:
- Browser defers loading until image is "near" the viewport
- For above-fold images, this adds unnecessary delay
- The image loads later than it would have naturally
- 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":
<!-- 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
<!-- 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:
<!-- ❌ 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:
<!-- 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:
// 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
Native lazy loading (loading="lazy") is supported in:
- Chrome 77+
- Edge 79+
- Firefox 75+
- Safari 15.4+
- Opera 64+
The fetchpriority attribute is supported in:
- Chrome 101+
- Edge 101+
- Opera 87+
Not yet in Firefox and Safari, but they ignore the attribute safely.
Further Reading
- Browser-level image lazy loading for the web (opens in a new tab) | web.dev
- Lazy loading images (opens in a new tab) | MDN Web Docs
- Optimize Largest Contentful Paint (opens in a new tab) | web.dev
- Fetch Priority API (opens in a new tab) | web.dev
- The loading attribute (opens in a new tab) | MDN Web Docs
- Don't lazy-load LCP images (opens in a new tab) | 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.