Web Performance

What Is Lazy Loading and How to Use It

Lazy loading defers offscreen resources until needed. What it is, how to use native loading="lazy", when not to lazy-load, and avoiding any layout shift.

StackOptic Research Team16 May 202610 min read
Lazy loading — deferring offscreen images and iframes until they are needed

Most pages download far more than the visitor ever sees. Images sit metres below the fold, embeds load in sections nobody scrolls to, and all of it competes for bandwidth during the critical first load. Lazy loading fixes that by deferring offscreen resources until they are actually needed. In short: lazy loading delays the download of non-critical, offscreen content — images, iframes, sometimes scripts — until the user scrolls near it, which makes the initial load lighter and faster; the easiest method is the native loading="lazy" attribute, and the one rule you must not break is never to lazy-load your LCP image. This guide explains what lazy loading is, the right and wrong ways to use it, and how to avoid the layout-shift trap that catches people out.

It is one of the levers covered in how to make your website load faster, expanded into a full guide here.

What lazy loading is

When a browser loads a page, it normally tries to fetch every resource the HTML references — every image, every iframe, every embed — regardless of whether the visitor will ever see it. On a long article with twenty images, the browser may download all twenty during the initial load even though the reader only sees the first two before deciding whether to stay. That is wasted bandwidth, wasted time, and wasted energy.

Lazy loading changes the timing. Instead of fetching everything upfront, the browser loads only what is needed for the initial view and defers the rest, fetching each offscreen resource just before the user scrolls to it. The visible content arrives faster because it is not competing with offscreen images for the network, and visitors who never reach the bottom of the page never pay to download what is down there. The opposite of lazy loading is eager loading — fetching immediately — which is the browser's default and the correct choice for anything in the initial viewport.

Why it helps performance

Deferring offscreen resources improves a page in several concrete ways. The initial load is lighter, because fewer bytes must arrive before the page is usable. The critical resources arrive sooner, because they are not queued behind images the user cannot yet see. Bandwidth is saved for visitors who bounce or read only the top of the page. And on data-capped mobile connections, users are not charged for content they never view. Because the largest above-the-fold element renders without competing against a queue of offscreen downloads, lazy loading can indirectly help Largest Contentful Paint — provided, crucially, that you do not lazy-load the LCP element itself.

The easy way: native loading="lazy"

For years, lazy loading required JavaScript libraries. Now the browser does it for you. Adding the loading="lazy" attribute to an <img> or <iframe> tells the browser to defer that resource until it nears the viewport — no JavaScript, no library, no configuration:

<img src="photo.jpg" width="800" height="600" loading="lazy" alt="...">
<iframe src="https://www.youtube.com/embed/..." loading="lazy"></iframe>

The attribute accepts three values, and knowing the difference matters:

  • lazy — defer until the resource is near the viewport. Use for offscreen images and iframes.
  • eager — load immediately, the default behaviour. Use (or simply omit the attribute) for above-the-fold content.
  • auto — let the browser decide; rarely needed and inconsistently treated, so prefer an explicit choice.

Native lazy loading is supported across modern browsers, and in browsers that do not understand the attribute it is simply ignored, so the image loads normally — meaning there is no downside to adding it as a progressive enhancement.

The one rule you must not break: don't lazy-load the LCP image

This is the single most common lazy-loading mistake, and it is worth stating bluntly: never apply loading="lazy" to your above-the-fold hero image or any element that is likely to be your Largest Contentful Paint.

The logic is straightforward once you see it. LCP measures how quickly the largest visible element renders. If that element is your hero image and you lazy-load it, you are explicitly telling the browser to delay fetching the very thing the metric is timing — so your LCP gets worse, not better. The browser may even wait for layout to settle before it decides the lazy image is in view, adding further delay.

The correct pattern is the inverse: eager-load the LCP image (omit loading="lazy", or set loading="eager"), and ideally go further by preloading it and adding fetchpriority="high" so the browser fetches it as early and as urgently as possible. Then lazy-load everything below it. A useful mental model: the first screenful is eager, everything offscreen is lazy. For the full picture on what drives LCP, see what is Largest Contentful Paint and how to improve it.

Resource-by-resource: what to lazy-load and how

Not everything is lazy-loaded the same way. This table maps common resources to the right approach.

