Lazy Loading Best Practices: You Made LCP Worse. Here's Why.

2026-03-13 · Nico Brandt

You slapped loading="lazy" on every <img> tag. Lighthouse rewarded you with a worse LCP score.

This isn’t a bug. You told the browser to deprioritize the most important image on the page. Pages without lazy-loaded LCP images hit “good” LCP 79% of the time. Pages that lazy load their LCP element? 52%. You’re probably in that second group. And the fix takes two lines — but you need to understand why it broke first, or you’ll break it again.

The Mistake That Tanks Your LCP Score

The LCP image is the largest visible element when the page first loads. Usually a hero image. Sometimes a product photo or banner. Whatever it is, the browser uses it to measure Largest Contentful Paint — one of the three Core Web Vitals that determines whether Google considers your page fast or slow.

When you add loading="lazy" to that image, you’re telling the browser: “don’t fetch this yet, it’s not important.” The browser listens. It defers the request. Your LCP metric tanks because the thing being measured is the thing you told it to wait on.

This isn’t a niche problem. WordPress auto-applies loading="lazy" to every image since version 5.5 — and WordPress is responsible for 84% of lazy loading adoption on the web. Millions of sites are running this lazy loading performance mistake without knowing it.

The instinct to lazy load everything makes sense. Images are the heaviest assets on most pages — at the 90th percentile, sites ship over 5 MB of images. Less loading up front means faster pages, right?

Not when “less loading” includes the one image the browser needs to render first. Your hero image should load eagerly. Not lazily. The question is how to tell the browser which is which.

The Two-Line Fix: fetchpriority and loading

Here’s the rule. Images visible in the initial viewport get loading="eager" — or just omit the loading attribute entirely, since eager is the default. Everything below the fold gets loading="lazy".

The code you want:

<!-- Hero image: load immediately, high priority -->
<img src="hero.webp" alt="..." fetchpriority="high" width="1200" height="630">

<!-- Below-the-fold images: lazy load -->
<img src="feature-1.webp" alt="..." loading="lazy" width="600" height="400">
<img src="feature-2.webp" alt="..." loading="lazy" width="600" height="400">

The code most people write:

<!-- Don't do this -->
<img src="hero.webp" alt="..." loading="lazy" width="1200" height="630">
<img src="feature-1.webp" alt="..." loading="lazy" width="600" height="400">
<img src="feature-2.webp" alt="..." loading="lazy" width="600" height="400">

The fetchpriority="high" attribute on your hero image tells the browser to fetch it before other images. Combined with not lazy loading it, you get the fastest possible LCP. Two attributes. That’s it.

The tricky part: you can’t always know which image is above the fold. It depends on viewport size. A hero that dominates desktop might sit below the fold on a 375px phone screen. When in doubt, don’t lazy load the first two images in your markup. Being slightly aggressive with eager loading is always safer than accidentally lazy loading your LCP element.

One more detail for responsive images. If you’re using <picture> or srcset, the loading attribute goes on the <img> tag, not on <source>:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="..." fetchpriority="high" width="1200" height="630">
</picture>

If you’re working through a broader web performance checklist, the LCP image fix belongs at the top. Native loading="lazy" handles the rest — for standard page layouts. But “standard” has limits.

When Native Won’t Cut It: Intersection Observer

Native loading="lazy" works great for a page where images appear in order as you scroll down. The browser handles the thresholds — 1250px on fast 4G connections, 2500px on slower 3G — and loads images before they enter the viewport. You don’t write JavaScript. You don’t configure anything.

Three cases where that breaks down.

Carousels and tabbed panels. The image is in the DOM but visually hidden behind a tab or off-screen in a slider. The browser might consider it “near the viewport” and load it eagerly, or it might not load it at all until the tab is active. Native behavior is inconsistent here.

Infinite scroll. Images are injected into the DOM dynamically. loading="lazy" only works on images present at parse time — dynamically added images need explicit handling.

Custom thresholds. Chrome’s 1250px default works for most sites. If you need tighter control — say, loading images only when they’re 200px from entering the viewport — you need rootMargin.

Here’s a clean Intersection Observer pattern:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));

Observe. Load on intersect. Unobserve. Three steps, no library needed. Intersection Observer is supported in all modern browsers — the only holdout is IE 11, and if you’re still supporting IE 11 in 2026, we need a different conversation.

One critical rule: don’t combine loading="lazy" with Intersection Observer on the same image. You get double-gating — the browser’s native threshold plus your JavaScript threshold — and the image loads later than either approach would on its own. Pick one. For standard layouts, native wins on simplicity. For carousels, tabs, and dynamic content, Intersection Observer wins on control.

You’ve got the loading strategy right. But there’s one more way lazy loading silently wrecks your page — and it shows up in a different Core Web Vital.

The CLS Trap (And How to Verify Your Setup)

A lazy-loaded image without explicit dimensions is a layout shift waiting to happen. The browser reserves zero space for it. When the image finally loads, everything below it jumps down. That tanks your Cumulative Layout Shift score — and annoys every user who was mid-sentence when the paragraph they were reading lurched downward.

The fix is boring: always set width and height attributes on lazy-loaded images. Or use CSS aspect-ratio. Either way, the browser knows how much space to reserve before the image arrives.

<!-- Good: browser reserves space -->
<img src="chart.webp" loading="lazy" width="800" height="450" alt="...">

<!-- Bad: layout shift on load -->
<img src="chart.webp" loading="lazy" alt="...">

If you’ve been debugging why your pages feel janky — why your website is slow has the full diagnostic — CLS from lazy-loaded images is one of the most common culprits.

To verify your implementation, open DevTools. Performance tab. Reload the page. Look for layout shift markers (pink bars on the timeline). If they coincide with image loads, you’re missing dimensions.

For a quick console audit:

document.querySelectorAll('img[loading="lazy"]').forEach(img => {
  if (!img.width || !img.height) console.warn('Missing dimensions:', img.src);
});

Every image that logs a warning needs width and height added. That’s your CLS fix list.

The Lazy Loading Checklist

You started with a worse LCP score because you lazy loaded everything. The fix isn’t “lazy load less.” It’s “lazy load precisely.”

Here’s the short version:

  1. Never lazy load the LCP/hero image. Use fetchpriority="high" instead.
  2. Only lazy load images below the initial viewport. When in doubt about fold position, eager-load.
  3. Always include width and height on lazy-loaded images. No exceptions.
  4. Use native loading="lazy" by default. Reach for Intersection Observer only for carousels, tabs, or infinite scroll.
  5. Never combine native and JS-based lazy loading on the same element. Pick one.
  6. Audit with DevTools. Check your LCP element isn’t lazy-loaded. Check CLS sources for missing dimensions.

Lazy loading is a scalpel. Most developers use it like a sledgehammer. Two attributes on your hero image, loading="lazy" on everything else below the fold, dimensions on all of them — that’s the whole job. Now your LCP score can stop hating you.