# 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.

Updated 2026-09-25 · Speed & Core Web Vitals · HTML version: https://getreport.app/guides/lazy-loading-done-right

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](https://getreport.app/tools/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

> **Free tool:** [Image size checker](https://getreport.app/tools/image-size-checker): List every image on a page with its file size, format, real pixel dimensions and the size it is displayed at. Find the 2 MB hero, the 1600-pixel thumbnail and the images with no width and height.

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](https://getreport.app/guides/img/lazy-loading-done-right/images.webp "The image weight finding counts every image the browser downloaded while loading the page, before any scrolling.")

> **Check: Total image weight.** Images are usually the heaviest part of a page. Over 1.5 MB in total, or a single image over 500 KB, adds seconds on mobile and costs visitors real data.
>
> 1. Resize images to the size they are shown at (a 400 px wide photo does not need a 4000 px file).
> 2. Convert to WebP or AVIF and compress to about 80 % quality; CMS plugins and CDNs can do this automatically.

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:

> **Check: Offscreen images are lazy-loaded.** Images far below the fold compete for bandwidth with the ones visitors see first. Lazy loading defers them until they scroll into view.
>
> 1. Add loading="lazy" to images below the fold — but never to the hero/LCP image.
> 2. WordPress adds it by default since 5.5; check your theme or slider has not removed it.

> **Check: Every image declares width and height.** Without width and height the browser cannot reserve space, so the text jumps when each image arrives. That is the most common cause of a poor Cumulative Layout Shift score.
>
> 1. Add width and height attributes with the image's real proportions; CSS can still scale it.
> 2. In WordPress, recent versions add them automatically for images inserted through the editor; hard-coded theme images need them by hand.

And the one to watch whenever you change lazy loading:

> **Check: Largest Contentful Paint.** LCP is when the biggest thing on screen — usually the hero image or headline — finishes loading. Visitors judge "is this site slow?" on it, and Google uses it for ranking.
>
> 1. Find the LCP element (named in your report) and make it lighter, earlier or both — a compressed WebP/AVIF, sized to the screen, without lazy loading.
> 2. Preload it with <link rel="preload" as="image"> or add fetchpriority="high" on the <img>.
> 3. Cut what comes before it — render-blocking CSS/JS and slow server responses push LCP back.

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="https://getreport.app/img/hero-1200.webp" fetchpriority="high"
     alt="Autumn collection on a wooden table" width="1200" height="600">

<!-- Further down the page: lazy -->
<img src="https://getreport.app/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="https://getreport.app/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](https://getreport.app/learn/lazy-loading) 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](https://getreport.app/guides/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 script | No | Yes, and the images wait for it |
| Browser can see the image early | Yes, `src` is in the HTML | No, the address is hidden in `data-src` |
| Works if the script fails | Yes | No: the images never load |
| Reserves space | With `width`/`height` | Only 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:

```js
// 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](https://getreport.app/guides/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](https://getreport.app/tools/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.
