Skip to content

SpeedPart of: Image optimisation

How to lazy load images (and iframes) without hurting LCP

To lazy load images, add loading="lazy" to every image and iframe below the first screen, never to the hero, and give each one width and height. What to lazy-load, what never to, and how WordPress and plugins handle it.

getReport teamUpdated 27 Sept 202612 min read

To lazy load images, add loading="lazy" to every <img> and <iframe> below the first screen: the browser then skips them until the visitor scrolls near them, with no JavaScript library needed. Never put it on the hero or anything visible without scrolling, because that delays your Largest Contentful Paint, and always give lazy images width and height so nothing jumps when they arrive. WordPress does most of this automatically; plugins are where it usually goes wrong. The pillar guide, image optimization for the web, covers the rest of what makes images fast.

This guide shows what to lazy-load and what not, the HTML for images, <picture> and iframes, how to handle CSS backgrounds, and how to check what WordPress, your theme and your plugins actually do.

Quick answer

  • Add loading="lazy" to <img> and <iframe> elements below the first screen. 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.
  • decoding="async" is a harmless extra; it does not replace lazy loading.
  • WordPress adds loading="lazy" automatically and keeps the first images eager; check that the theme and plugins have not overridden that.
  • Check with the image size checker: its 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 easily passes 3 MB, and a visitor who reads the first screen and leaves never sees most of it. Without lazy loading their phone downloads it all anyway, and those downloads compete with the images and scripts the first screen needs.

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 it adds a delay to LCP, which is why a 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 deferred by a JavaScript library also show "eager", 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, properly lazy-loaded images 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.

This finding is Lighthouse's "Defer offscreen images" audit, read from the PageSpeed Insights run: it lists images outside the first screen that loaded early, with the bytes lazy loading would save. Lighthouse 13 dropped this audit without a replacement, so once PageSpeed Insights runs version 13 the finding no longer appears and the image weight finding above and the Loading column are the checks to use.

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

How to lazy load images, step by step

1. Decide what is above the fold

"Above the fold" means visible without scrolling on the device that matters most, 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 load normally; everything else is a candidate. 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 about 1,250 px of the viewport on a fast connection and 2,500 px on a slow one (web.dev, "Browser-level image lazy loading"), so a visitor scrolling normally 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 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 has no content 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 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 best image size for a website covers the export sizes that go with them.

5. 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. Native lazy loading is better in almost every case:

Native loading="lazy"JavaScript library
Needs a scriptNoYes, and the images wait for it
Browser sees the image earlyYes, src is in the HTMLNo, the address hides 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 CSS background images, which have no loading attribute. Where you can, turn them into <img> elements with object-fit: cover. For the rest, an IntersectionObserver that adds a class near the viewport is enough:

JavaScript
// Load backgrounds for .lazy-bg sections as they approach 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"); }

6. Make the lazy images light too

Lazy loading postpones bytes; it does not remove them. A visitor who scrolls the whole page still downloads every image, so resize and convert them as well, and offer sizes with srcset and sizes.

WordPress lazy load: what core does and where it breaks

WordPress core handles most of this without a plugin:

  • Since 5.5 it adds loading="lazy" to images in post content, excerpts and widgets, with width and height where it knows the size. Since 5.7 iframes get it too.
  • Since 5.9 it leaves the first content image eager, so the likely hero is not lazy-loaded. Since 6.3 it keeps the first few images eager and adds fetchpriority="high" to the image it expects to be the LCP element.
  • 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. The hero's Loading column in the image size checker should say "eager".
  • Optimisation plugins (WP Rocket, LiteSpeed Cache, Smush, Perfmatters and others) often replace core lazy loading with their own JavaScript version that catches 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. Two lazy-load systems at once is a common source of late or missing images.
  • Page builders and sliders sometimes lazy-load slides and background sections. Disable it for the first section.

To change how many content images core leaves eager, add a filter to your child theme's functions.php:

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

On Shopify, the image_tag filter takes a loading parameter; the first section's image should use loading: 'eager' and the rest 'lazy'.

How to verify

  • In the image size checker, the hero and logo show "eager"; images further down show "lazy".
  • The image weight finding shows a lower total, 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. LCP gets worse after enabling a lazy-load plugin. Exclude the first images, or let WordPress core handle it.
  • Lazy images without dimensions. Text jumps as you scroll and CLS rises. Add width and height to every image.
  • A 1×1 placeholder in src and the real image in data-src. Blank images when a script fails, and a slower LCP. Switch to native loading="lazy" with the real src.
  • Two lazy-load systems. Images appear late, twice or not at all. Keep one, preferably core.
  • Treating lazy loading as image optimisation. A fast first screen, but a 12 MB page for anyone who scrolls. Resize and convert too.

Questions people ask

How do I lazy load images in WordPress?

You usually do not have to: since version 5.5 WordPress adds loading="lazy" to images in posts, pages and widgets, and since 5.9 it keeps the first content image eager so the hero loads straight away. Check the result with an image checker. If the hero shows as lazy, a theme template or an optimisation plugin is overriding core; exclude that image in the plugin's settings or turn the plugin's lazy loading off.

Should I lazy load images above the fold?

No. Images visible without scrolling, especially the hero, should load as early as possible. A lazy image waits until the browser has laid out the page before it starts downloading, which delays the Largest Contentful Paint by hundreds of milliseconds. Load those images normally and give the main one fetchpriority="high"; use loading="lazy" only for images further down the page.

What does "defer offscreen images" mean?

It is Lighthouse's advice to lazy-load images that are outside the first screen when the page loads. The audit lists images the browser downloaded early although nobody could see them yet, with the kilobytes lazy loading would save. Add loading="lazy" and width and height to those images. Lighthouse 13 removed this audit, but the advice itself still applies.

Do I still need a lazy load plugin?

Usually not. Native loading="lazy" works in every current major browser and WordPress adds it automatically, so a plugin mostly adds JavaScript and the risk of lazy-loading the hero. A plugin or small script is still useful for CSS background images, which have no loading attribute, and for replacing heavy video embeds with a click-to-load facade.

Can Google see lazy-loaded images?

Yes, when you use native loading="lazy" with the real address in src or srcset: Googlebot renders the page and reads the image URLs from the HTML. Problems come from JavaScript libraries that hide the address in data-src until a scroll event that Googlebot never triggers. Check with the URL Inspection tool in Search Console that the rendered HTML contains the real image URLs.

Check your site before and after Check