Lazy Loading Images and Videos: Complete Guide
Master lazy loading images and videos to cut page weight and speed up load. Native loading=lazy, Intersection Observer, framework code, and what NOT to defer.

Lazy loading is a performance technique where images, videos, and iframes load only when they're about to enter the viewport β instead of all at once when the page first renders. The simplest way to do it is the native loading="lazy" HTML attribute, which every modern browser supports with zero JavaScript. Done right, lazy loading shrinks initial page weight, speeds up your Largest Contentful Paint, and keeps off-screen media from competing for bandwidth your visible content needs.
Here's the catch most guides skip: lazy loading the wrong image β your hero or LCP element β actively damages your Core Web Vitals. This guide covers exactly what to defer, what to load eagerly, and how to verify both, with copy-paste code for every framework.
What Lazy Loading Actually Does
Without lazy loading, the browser downloads every image on the page as soon as the HTML parses. On a page with 30 images, that's 30 HTTP requests fired immediately β including images at the very bottom the user may never scroll to. All of them compete for the same connection pool and bandwidth, slowing down the content the user can actually see right now.
With lazy loading, the browser only fetches what's visible or about to be visible. Everything below the fold waits until the user scrolls near it. The result: faster first render, less wasted bandwidth, and lighter server load.
The principle is one sentence long: don't download what the user can't see yet.
This matters most on long, media-heavy pages β blog posts, product listings, image galleries, and infinite-scroll feeds β where a large share of images sit far below the initial viewport.

Why It Matters for Performance and SEO
Lazy loading touches every metric Google weighs in Core Web Vitals.
- Faster initial load. Deferring off-screen images frees up bandwidth and CPU for the content the user sees first. The visible portion of the page can render in a fraction of the time it takes to load everything.
- Lower bandwidth cost. Users on mobile data shouldn't pay β literally β for images they'll never scroll to. Every eagerly loaded off-screen image is wasted data.
- Better LCP. Largest Contentful Paint measures when the largest visible element finishes loading. When your hero image isn't fighting 30 other downloads for bandwidth, it loads sooner. Google's "good" LCP threshold is under 2.5 seconds.
- Fewer layout shifts. Lazy-loaded images with explicit
widthandheightlet the browser reserve correct space before the file arrives, preventing Cumulative Layout Shift as images pop in.
One honest caveat: lazy loading defers when images load β it doesn't make them smaller. Pair it with image optimization so the files you eventually load are already compressed and in a modern format.
Native Lazy Loading: Start Here
Modern browsers support lazy loading with a single attribute β no library, no JavaScript.
<!-- Below-the-fold image: defer until near viewport -->
<img
src="product-photo.webp"
alt="Red running shoes, side view"
width="800"
height="600"
loading="lazy"
/>
<!-- Above-the-fold / LCP image: load immediately -->
<img
src="hero-banner.webp"
alt="Summer collection hero banner"
width="1200"
height="400"
loading="eager"
fetchpriority="high"
/>
<!-- Iframes (YouTube, maps, widgets) support it too -->
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
width="560"
height="315"
loading="lazy"
title="Product demo video"
></iframe>The loading="lazy" attribute tells the browser to defer the fetch until the element is near the viewport. The browser decides how "near" based on connection speed and device β on slow networks it starts earlier to compensate, so images are usually ready by the time you scroll to them.
Browser support is effectively universal across Chrome, Edge, Firefox, Safari, and Opera. On the rare older browser that doesn't recognize the attribute, the image simply loads normally β there's no broken experience or fallback to write.
Non-negotiable rule: always set width and height (or a CSS aspect-ratio) on lazy-loaded images. Without dimensions, the browser can't reserve space, and content jumps when the image arrives β a classic CLS failure.
Native vs. JavaScript: pick the right tool
| Scenario | Best approach |
|---|---|
Standard <img> below the fold | Native loading="lazy" |
| Iframes (video, maps, embeds) | Native loading="lazy" |
| Hero / LCP image | loading="eager" + fetchpriority="high" |
CSS background-image | Intersection Observer |
| Custom fade-in / load tracking | Intersection Observer |
| Precise pre-load distance control | Intersection Observer |
For the vast majority of sites, native lazy loading is all you need. Reach for JavaScript only when you need behavior the browser doesn't expose.
JavaScript Intersection Observer for Fine Control
When you need to lazy load CSS background images, add custom animations, or track which images users actually reach, the Intersection Observer API gives you exact control over the trigger.
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
img.classList.add('loaded');
observer.unobserve(img); // stop watching once loaded
});
},
{
rootMargin: '200px 0px', // start 200px before viewport entry
threshold: 0.01,
}
);
lazyImages.forEach((img) => imageObserver.observe(img));
});The matching HTML stores the source in data-src so the browser doesn't fetch it on parse β your script swaps it to src when the image nears the viewport:
<img
data-src="product-photo.webp"
data-srcset="product-400.webp 400w, product-800.webp 800w"
alt="Blue wireless headphones"
width="800"
height="600"
class="lazy"
/>
<noscript>
<img src="product-photo.webp" alt="Blue wireless headphones" />
</noscript>rootMargin: '200px 0px' is the key tuning knob β it starts loading 200px before the image enters view, so images are typically ready by the time the user scrolls to them. Always include the <noscript> fallback so users (and crawlers) without JavaScript still get the image.