ResourceApproachNotes
Above-the-fold image (LCP)Eager + fetchpriority="high" + preloadNever lazy; it is the metric being timed
Below-the-fold imagesNative loading="lazy"Set width/height to avoid CLS
Iframes (video, maps, embeds)Native loading="lazy"Heavy; great lazy-load candidates
Background images (CSS)IntersectionObserverNative attribute does not cover CSS backgrounds
Videopreload="none" + poster imageDefer the video file; show a lightweight poster
Non-critical scripts/widgetsIntersectionObserver or on-interactionLoad a chat widget or map when needed, not upfront
Offscreen components (SPA)Framework lazy-load + code-splittingDefer both the component and its JavaScript

The recurring theme is that iframes and video are especially worth deferring, because a single embedded video player or map can pull in hundreds of kilobytes of its own scripts and assets — often more than the rest of the page combined.

Lazy-loading video the smart way

Video is the heaviest media most pages carry, so deferring it pays off handsomely. Two techniques work together. First, set preload="none" on a <video> element so the browser does not begin downloading the video data until the user presses play; pair it with a poster image so the player still shows something attractive immediately at a tiny fraction of the weight. Second, for embedded third-party players (YouTube, Vimeo), use a facade pattern: show a static thumbnail with a play button, and only load the heavy embed iframe when the user actually clicks it. This avoids downloading the player's substantial JavaScript on every page view when most visitors never press play. The poster-and-facade combination can remove an enormous amount of weight from pages that feature video prominently.

The other essential rule: reserve space to avoid CLS

Lazy loading and layout shift are intimately connected, and getting this wrong undoes much of the benefit. When a lazy-loaded image arrives, it needs somewhere to go. If you declared no dimensions, the browser reserved no space, so the image pushes surrounding content out of the way as it loads — a jarring jump that registers as Cumulative Layout Shift.

The fix is non-negotiable: always set width and height attributes (or a CSS aspect-ratio) on every lazy-loaded image and iframe. With dimensions declared, the browser computes the aspect ratio and reserves exactly the right space before the file arrives, so when it loads it slots into a gap that already existed and nothing moves. This is doubly important for lazy-loaded content, because by definition it arrives after the user has started reading — the worst possible moment for things to shift. Reserving space turns lazy loading from a potential CLS hazard into a clean win.

When native isn't enough: IntersectionObserver

Native loading="lazy" covers images and iframes, but not CSS background images, scripts, or custom components. For those, the modern foundation is the IntersectionObserver API — a browser feature that efficiently reports when an element enters or leaves the viewport, without the jank of old scroll-event listeners that fired hundreds of times a second.

The pattern is consistent: you observe a placeholder element, and when it crosses a threshold near the viewport, a callback fires where you do the real work — swap a data-src into src, load a script, initialise a map, or render a deferred component. You can configure a rootMargin so loading begins slightly before the element is visible (for example, 200px early), which means the content is usually ready by the time the user actually reaches it, avoiding a visible pop-in. Most framework lazy-loading features and lazy-load libraries are built on IntersectionObserver under the hood, so even if you never write it directly, it is what powers the behaviour.

Lazy-loading in modern frameworks

If you build with a component framework, much of this is handled for you, and it is worth using the built-in tools rather than reinventing them. Framework image components (such as those in Next.js and similar frameworks) typically lazy-load offscreen images, set dimensions to prevent layout shift, and serve modern formats automatically — wrapping several best practices into one component. Frameworks also offer dynamic imports and lazy component loading, which defer not just an image but the component's JavaScript too, combining lazy loading with code-splitting to reduce JavaScript. The principle to carry over is the same regardless of framework: defer what is offscreen, eager-load what is critical, and always reserve space.

