Cookie Consent by Free Privacy Policy Generator

Screaming Frog Custom JS snippet to flag images that are above the fold and lazy-loaded

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.

1791211672739.webp


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);
});
 
Back
Top
Community platform by XenForo® © 2010-2024 XenForo Ltd.