Lazy Loading Videos and Iframes
Embedded video is often the single heaviest thing on a page. One YouTube embed pulls in multiple scripts, stylesheets, and a preview image β hundreds of kilobytes β even if the user never clicks play.
The facade (lite-embed) pattern
Instead of loading the full iframe upfront, show a thumbnail plus a play button and only inject the real embed on click:
<div class="video-facade" data-video-id="VIDEO_ID">
<img
src="https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg"
alt="Product demo video thumbnail"
width="560"
height="315"
loading="lazy"
/>
<button class="play-button" aria-label="Play video">▶</button>
</div>
<script>
document.querySelectorAll('.video-facade').forEach((facade) => {
facade.addEventListener('click', () => {
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${facade.dataset.videoId}?autoplay=1`;
iframe.width = 560;
iframe.height = 315;
iframe.allow = 'autoplay; encrypted-media';
iframe.allowFullscreen = true;
facade.replaceWith(iframe);
});
});
</script>This removes the entire embed payload from initial load. Visitors who want the video get it with one click; everyone else never pays for it.
Self-hosted video
For <video> you control, use the preload attribute to stop the browser eating data upfront:
<video
src="demo.mp4"
preload="none"
poster="demo-poster.jpg"
controls
width="800"
height="450"
>
Your browser does not support the video tag.
</video>preload="none" downloads nothing until the user hits play. preload="metadata" fetches just duration and dimensions β useful when you want the scrubber populated. Reserve preload="auto" for cases where the video is the main reason the page exists.
Framework-Specific Lazy Loading
Every modern framework ships lazy loading. Use the built-in path rather than rolling your own.
Next.js (`next/image`) lazy loads by default and reserves layout space automatically:
import Image from 'next/image';
// Lazy loaded by default
<Image src="/products/shoes.webp" alt="Running shoes" width={800} height={600} />
// Above-fold hero: opt out of lazy, add preload hint
<Image
src="/hero-banner.webp"
alt="Summer sale hero banner"
width={1200}
height={400}
priority
/>The priority prop is the React equivalent of loading="eager" plus a preload hint β use it on your hero and any above-fold visual. (See the dedicated WordPress SEO guide and Shopify SEO checklist for platform-level image handling.)
WordPress adds loading="lazy" to images automatically since version 5.5, and skips it on the first in-content image (the likely LCP element) in newer versions. Use the wp_lazy_loading_enabled filter when you need finer control.
Vanilla React combines React.lazy() for component-level splitting with native loading="lazy" on the <img> tags inside those components.
What NOT to Lazy Load
This is where most performance gains turn into regressions. Some things must load eagerly.
- Hero and above-fold images. Anything visible without scrolling should load immediately. Lazy loading these shows a blank gap before the image appears β bad UX and worse LCP.
- The LCP element. Whatever your Largest Contentful Paint element is β usually the hero image or a large heading β must never carry
loading="lazy". The browser deliberately delays lazy resources, so you'd be telling it to slow down your single most important paint. - Critical background images inside the initial viewport. Don't defer these β use
fetchpriority="high"instead. - Auto-advancing carousel slides. If a slider rotates on its own, the next image needs to be ready before it appears, or it pops in mid-transition.
Not sure which images sit above the fold on a given template? Run a free SlapMyWeb audit to see which of these issues your site actually has β the Performance pillar flags images that should and shouldn't be lazy loaded based on viewport position.

Common Mistakes to Avoid
- Lazy loading the hero. The most frequent and most damaging error β your hero is almost always the LCP element. Google flags this directly in PageSpeed Insights.
- No width/height. Missing dimensions mean zero reserved space, so content jumps when images load. Set explicit
width/heightoraspect-ratioon every image. - JavaScript when native works. Loading a 30KB lazy-load library to do what
loading="lazy"does for free is pure overhead. Native is lighter, faster, and more reliable. - No `<noscript>` fallback. JavaScript-based
data-srcapproaches leave JS-disabled users β and some crawlers β with no image at all. Always include the fallback. - Skipping compression. Lazy loading defers when an image loads, not how heavy it is. A 5MB PNG is still 5MB when it lazy loads. Compress and convert to WebP or AVIF first.
- Mismatched placeholders. A placeholder with a different aspect ratio than the final image shifts the page when the real file swaps in. Match placeholder dimensions exactly.
How to Verify It's Working
After shipping lazy loading, confirm it with real measurement β don't assume.
- [Google PageSpeed Insights](https://pagespeed.web.dev/). Look for the "Defer offscreen images" audit; a pass means it's working. Confirm LCP improved (or at least didn't regress β a regression usually means you lazy-loaded the hero). For the bigger picture, see what a PageSpeed score means and how to hit 90+.
- Chrome DevTools β Network tab. Filter by "Img" and scroll slowly. Image requests should fire as you scroll, not all at once on load. If everything loads upfront, your lazy loading isn't taking effect.
- WebPageTest filmstrip. Compare the visual load timeline with and without lazy loading β the difference in time-to-first-render is usually obvious.
Google's own lazy loading documentation and Search Central guidance confirm native lazy loading is fully crawlable and safe for image indexing.
Quick Implementation Checklist
- Add
loading="lazy"to every<img>below the fold. - Add
loading="lazy"to every non-critical<iframe>(video, maps, widgets). - Set
widthandheighton every image element. - Use
priority(Next.js) orloading="eager"+fetchpriority="high"on the hero/LCP image. - Replace heavy YouTube embeds with the facade pattern.
- Set
preload="none"on self-hosted videos. - Compress all images to WebP/AVIF before applying lazy loading.
- Add
<noscript>fallbacks for any JavaScript-based approach. - Verify in DevTools Network and confirm LCP/CLS in PageSpeed Insights.
Lazy loading sits inside the broader picture of improving website speed and a healthy technical SEO foundation β it's one of the highest-leverage changes you can make on a media-heavy page.
Frequently Asked Questions
Does lazy loading hurt SEO or stop images from being indexed?
No. Googlebot renders JavaScript and scrolls pages, so it discovers natively lazy-loaded images normally β Google has stated loading="lazy" does not prevent indexing. The only risk is a JavaScript-only approach without a <noscript> fallback, which some non-rendering crawlers may miss. Native loading="lazy" is always SEO-safe.
What's the difference between loading="lazy" and fetchpriority="high"?
They are opposite signals. loading="lazy" tells the browser to defer a resource until it nears the viewport, while fetchpriority="high" tells the browser to prioritize a resource above others. Use fetchpriority="high" on your hero/LCP image and loading="lazy" on everything below the fold β never both on the same element, since they contradict each other.
Should I lazy load images in a single-page app (SPA)?
Yes, with one adjustment. Native loading="lazy" works fine within each rendered view, since it watches the viewport regardless of routing. For route-level deferral, use your framework's code splitting β React.lazy, Vue async components, or Angular loadChildren β so an entire route and its images load only when the user navigates there.
How much faster will my page get with lazy loading?
It depends entirely on how many off-screen images you currently load eagerly. Image-heavy pages β product listings, galleries, long articles β see the largest gains because they have the most to defer. A page with only two or three images, all above the fold, will see little change since there's almost nothing to lazy load.
Can I lazy load CSS background images?
Not with native loading="lazy" β that attribute only works on <img> and <iframe> elements. For CSS background-image, use the Intersection Observer API to add a class (and the background) when the element nears the viewport. This is one of the main reasons to reach for Intersection Observer over the native attribute.
SlapMyWeb Team
We build SlapMyWeb β a brutally honest AI website audit that scans 240+ SEO, performance and Core Web Vitals signals and hands you the fix code.