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.
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.
| Resource | Approach | Notes |
|---|---|---|
| Above-the-fold image (LCP) | Eager + fetchpriority="high" + preload | Never lazy; it is the metric being timed |
| Below-the-fold images | Native loading="lazy" | Set width/height to avoid CLS |
| Iframes (video, maps, embeds) | Native loading="lazy" | Heavy; great lazy-load candidates |
| Background images (CSS) | IntersectionObserver | Native attribute does not cover CSS backgrounds |
| Video | preload="none" + poster image | Defer the video file; show a lightweight poster |
| Non-critical scripts/widgets | IntersectionObserver or on-interaction | Load a chat widget or map when needed, not upfront |
| Offscreen components (SPA) | Framework lazy-load + code-splitting | Defer 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:
- 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.
- 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". - Inspect individual images in the Elements panel to confirm offscreen ones carry
loading="lazy"and that all of them havewidthandheightset. - 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
widthandheighton 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
rootMarginin 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
widthandheight(oraspect-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
- The big picture, in priority order: how to make your website load faster.
- The metric you must not delay: what is Largest Contentful Paint and how to improve it.
- Why dimensions matter: what is Cumulative Layout Shift and how to fix it.
- The image companion: how to optimize images for the web.
- Deferring scripts too: how to reduce JavaScript and speed up your site.
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 websiteRelated articles
How to Test Your Website Speed (Tools and Metrics)
Which tools to use, lab versus field data, and the metrics that matter. Test your site speed with PageSpeed Insights, Lighthouse, WebPageTest and DevTools.
How to Optimize CSS for Faster Pages
CSS is render-blocking, so bloated stylesheets delay your page. How to minify, cut unused CSS, inline critical CSS and defer the rest to speed up FCP and LCP.
What Are Gzip and Brotli Compression (and How to Enable Them)?
Gzip and Brotli shrink HTML, CSS and JavaScript before they are sent. What each is, how Brotli compares to Gzip, how to verify it, and how to enable it.