How to check what is (and isn't) lazy-loaded

You do not have to guess whether lazy loading is working. A short verification routine:

  1. Open Chrome DevTools → Network panel, filter to Img, and reload the page. Note which images load immediately. Then scroll slowly and watch new image requests fire as you approach them — that is lazy loading in action. Images that load upfront despite being far down the page are not being deferred.
  2. Use the Performance panel or PageSpeed Insights to confirm your LCP element loads early and is not deferred. Lighthouse will flag if your LCP image carries loading="lazy".
  3. Inspect individual images in the Elements panel to confirm offscreen ones carry loading="lazy" and that all of them have width and height set.
  4. Throttle the network (e.g. "Fast 3G") in DevTools and reload — on a slow connection, the difference between an eager and a lazy page is dramatic and easy to feel.

This is also exactly the kind of thing a broader site audit surfaces: StackOptic, for instance, reports on image handling and offscreen-resource issues alongside the rest of a page's performance, so you can see at a glance whether deferral is being used well.

Common mistakes

  • Lazy-loading the hero / LCP image, delaying the main content the metric times — the cardinal sin.
  • Omitting width and height on lazy images, so they cause layout shift when they pop in.
  • Lazy-loading everything indiscriminately, including above-the-fold content that should load immediately.
  • Setting no rootMargin in a custom observer, so images visibly pop in late instead of loading just ahead of view.
  • Forgetting iframes and video, which are often the heaviest offscreen resources and the best candidates for deferral.

A quick checklist

  • Eager-load the above-the-fold / LCP image; add fetchpriority="high" and consider a preload.
  • Add loading="lazy" to every below-the-fold image and iframe.
  • Set width and height (or aspect-ratio) on all media to prevent CLS.
  • Use a facade or poster for video so the heavy player loads only on demand.
  • For backgrounds, scripts and components, use IntersectionObserver with a sensible rootMargin.
  • Verify in the DevTools Network panel that offscreen images load on scroll, not upfront.

Why it is worth doing

Lazy loading is one of those rare optimisations that is cheap to apply and broadly beneficial. A single attribute defers a whole category of resources, the initial load gets lighter, bandwidth is saved, and — done with dimensions in place — there is no downside. It pairs naturally with image optimisation and JavaScript reduction as part of cutting page weight, and like most performance work it also trims the energy a page consumes. The only real risk is the LCP mistake, and once you internalise "first screen eager, everything else lazy," even that is easy to avoid. Apply it carefully and it quietly makes nearly every page faster, especially the long, media-rich ones where it matters most.

Go deeper

Want to see whether your site defers offscreen resources well, alongside performance, SEO and security? Analyse any URL with StackOptic — free, no sign-up.

Frequently asked questions

What is lazy loading?

Lazy loading is a performance technique that defers loading non-critical resources until they are actually needed — usually when the user scrolls them into view. Instead of downloading every image, iframe and embed on the initial page load, the browser fetches offscreen content only as the user approaches it. This makes the first load lighter and faster, saves bandwidth for visitors who never scroll to the bottom, and reduces work for both the network and the device.

How do I lazy-load images?

The easiest way is the native loading attribute: add loading="lazy" to an img or iframe element and the browser defers the download until it nears the viewport, with no JavaScript required. It is supported across modern browsers. For background images, scripts or custom components that the native attribute does not cover, use the IntersectionObserver API to detect when an element approaches the viewport and trigger loading yourself. Always set width and height to prevent layout shift.

Should I lazy-load all images?

No. Lazy-load images below the fold, but never lazy-load your above-the-fold or Largest Contentful Paint image. Applying loading="lazy" to the hero image tells the browser to delay the very element LCP is timing, which makes your loading score worse, not better. The rule is simple: eager-load (and ideally preload with high fetch priority) the main above-the-fold image, and lazy-load everything offscreen below it.

Does lazy loading cause layout shift?

It can, if you forget to reserve space. When a lazy-loaded image has no declared dimensions, the browser holds no room for it, so the page jumps when it finally arrives — hurting Cumulative Layout Shift. The fix is to always set width and height attributes (or a CSS aspect-ratio) on every lazy-loaded image and iframe, so the browser reserves the correct space in advance and nothing moves when the content loads.

What is IntersectionObserver?

IntersectionObserver is a browser API that efficiently tells you when an element enters or leaves the viewport, without the performance cost of older scroll-listener approaches. It is the standard foundation for custom lazy loading: you observe a placeholder element, and when it nears the viewport the observer fires a callback where you swap in the real image, load a script, or initialise a component. It powers most JavaScript lazy-loading patterns and many framework lazy-load features.

Analyse any website with StackOptic

Get the full technology stack, performance, security and SEO report in seconds — free.

Analyse a website

Related articles