Skip to content

Speed

Lazy loading images and iframes done right (without hurting LCP)

Lazy loading saves bandwidth on long pages, but on the wrong image it makes LCP slower. Learn what to lazy-load, what never to, why dimensions matter, and how WordPress and plugins handle it for you.

getReport teamUpdated 25 Sept 202610 min read

Lazy loading tells the browser to skip images and iframes that are far down the page until the visitor scrolls near them. On a long category page with 40 product photos, that can cut the initial download by megabytes. On the hero image, the same attribute makes the page slower, because the most important image now waits. This guide shows how to lazy-load the right things with one HTML attribute, what to leave alone, and how to check what WordPress and your plugins actually do.

Quick answer

  • Add loading="lazy" to <img> and <iframe> elements below the first screen. No JavaScript library needed; every current major browser supports it.
  • Never lazy-load the LCP image or anything visible without scrolling. Give the hero fetchpriority="high" instead.
  • Always set width and height on lazy images so the space is reserved and nothing jumps when they arrive.
  • decoding="async" is a harmless extra for images; it does not replace lazy loading.
  • WordPress adds loading="lazy" automatically and skips the first images; check that your theme and plugins have not overridden it.
  • Check the result with the image size checker: the Loading column shows the attribute for every image.

Why lazy loading matters

Images are usually most of a page's weight. A blog post with 15 screenshots or a product grid with 48 photos can easily pass 3 MB, and a visitor who reads the first screen and leaves never sees 90 % of it. Without lazy loading, their phone downloads all of it anyway, and those downloads compete with the images and scripts the first screen needs.

The benefit is bandwidth and early-load competition. The risk is priority: a lazy image starts downloading only after the browser has laid out the page and worked out that the image is near the viewport. For images further down, that is exactly what you want. For the hero image, it adds a delay to your Largest Contentful Paint, which is why "lazy-loaded hero" is one of the most common causes of a failing LCP.

How getReport checks it

The image size checker reads every <img> in the HTML, including the data-src and srcset attributes lazy-load plugins use, and lists each image with its file size, format, real pixel size, displayed size and loading attribute. Images without a loading attribute show as "eager", the browser default. Images handled by a JavaScript lazy-load library also show "eager" there, because the library, not the attribute, does the deferring.

Chromium then loads the page without scrolling and counts every image it downloaded, including CSS backgrounds:

The image weight finding on a page with more than twenty heavy images: the total image weight well over the 1.5 MB threshold, the largest file named, and the fix steps
The image weight finding counts every image the browser downloaded while loading the page, before any scrolling.

Because the page is not scrolled, images that are properly lazy-loaded far below the fold are usually not requested and do not count. A high total on a long page is a strong hint that everything loads up front.

Lighthouse, in the speed module, estimates what lazy loading would save, and the HTML check flags images without dimensions:

And the one to watch whenever you change lazy loading:

If LCP got worse after you turned on a lazy-load plugin, the plugin almost certainly caught the hero.

Step by step

1. Decide what is above the fold

"Above the fold" means visible without scrolling on the device that matters most, which is usually a phone. On a typical page that is the logo, the hero image or first product photo, and perhaps the first row of a grid. Open the page on a phone (or in DevTools device mode at about 390 px wide) and note which images you can see.

Those images load normally. Everything else is a candidate for lazy loading. When in doubt about an image near the edge, load it eagerly: a few extra kilobytes cost less than a delayed first screen.

2. Add loading="lazy" below the fold

HTML
<!-- Hero: eager (the default) with high priority -->
<img src="/img/hero-1200.webp" fetchpriority="high"
     alt="Autumn collection on a wooden table" width="1200" height="600">

<!-- Further down the page: lazy -->
<img src="/img/lookbook-3.webp" loading="lazy" decoding="async"
     alt="Wool coat, side view" width="800" height="1000">

For <picture>, the attribute goes on the inner <img>:

HTML
<picture>
  <source srcset="/img/lookbook-4.avif" type="image/avif">
  <img src="/img/lookbook-4.webp" loading="lazy"
       alt="Scarf detail" width="800" height="1000">
</picture>

Browsers do not wait until the image is exactly on screen. Chrome starts loading lazy images when they come within roughly 1,250 px of the viewport on a fast connection (more on slow ones), so a visitor scrolling at a normal speed should not see empty boxes.

3. Lazy-load iframes too

Embedded videos, maps and social posts are among the heaviest things on a page: a single YouTube embed loads several hundred kilobytes of script before anyone presses play. The same attribute works on <iframe>:

HTML
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
        loading="lazy" title="Product demo video"
        width="560" height="315" allowfullscreen></iframe>

For an embed that is above the fold, a facade (a static thumbnail with a play button that loads the real player on click) saves even more. The lazy loading learn page has the short version.

4. Always set width and height

