I’ve put together a Screaming Frog Custom JavaScript snippet to flag images that are both above the fold and lazy-loaded. It checks native <span>loading="lazy"</span> as well as common JavaScript lazy-loading classes and attributes, so it should catch more than just the obvious native implementation.
It also flags whether the image is the LCP element, the lazy-loading method, rendered dimensions and image URL. Handy for quickly spotting hero and other prominent images that probably shouldn’t be lazy-loaded, without having to manually work through page templates.
It also flags whether the image is the LCP element, the lazy-loading method, rendered dimensions and image URL. Handy for quickly spotting hero and other prominent images that probably shouldn’t be lazy-loaded, without having to manually work through page templates.
Code:
// Above-the-fold lazy-loaded images (native + JS libraries), with LCP flag.
// Output per image: LCP? | method | size | url
const LAZY_CLASS = /\blazy(load|loaded|loading)?\b/i;
const LAZY_ATTRS = ['data-src', 'data-srcset', 'data-lazy-src', 'data-lazy-srcset', 'data-original'];
function getLcpElement(timeout = 1000) {
return new Promise(resolve => {
if (!('PerformanceObserver' in window) ||
!(PerformanceObserver.supportedEntryTypes || []).includes('largest-contentful-paint')) {
return resolve(null);
}
let last = null;
const po = new PerformanceObserver(list => {
const entries = list.getEntries();
if (entries.length) last = entries[entries.length - 1];
});
po.observe({ type: 'largest-contentful-paint', buffered: true });
setTimeout(() => { po.disconnect(); resolve(last ? last.element : null); }, timeout);
});
}
function lazyMethod(img) {
if (img.loading === 'lazy') return 'native';
const hasLazyAttr = el => LAZY_ATTRS.some(a => el.hasAttribute(a));
if (hasLazyAttr(img) || LAZY_CLASS.test(img.className)) return 'js';
const picture = img.closest('picture');
if (picture && [...picture.querySelectorAll('source')].some(hasLazyAttr)) return 'js';
return null;
}
function isRendered(img) {
if (typeof img.checkVisibility === 'function') {
return img.checkVisibility({
opacityProperty: true, visibilityProperty: true,
checkOpacity: true, checkVisibilityCSS: true
});
}
for (let el = img; el; el = el.parentElement) {
const s = getComputedStyle(el);
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
}
return true;
}
function imageUrl(img) {
const candidates = [
img.currentSrc,
img.getAttribute('data-src'),
img.getAttribute('data-lazy-src'),
img.getAttribute('data-original'),
img.getAttribute('src')
];
const raw = candidates.find(u => u && !u.startsWith('data:'));
if (!raw) return '(inline placeholder only)';
try { return new URL(raw, document.baseURI).href; } catch (e) { return raw; }
}
return getLcpElement().then(lcpEl => {
const vw = window.innerWidth;
const vh = window.innerHeight;
const seen = new Set();
const rows = [...document.querySelectorAll('img')]
.map(img => ({ img, method: lazyMethod(img), rect: img.getBoundingClientRect() }))
.filter(({ img, method, rect }) =>
method &&
isRendered(img) &&
rect.top < vh && rect.bottom >= 0 &&
rect.left < vw && rect.right >= 0
)
.sort((a, b) => a.rect.top - b.rect.top)
.map(({ img, method, rect }) => {
const url = imageUrl(img);
if (seen.has(url)) return null;
seen.add(url);
const size = (rect.width && rect.height)
? `${Math.round(rect.width)}x${Math.round(rect.height)}`
: 'unsized';
const lcp = (lcpEl && (lcpEl === img || lcpEl.contains(img))) ? 'LCP' : '-';
return `${lcp} | ${method} | ${size} | ${url}`;
})
.filter(Boolean);
return seoSpider.data(rows);
});