Quick Tip
    frontend

    Why native lazy loading hurts LCP and how to fix it

    Analyzes the performance penalty of using loading="lazy" on above-the-fold images. Demonstrates how to properly combine eager loading for hero images with lazy loading for off-screen content.

    Editor: Paul RadfordAug 10, 20268 min read

    Paul RadfordFull-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

    Why native lazy loading hurts LCP and how to fix it

    Why native lazy loading hurts LCP and how to fix it

    Putting loading="lazy" on your hero image is one of the most common ways to sabotage your own Largest Contentful Paint score. The attribute tells the browser to skip a resource until it's near the viewport, which is exactly the wrong instruction for the image users see first. The fix is simple: eager-load (or don't annotate at all) anything above the fold, and reserve loading="lazy" strictly for images that start off-screen.

    What the preload scanner actually does

    Before the main HTML parser even gets going, Chrome, Firefox, and Safari all run a separate, lightweight pass over the document called the preload scanner. Its job is to look ahead in the raw markup, spot resources like <img>, <link rel="stylesheet">, and <script>, and kick off network requests for them immediately, well before layout or render-blocking scripts finish executing. This is why a <link rel="preload"> or an early <img> tag can start downloading in the first few milliseconds of a page load, in parallel with everything else.

    Here's the part that trips people up: the preload scanner deliberately skips images marked loading="lazy". That's the entire point of the attribute. According to the MDN documentation for the img element's loading attribute, a lazy image's fetch is deferred until the browser calculates that the image is close to entering the viewport, which usually means waiting on layout, sometimes waiting on JavaScript, and always waiting longer than an eager fetch would.

    So when your hero image, the one rendered at the top of the page and almost always the LCP candidate, carries loading="lazy", you've manually disabled the exact browser optimization that would have gotten it downloading fastest. Instead of the preload scanner firing off the request in the first few milliseconds, the browser now has to finish building the DOM, run layout, determine the image is in or near the viewport, and only then start the fetch. On a slow mobile connection, that gap can add hundreds of milliseconds, sometimes over a second, directly onto your LCP time.

    The HTML: hero eager, everything else lazy

    The pattern is straightforward once you see it laid out. Your hero or above-the-fold images get no loading attribute (the browser default is eager), while everything the user has to scroll to see gets loading="lazy".

    html
    1<!-- Hero image: no loading attribute needed.
    2 Default behavior is eager, which lets the preload
    3 scanner discover and fetch it immediately. -->
    4<img
    5 src="/images/hero-desktop.jpg"
    6 srcset="/images/hero-mobile.jpg 480w, /images/hero-desktop.jpg 1200w"
    7 sizes="(max-width: 600px) 480px, 1200px"
    8 width="1200"
    9 height="600"
    10 alt="Team collaborating around a whiteboard"
    11 fetchpriority="high"
    12/>
    13
    14<!-- Article body content, scrolled into view later.
    15 loading="lazy" is correct here: the browser will
    16 defer the network request until the image nears
    17 the viewport, saving bandwidth on initial load. -->
    18<img
    19 src="/images/article-diagram.jpg"
    20 width="800"
    21 height="450"
    22 alt="Diagram showing request lifecycle"
    23 loading="lazy"
    24/>
    25
    26<!-- Footer or "related articles" thumbnails, definitely
    27 below the fold on virtually every screen size. -->
    28<img
    29 src="/images/related-post-1.jpg"
    30 width="400"
    31 height="250"
    32 alt="Related post thumbnail"
    33 loading="lazy"
    34/>

    Two details matter beyond the loading attribute itself. First, fetchpriority="high" on the hero image is worth adding on top of the default eager behavior. It tells the browser this resource should be prioritized over other same-priority requests competing for bandwidth, which is documented in the Chrome developers guide to fetchpriority. Second, always set explicit width and height (or use aspect-ratio in CSS) on every image regardless of loading strategy. Without dimensions, the browser can't reserve space in the layout, and you'll get cumulative layout shift on top of your LCP problems.

    When does an image actually count as "above the fold"?

    This is where teams get sloppy. "Above the fold" isn't a fixed pixel count. It depends on viewport size, and a component that's above the fold on a 1440px desktop monitor might be scrolled halfway down on a 375px phone screen. If you're building a responsive layout where the hero image behaves differently across breakpoints, don't lazy-load conditionally with JavaScript media query checks. That adds complexity and a layer of runtime logic for something the browser already handles better through normal document order and CSS.

    A more reliable rule: if an image is likely to be visible without any scrolling on your most common viewport (check your analytics for the actual device breakdown, don't guess), treat it as eager. Everything after the first visible screen's worth of content is a safe candidate for lazy loading. Carousels are a special case worth calling out: the first slide should be eager since it's visible immediately, but subsequent slides, even if technically "in the DOM already", should be lazy since the user hasn't scrolled or clicked to see them yet.

    Measuring the actual LCP penalty

    The impact isn't theoretical. Google's own performance team has documented this exact anti-pattern in the web.dev article on LCP and lazy loading, which walks through why lazy-loading an LCP candidate delays its render and directly inflates the metric. The mechanism is simple: LCP measures the render time of the largest visible content element, and if that element's image fetch doesn't even start until layout completes, you've added a full render-blocking dependency chain in front of the fetch that should have started at parse time.

    In practice, on a typical hero image around 100 to 200 KB served without lazy loading, the preload scanner can have the request in flight within the first 100ms of HTML parsing on a fast connection, often sooner. Add loading="lazy" to that same image, and the fetch doesn't begin until the layout engine has computed geometry for the element, which itself is gated behind CSS parsing and, if you're using a JS framework that hydrates the DOM, sometimes behind hydration. On mid-tier Android hardware over a throttled 4G connection, that extra step alone commonly costs 200 to 600ms before the request even starts, on top of whatever the download itself takes. That's the gap between an LCP under 2.5 seconds (the "good" threshold under Google's Core Web Vitals guidance) and an LCP that slides into the "needs improvement" or "poor" bucket.

    Testing whether your hero image is actually eager

    Don't rely on eyeballing the HTML. Verify it directly:

    js
    1// Run this in the browser console on your page.
    2// It reports whether the LCP candidate had a lazy
    3// loading attribute at the time it was measured.
    4new PerformanceObserver((entryList) => {
    5 const entries = entryList.getEntries();
    6 const lastEntry = entries[entries.length - 1];
    7 console.log('LCP element:', lastEntry.element);
    8 console.log('LCP time (ms):', lastEntry.renderTime || lastEntry.loadTime);
    9 if (lastEntry.element && lastEntry.element.tagName === 'IMG') {
    10 console.log('loading attribute:', lastEntry.element.loading);
    11 }
    12}).observe({ type: 'largest-contentful-paint', buffered: true });

    This uses the PerformanceObserver API to grab the actual LCP entry, including a reference to the DOM element responsible. If loading reports "lazy" on that element, you've found your bug directly, no guessing required.

    Beyond DevTools, run Lighthouse or PageSpeed Insights against the production URL rather than localhost. CDN latency, real image compression, and actual network conditions all factor into LCP, and a localhost test with everything cached will hide the problem entirely. Chrome DevTools' Network panel is also useful here: filter by image type, then check the "Waterfall" column. An eager hero image should show its request starting essentially at the same time as the initial HTML document request. If you see a large gap, something (a lazy attribute, a JS-driven src swap, or a slow-loading stylesheet blocking layout) is delaying it.

    The edge case that catches people out: lazy loading inside components

    Component-based frameworks often default every <img> in a shared "Image" or "Card" component to loading="lazy" for consistency, since that's the safe default for most usage. The problem shows up when that shared component gets reused for the hero slot on a landing page. Nobody remembers to override the prop, the hero image inherits lazy loading, and LCP quietly regresses on a page nobody thought to re-audit. If you're building a design system, give the image component an explicit priority or eager boolean prop that developers must set for above-the-fold placements, rather than relying on everyone remembering to override a lazy default. Next.js's Image component actually enforces this directly: passing priority disables lazy loading and adds a preload hint, and the framework will warn you in development if it detects an LCP image that isn't marked as a priority.

    One more gotcha worth flagging: loading="lazy" has no effect on images inside an iframe that hasn't loaded yet, and Safari's implementation (shipped in Safari 16.4, released March 2023) still handles lazy-loaded image priority slightly differently from Chromium's under certain scroll-anchoring conditions. If you're chasing a small LCP regression that only shows up in Safari, check whether it's related to loading strategy before assuming it's a CSS or font issue.

    The rule of thumb that actually holds up in production: lazy loading is a bandwidth optimization for images the user probably won't see immediately, not a default you sprinkle on every <img> tag for tidiness. Treat your LCP candidate as a VIP that skips the queue, and let everything else wait its turn.

    Related Articles