A lazy image is not in the layout until it loads. Without width and height, the browser reserves no space, and the content below jumps when the image arrives: a layout shift, counted in CLS. It also means the browser sees a column of zero-height images "near the viewport" and may load more of them than needed.

The attributes are the image's real pixel size (or the same proportions); CSS still controls the displayed size with max-width: 100%; height: auto;. The full walkthrough is in How to fix Cumulative Layout Shift.

5. Use decoding="async" as an extra, not a fix

decoding="async" tells the browser it may decode the image off the critical path instead of holding up the rest of the page's rendering. It is a hint with a small effect, safe on almost any image. It does not delay the download, so it is no substitute for lazy loading, and on the hero it gains little; leave it off there if you are unsure.

6. Prefer native lazy loading over JavaScript libraries

Before browsers supported loading="lazy", sites used scripts: the real address goes in data-src, and a script swaps it into src when the image scrolls into view. Those libraries still work, but native lazy loading is better in almost every case:

Native loading="lazy"JavaScript library
Needs a scriptNoYes, and the images wait for it
Browser can see the image earlyYes, src is in the HTMLNo, the address is hidden in data-src
Works if the script failsYesNo: the images never load
Reserves spaceWith width/heightOnly if the library keeps them

The one case for a script is lazy-loading CSS background images, which have no loading attribute. Where you can, turn such backgrounds into <img> elements with object-fit: cover. For the rest, an IntersectionObserver that adds a class when the section approaches the viewport is enough:

JavaScript
// Add to your main script: load backgrounds for .lazy-bg sections near the viewport
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');   // CSS sets the background here
      observer.unobserve(entry.target);
    }
  }
}, { rootMargin: '600px 0px' });

document.querySelectorAll('.lazy-bg').forEach((el) => observer.observe(el));
CSS
/* The background is only requested once .is-visible is added */
.lazy-bg.is-visible {
  background-image: url("/img/newsletter-bg.webp");
}

7. Make the lazy images light too

Lazy loading postpones the bytes; it does not remove them. A visitor who scrolls the whole page still downloads every image. Resize and convert them as well: Image sizes that do not hurt covers export sizes and formats.

Platform notes

WordPress

WordPress core handles most of this without a plugin:

  • Since 5.5, it adds loading="lazy" to images in post content, excerpts and widgets, and adds width and height where it knows the image size. Since 5.7, iframes get it too.
  • Since 5.9, it leaves the first image(s) in the content eager, so the likely hero is not lazy-loaded. Later versions refined this, and since 6.3 WordPress also adds fetchpriority="high" to the image it expects to be the LCP.
  • Since 6.1, images also get decoding="async".

Where it goes wrong:

  • Themes that print the hero or featured image outside the content, or hard-code loading="lazy" in a template. Check the hero in the image size checker: its Loading column should say "eager".
  • Optimisation plugins (WP Rocket, LiteSpeed Cache, Smush, Perfmatters and others) often replace core lazy loading with their own JavaScript version, which lazy-loads every image, including the first. Use their exclusion settings for the hero and logo, or turn their lazy loading off and let core do it. Running two lazy-load systems at once is a common source of broken or late images.
  • Page builders and sliders sometimes add their own lazy loading to slides and background sections. Disable it for the first section.

If you need to adjust how many content images core leaves eager, a filter in your child theme's functions.php does it:

PHP
// functions.php (child theme): keep the first 2 content images eager
add_filter('wp_omit_loading_attr_threshold', function () {
    return 2;
});

Shopify

Many current themes lazy-load images below the first section and load the first section eagerly. If a theme lazy-loads the hero, the setting is in the section's Liquid: the image_tag filter takes a loading parameter, so the first section's image should use loading: 'eager'.

Verify

  • In the image size checker, the hero and logo show "eager" in the Loading column; images further down show "lazy".
  • The image weight finding shows a lower total after the change, because images far below the fold are no longer loaded before scrolling.
  • The LCP finding in the speed test is no worse than before. If it is, the hero got caught.
  • In Chrome DevTools, Network panel, filter by Img and reload: only the first screen's images load until you scroll.

Common mistakes

  • Lazy-loading the hero. Symptom: LCP gets worse after enabling a lazy-load plugin. Fix: exclude the first images, or let WordPress core handle it.
  • Lazy images without dimensions. Symptom: text jumps as you scroll, CLS rises. Fix: add width and height to every image.
  • A 1×1 placeholder in src and the real image in data-src. Symptom: blank images when a script fails, and a slower LCP. Fix: switch to native loading="lazy" with the real src.
  • Two lazy-load systems. Symptom: images appear late or twice, or not at all in some browsers. Fix: keep one, preferably core.
  • Treating lazy loading as image optimisation. Symptom: fast first screen, but a 12 MB page for anyone who scrolls. Fix: resize and convert the images as well.
Check your site before and after Check