---
title: "Lazy Loading Images: How loading=lazy Works and When to Use It"
description: Learn how lazy loading images works, when to use loading=lazy, and how lazy loading placeholders improve performance.
---

[Content Delivery Network Blog](https://blog.blazingcdn.com/en-us)

# [Lazy Loading Images: How loading=lazy Works and When to Use It](https://blog.blazingcdn.com/en-us/lazy-loading-images-how-loading-lazy-works-and-when-to-use-it)

 Written by [Vadym Borovskyi](https://blog.blazingcdn.com/en-us/author/blazingcdn) | Nov 9, 2026, 3:10:40 PM

Native lazy loading defers an off-screen image or iframe until the user scrolls near it. For lazy loading images, the rule that holds up in production is to lazy load everything below the first viewport and never the Largest Contentful Paint (LCP) element. Applied to a hero image, loading="lazy" typically adds 300 to 500 ms to LCP, because the fetch cannot start until the browser has finished layout.

Since a 2020 change, Chromium-based browsers start fetching an image marked loading="lazy" once it is within 1,250 pixels of the viewport on fast connections and 2,500 pixels on 3G or slower. Firefox and Safari use their own, tighter margins, so the same page requests below-the-fold images at different scroll depths in each engine.

## **How loading=lazy decides when to fetch an image**

The attribute is a hint with one precondition: the browser has to know where the element sits relative to the viewport. That information only exists after style and layout, and layout usually waits for render-blocking CSS. An eager image found by the preload scanner can be requested within milliseconds of the HTML arriving. A lazy image is registered with an internal intersection observer and requested only when its box crosses the engine's distance threshold.

Three implementation details matter in practice:

- **Scripting must be enabled.** The HTML standard tells browsers to load lazy images eagerly when scripting is disabled. Without that rule, scroll-triggered fetches could track scroll position without JavaScript.
- **Dimensions drive the distance check.** An image with no width and height attributes and no CSS aspect ratio lays out at 0 by 0 pixels. That causes Cumulative Layout Shift (CLS) when the image arrives, and it skews the distance calculation for every element after it.
- **Hidden elements never load.** Images inside display:none containers, such as collapsed tabs or inactive carousel slides, never intersect, so they are never requested until they become visible.

### Iframes: same attribute, larger payoff

loading="lazy" on iframes is supported in all major engines as of early 2024. The savings per element are usually much larger than for images. A typical third-party video or map embed pulls several hundred kilobytes of script plus its own connections, and a lazy iframe defers all of it. A facade (a static thumbnail that swaps in the real embed on click) defers it further, at the cost of one extra interaction before playback starts.

## **When lazy loading images hurts LCP**

Lazy loading hurts LCP whenever the LCP element is a lazy image. The request is held until layout completes, so the time between Time to First Byte (TTFB) and the image request, which Chrome reports as "resource load delay", grows by however long render-blocking CSS and layout take. That is often 200 to 600 ms on mobile.

The worked example below uses round numbers. It is illustrative arithmetic, not a measurement:

- TTFB is 400 ms. The preload scanner finds an eager hero at about 450 ms. A 250 ms download finishes at 700 ms, and the hero paints at about 750 ms.
- With loading="lazy", the same image waits for the stylesheet (finished at 900 ms) and first layout (950 ms). The same 250 ms download then ends at 1,200 ms, and the hero paints at about 1,250 ms.
- The difference is 500 ms, a 67% worse LCP for zero bytes saved, because the hero was always going to be fetched.

This mistake is common. Public crawl data from 2021 to 2024 has repeatedly put the share of mobile pages that lazy load their own LCP image at roughly 10 to 18 percent. The usual causes are CMS defaults and templates that apply the attribute to every image in the content.

## **Decision matrix: lazy, eager or fetchpriority=high**

| Element position | loading | fetchpriority | Reason |
| --- | --- | --- | --- |
| LCP candidate (hero, product shot) | eager (omit the attribute) | high | Removes the 200 to 600 ms layout wait and jumps the request queue |
| Other images in the first viewport | eager | auto | Visible on load anyway; lazy only adds latency |
| Within about 1 screen below the fold | lazy | auto | Fetched almost immediately in Chromium, but stays off the critical path |
| Deep content, comments, footers | lazy | low or auto | Largest byte savings for readers who never scroll |
| Third-party embeds (video, maps) | lazy iframe or facade | n/a | Defers hundreds of kilobytes of script and extra connections |

The position of the element in the first rendered viewport, not its position in the HTML, decides whether lazy loading helps or hurts.

Responsive layouts make the rule harder to apply. The third card in a grid can be above the fold on desktop and two screens down on mobile. Templates should mark the first N items eager per breakpoint, or simply the first two or three content images. Marking nothing eager and relying on the browser to sort it out is the version that fails.

## **Lazy loading placeholder techniques that do not cost LCP or CLS**

A lazy loading placeholder fills the reserved box until the real image arrives. Its job is to stop layout shift and soften the blank flash during a fast scroll, without adding bytes or main-thread work. The box size comes from width and height attributes or CSS aspect-ratio. The placeholder only decides what fills that box.

- **Dominant color.** A single background color taken from the image at build time. It costs a few bytes of CSS, needs no decode, and is the default worth reaching for.
- **Low-quality image placeholder (LQIP).** A 16 to 32 pixel wide version, typically 200 to 600 bytes, inlined as a data URI and upscaled with a blur. Since Chrome 112 (2023), low-entropy images below about 0.05 bits per pixel are excluded as LCP candidates, so a blurred LQIP does not "win" LCP early and mask a slow real image.
- **Hash placeholders (BlurHash, ThumbHash).** A 20 to 30 character string decoded into pixels on the client. They are compact in JSON APIs, but they need JavaScript and a decode step. They fit app shells better than server-rendered pages.
- **Traced vector outlines.** Visually distinctive, but often 1 to 3 KB per image. That is a lot of inline markup for a long listing page.

Avoid any placeholder scheme that swaps the real URL in with script after page load. It hides the image from the preload scanner and from crawlers that do not scroll, and it duplicates what the native attribute already does.

## **How to combine lazy load images with an image CDN**

Lazy loading decides when an image is fetched. An image CDN decides how many bytes that fetch costs. The two compound. A lazy image served at the wrong width still wastes bandwidth when it does load, and a perfectly sized eager image still competes with the hero.

The pattern that works, described attribute by attribute:

1. Point srcset at CDN transform URLs on a fixed width ladder, for example 320, 640, 960, 1280 and 1920 pixels. Arbitrary widths fragment the cache: every unique width is a separate object and a separate origin miss.
2. For lazy images, set sizes to auto. Chromium-based browsers have supported this since 2024. Because a lazy image is only fetched after layout, the browser already knows the rendered width and picks the right ladder step without a hand-written sizes expression.
3. Keep explicit width and height attributes on every image so CLS stays at zero regardless of which variant arrives.
4. Negotiate AVIF or WebP from the Accept request header and return Vary: Accept, or encode the format in the URL path. Returning a negotiated format without Vary is a cache-poisoning bug, not an optimization.

The byte math explains why both halves matter. Take an article page with 25 images averaging 80 KB, where a typical reader scrolls only far enough to trigger the first 12. With lazy loading, the other 13 images (about 1.04 MB) are never requested for that reader. At 5 million such views a month, that is about 5.2 TB of egress avoided. Correct widths from the CDN then shrink the 12 images that do load.

If your delivery layer is BlazingCDN, crop, resize, resizefit and resizefill run on the fly at no extra charge, so a whole width ladder can come from one master file. The [**BlazingCDN features for image delivery and origin shield**](https://blazingcdn.com/features/) describe how origin shield and request coalescing keep first requests for new variants off your origin.

## **Failure modes and observability gaps**

- **Fast scrolls on slow links.** Tighter thresholds in Firefox and Safari mean users who flick-scroll on a 4G connection see placeholders for 300 ms or more. Dominant-color placeholders make this tolerable. Empty boxes do not.
- **Carousels and tabs.** Slides in an overflow-hidden track only load if the engine treats them as near the viewport. Mark the first slide eager and accept that later slides may load on demand.
- **Double lazy loading.** A JavaScript lazy-load library layered on top of the native attribute can delay images twice: the script waits for its own observer, then the browser applies its threshold.
- **Lab tools miss the regression.** A lab run on a fast desktop profile may never surface a lazy LCP image whose cost appears only on mid-range mobile devices. Field data with LCP attribution is the only reliable signal.
- **CDN logs undercount.** Lazy images that were never requested leave no trace. Request counts per page view will fall after rollout, so do not mistake that for a cache problem.

## **Verify this on your own stack**

In the Chrome DevTools Performance panel, open the LCP marker and check two things: the element's loading attribute, and the "resource load delay" subpart. Anything above about 100 ms on an image LCP is a lazy-loading or discovery problem. In real user monitoring, segment LCP by whether the attributed element carried loading="lazy". On the CDN side, confirm that variant URLs return a cache hit status and that format-negotiated responses carry Vary: Accept.

## **Who should lazy load everything, and who should not**

Long, image-heavy pages such as feeds, catalogs, galleries and documentation gain the most. On those pages, 50% or more of image bytes can sit below the median scroll depth. Single-screen landing pages gain almost nothing and risk LCP regressions, so leave their images eager and spend the effort on fetchpriority and correct widths. Pages whose LCP element changes per breakpoint need per-breakpoint eager rules, not a global default.

## **FAQ: lazy loading images in practice**

### Does loading=lazy hurt LCP?

Yes, when it is applied to the LCP image itself. A lazy image waits for render-blocking CSS and layout before its request starts, which commonly adds 200 to 600 ms of resource load delay on mobile. Below-the-fold images marked lazy do not hurt LCP, and they can help by freeing bandwidth for the hero image.

### Should I lazy load images above the fold?

No. Images visible in the first viewport should load eagerly, and the LCP candidate should also carry fetchpriority=high. Lazy loading above-the-fold images saves no bytes, because they are fetched immediately anyway, and it adds a layout-dependent delay. On responsive sites, decide which images count as above the fold separately for each breakpoint.

### What is the best lazy loading placeholder for performance?

A dominant-color background is the best default placeholder for lazy loading images. It costs a few bytes, needs no decode and no JavaScript. Blurred low-quality placeholders of 200 to 600 bytes look better, and Chrome has excluded such low-entropy images from LCP since 2023. Hash-based placeholders suit client-rendered apps that can afford the decode step.

### Does Google index lazy loaded images?

Yes, Google indexes images that use native loading="lazy", because its renderer handles the attribute. Problems arise with script-based lazy loading that keeps the real URL out of the src or srcset attributes until a scroll event fires. A crawler that does not scroll may never see those images, so prefer the native attribute.

### Does loading=lazy work on iframes?

Yes. As of early 2024, all major browser engines support loading="lazy" on iframes. It is often the larger win, because video and map embeds pull several hundred kilobytes of script and open extra connections. For heavy embeds, a click-to-load facade defers even more, at the cost of one extra interaction before playback.

## **Audit your LCP image and image widths this week**

Pull a week of field LCP data and split it by template and by whether the LCP element carried loading="lazy". For every template where it did, remove the attribute, add fetchpriority=high, and compare p75 LCP a week later. Then sample 100 image requests from your CDN logs and check how often the delivered width exceeds the rendered width by more than 1.5 times. That ratio is the bandwidth a width ladder would recover.

If you want those variants generated at the edge, the [**BlazingCDN on-the-fly image processing**](https://blazingcdn.com/image-processing/) page documents the included crop and resize operations, so you can try a width ladder against your own images.

[View full post](https://blog.blazingcdn.com/en-us/lazy-loading-images-how-loading-lazy-works-and-when-to-use-it)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Vadym Borovskyi"
  },
  "dateModified" : "2026-11-09T15:10:40.550Z",
  "datePublished" : "2026-11-09T15:10:40Z",
  "headline" : "Lazy Loading Images: How loading=lazy Works and When to Use It",
  "image" : {
    "@type" : "ImageObject",
    "height" : 768,
    "url" : "https://143144902.fs1.hubspotusercontent-eu1.net/hubfs/143144902/Gemini-Blog/image-Nov-09-2026-03-08-21-4821-PM.jpeg",
    "width" : 1408
  },
  "mainEntityOfPage" : "https://blog.blazingcdn.com/en-us/lazy-loading-images-how-loading-lazy-works-and-when-to-use-it",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60.0,
      "url" : "https://143144902.fs1.hubspotusercontent-eu1.net/hubfs/143144902/Logo%20b-3.png",
      "width" : 245.85367
    },
    "name" : "CDN Blog"
  }
}
```