How to Use Lazy Loading for Images & Videos Without Hurting UX
If your site serves modern, media-heavy content, lazy loading is one of the highest-ROI tactics you can implement. It’s also one of the easiest to misapply. Do it right and your pages feel blazing fast, Core Web Vitals improve, and bandwidth bills drop. Do it wrong and you degrade user experience with janky page shifts, delayed content, or broken SEO.
This guide shows you how to use lazy loading for images and videos without hurting UX. You’ll learn the principles, patterns, and exact code to create a responsive, accessible, SEO-friendly experience that loads fast on every device.
TL;DR
Lazy load most images and embedded videos that are off-screen on initial page load. Never lazy load your hero/LCP image or any above-the-fold media that’s essential to first impression.
Reserve space for media to prevent layout shifts (CLS). Always set width and height (or CSS aspect-ratio) on images and video containers.
Use native lazy loading first (loading='lazy' for images and iframes) and supplement with IntersectionObserver for advanced behaviors.
Preload and give fetchpriority='high' to your LCP image. Use decoding='async' for images to avoid main-thread jank.
For video: defer heavy players (YouTube/Vimeo) and render a lightweight poster thumbnail with a play button. Only load the player on interaction or when near viewport.
Use responsive images (srcset/sizes, picture) and modern formats (AVIF/WebP) for smaller payloads. Use a tasteful placeholder (blur, color, skeleton) that matches the final aspect ratio.
Measure impact on Core Web Vitals (LCP, CLS, INP) using both lab (Lighthouse, WebPageTest) and real-user monitoring. Iterate.
Why Lazy Loading Matters (And How It Can Hurt)
Lazy loading defers fetching of off-screen images and media until they are likely to be needed. This reduces initial network transfers, speeds first render, and improves perceived performance. But there are trade-offs. Let’s break down the benefits and pitfalls so you know how to avoid common UX regressions.
Benefits
Faster initial render time: Less data to load up front means faster first paint and a quicker time to interact.
Lower bandwidth usage: Especially powerful on image-heavy product listings, galleries, feeds, and long-form content.
Better Core Web Vitals (when done correctly): Deferring non-critical media improves LCP and reduces render blocking. Reserving space cuts CLS.
Energy efficiency: Users on mobile devices consume less battery if fewer bytes are decoded and painted early.
Where Lazy Loading Goes Wrong
Lazy loading the LCP/hero image: If your hero is lazy loaded, your LCP will be delayed and your page will feel slow. This is one of the most common and costly mistakes.
CLS from missing dimensions: If you don’t reserve space for media, content shifts when images/videos load. Users perceive this as jank.
Too-late loading thresholds: Content pops in just as users scroll to it. That micro-wait can feel unresponsive, especially on slow connections.
Placeholder mismatches: A placeholder with a different aspect ratio than the final media creates jumps on swap.
Overuse of spinners: Spinners signal waiting and are poor placeholders. Blurs or color-dominant placeholders feel faster and calmer.
SEO traps: If media is injected via JavaScript in a way crawlers can’t see, or if you block lazy content from being included in the DOM, search engines may not index it.
How Browsers Lazy Load Today
Native lazy loading support is now broadly available in modern browsers for images and iframes.
Images: loading='lazy' tells the browser to defer loading until the image is near the viewport.
Iframes: loading='lazy' works similarly, ideal for embedded video players and third-party content.
Priority Hints: fetchpriority='high' or 'low' lets you tune resource prioritization, especially for hero images.
Async Decoding: decoding='async' for images allows the browser to decode off the main thread when possible, improving responsiveness.
Native behavior is robust and simple, but you should still consider an IntersectionObserver-based approach for:
Fine-grained control over threshold and rootMargin (e.g., start loading 300-500px before visibility).
Feature parity across older browsers (if you target legacy environments).
Complex behaviors (e.g., deferred swapping of background images or videos).
UX-First Lazy Loading Principles
1) Never Lazy Load Above-the-Fold, Critical Media
Your hero image (often LCP) should always load eagerly. Mark it explicitly:
Do not set loading='lazy' on the LCP image.
Consider fetchpriority='high' on the LCP image and preload it via a link tag.
Use decoding='async' to keep the main thread responsive.
2) Reserve Space to Prevent CLS
Layout shifts are one of the fastest ways to harm perceived quality. Avoid them by ensuring media dimensions are known before loading.
For : always set width and height attributes (intrinsic dimensions) or style with aspect-ratio.
For video/iframes: give the wrapper a consistent aspect ratio (e.g., 16/9) using CSS.
For CSS background images: pre-size the container; better yet, use real for content images so the browser can handle decoding, responsive formats, and lazy loading natively.
3) Use Friendly Placeholders
Good placeholders feel like the content is already there and loading is natural. Options include:
Blur-up (LQIP): display a tiny blurred version of the image.
Solid or dominant color: derived from the image palette for a subtle, lightweight placeholder.
Skeleton: use sparingly for lists or tiled UI; ensure the skeleton has the same dimensions as the final content.
Avoid spinners: they emphasize waiting. If you use them, limit to truly long waits.
4) Load Early Enough to Avoid Pop-in
A good heuristic is to begin loading when content is roughly 200–600px away from the viewport. Faster networks and lighter images can tolerate smaller margins; slower networks and heavier media benefit from larger margins.
5) Accessibility and Semantics
Always provide alt text for images that convey meaning.
For decorative images, use empty alt to avoid noise for screen readers.
Ensure focus states and keyboard navigation aren’t blocked by overlays or lazy placeholders.
For video, include controls unless it’s purely decorative, and provide captions or transcripts.
6) Don’t Break SEO
Keep lazy-loaded content in the DOM with proper src attributes (or standard native attributes). Avoid hiding critical content behind JS that runs only on interaction.
If you must swap data attributes for src via JS, consider noscript fallbacks.
Use image sitemaps for important media, and use descriptive filenames and alt text.
Implementing Lazy Loading for Images
The best lazy loading strategy uses native attributes first, with progressive enhancement.
Basic HTML Pattern (Native Lazy Loading)
<imgsrc='hero-1440.jpg'alt='A person hiking at sunrise on a mountain ridge'width='1440'height='810'fetchpriority='high'decoding='async'loading='eager'/><imgsrc='gallery-01-640.jpg'alt='Close-up of artisan pottery in natural light'width='640'height='480'decoding='async'loading='lazy'fetchpriority='low'/>
Notes:
The hero image uses loading='eager' and fetchpriority='high'. This marks it as critical.
The gallery image uses loading='lazy' and fetchpriority='low'.
width and height reserve space and allow the browser to compute aspect ratio.
Responsive Images with Modern Formats
Leverage the picture element with AVIF/WebP fallbacks and srcset/sizes:
Combine with the in-HTML tag using matching srcset/sizes so the browser doesn’t download the wrong variant twice.
Using CSS Aspect Ratio
If you can’t set width and height in HTML, use CSS to lock aspect ratios:
.card-image{aspect-ratio:4/3;/* Matches the intrinsic image ratio */width:100%;display: block;background:#f0f0f0;/* Neutral placeholder color */}.card-image img{width:100%;height:100%;object-fit: cover;display: block;}
This ensures the layout stays stable while the image loads.
IntersectionObserver for Advanced Control
For situations requiring more nuance (e.g., background images, conditional decoding), use IntersectionObserver:
Use a generous rootMargin (e.g., 300–600px) on slow networks to avoid pop-in.
Unobserve elements once loaded to avoid resource leaks.
When lazy loading background images, apply a class that sets background-image only after intersection, and use aspect-ratio or fixed height on the container.
Blur-Up and LQIP Placeholders
Blur-up placeholders help perceived performance. One simple pattern:
<img
class='blur-up'
src='mountain-tiny.jpg' /* very small, e.g., 20x13 */
data-src='mountain-960.jpg'
alt='Golden light across a mountain valley'
width='1440'
height='960'
/>
Next.js Image: Next/Image lazily loads by default. Use priority for LCP/hero images and placeholder='blur' for LQIP.
Hero:
Non-hero:
Nuxt/Nitro: Use @nuxt/image or nuxt/image-edge for automatic formats, sizes, and lazy loading. Mark hero images with preload and priority.
React (vanilla): Use loading='lazy' and consider libraries for IntersectionObserver patterns. Keep placeholders minimal to avoid layout shift.
Vue: Directives can simplify lazy loading (e.g., v-lazy). Ensure space reservation is present.
SvelteKit: Svelte components can wrap native lazy loading and add IntersectionObserver for backgrounds.
Angular: Use native attributes; RxJS can power advanced intersection patterns. Keep change detection overhead low.
WordPress, Shopify, and CMS Notes
WordPress: Core adds loading='lazy' by default. For the first contentful or LCP image, disable lazy and add fetchpriority='high'. In theme or plugin:
For LCP: set 'loading' => 'eager' and 'fetchpriority' => 'high' on the featured image.
Ensure width/height are output by get_the_post_thumbnail.
Shopify: Use image_url and image_tag filters to set sizes and loading. Do not lazy load the primary product image above the fold. Add fetchpriority='high' to it.
Headless CMS: Keep assets addressable with explicit width/height. If using an image CDN (Cloudinary/Imgix/Cloudflare Images), let it generate AVIF/WebP and LQIPs and wire them into picture/srcset.
Implementing Lazy Loading for Videos
Video is heavier and can easily dominate network and CPU budgets. The right lazy strategy protects UX and Core Web Vitals.
Native HTML5 Video
For self-hosted video:
Use preload='metadata' or preload='none' to avoid fetching the full file up front.
Provide a poster image to give users immediate context.
Use playsinline on mobile to avoid full-screen interruptions.
Only autoplay muted video, and consider deferring play until in view.
Poster is essential: It provides instant feedback and sets context.
Avoid auto-playing sound: If autostart is important, keep it muted; allow unmute interactions.
Controls and captions: Always include accessible controls and captions where appropriate.
Data usage: Offer multiple bitrates and formats. Consider HLS/DASH for longer videos and adaptive streaming.
Avoiding SEO Pitfalls with Lazy Loading
Search engines are much better at rendering JavaScript and handling lazy-loaded content than they used to be. Still, protect your content’s discoverability using these safeguards.
Keep media in the DOM: Prefer native loading='lazy' because the real src is present. If your approach relies on swapping data-src, include a noscript fallback.
<imgdata-src='/images/gallery-01-960.jpg'alt='Artisan pottery on a shelf'width='960'height='720'><noscript><imgsrc='/images/gallery-01-960.jpg'alt='Artisan pottery on a shelf'width='960'height='720'></noscript>
Don’t lazy load above-the-fold content: This can tank LCP and also hinder meaningful render for crawlers.
Image sitemaps: For galleries and media-heavy sites, use image sitemaps to help discovery.
Structured data: If you use product, recipe, or news schema, ensure image URLs in structured data match the lazy-loaded images.
CDNs and robots: Ensure your image CDN isn’t blocked to crawlers.
Performance budgets: Keep heroes fast and small. Use modern formats and preloading.
Performance Tuning: Thresholds, Priorities, and Heuristics
Lazy loading is not just on/off. Tuning improves UX across diverse networks and devices.
Thresholds and rootMargin
Root margin is your prefetch buffer. Typical values: 200–800px depending on asset weight and network conditions.
For infinite scroll: A larger rootMargin improves experience as new content enters.
Threshold: A low threshold like 0.01 triggers early without waiting for full visibility.
Priority Hints
fetchpriority='high' on LCP images helps the browser understand their importance.
fetchpriority='low' on below-the-fold images nudges them behind critical resources.
Decoding Hints
decoding='async' prevents long image decodes from blocking the main thread.
Avoid decoding='sync' unless you have a very specific reason.
Network Awareness
Use save-data client hint or Network Information API (where available) to reduce or delay loading on constrained networks.
On 2G/slow-3G, increase rootMargin and consider lower-quality variants.
Resource Contention
Avoid triggering too many new image downloads at once. Stagger via IntersectionObserver callbacks or requestIdleCallback where appropriate.
Use HTTP/2/3 effectively: serve images from a single domain or a CDN optimized for multiplexing.
Testing and Monitoring Your Implementation
Shipping lazy loading is not the end. Measure how it performs for users.
Lab Testing
Lighthouse or PageSpeed Insights: Check LCP, CLS, and diagnostics for off-screen images.
WebPageTest: Run on mobile profiles. Inspect waterfalls to see when images start and whether the LCP is prioritized.
Browser DevTools: Network panel reveals if lazy images start loading at the right scroll point. Performance panel helps you inspect decoding and layout shifts.
Field Monitoring
Real User Monitoring (RUM): Use PerformanceObserver to capture LCP and CLS in the wild. Tie metrics to connection types and devices.
Chrome UX Report (CrUX): Validate improvements and identify regressions in field data.
Debugging Checklist
Does the hero image load eagerly with fetchpriority='high'? Is it indeed the LCP element in DevTools?
Do any images shift layout on load? If yes, add width/height or CSS aspect-ratio.
Do embedded players block interactivity? If yes, use placeholders and load the iframe late.
Are placeholders tasteful and consistent across dark/light themes and content types?
Inline images: loading='lazy', decoding='async', sizes set to content width.
Use picture for AVIF/WebP and fallback.
Placeholders: blur-up or palette-based background color.
<!-- In <head> --><linkrel='preload'as='image'href='/images/hero-1600.jpg'imagesrcset='/images/hero-800.jpg 800w, /images/hero-1600.jpg 1600w'imagesizes='100vw'fetchpriority='high'><!-- In article --><picture><sourcetype='image/avif'srcset='/images/hero-800.avif 800w, /images/hero-1600.avif 1600w'sizes='100vw'><sourcetype='image/webp'srcset='/images/hero-800.webp 800w, /images/hero-1600.webp 1600w'sizes='100vw'><imgsrc='/images/hero-1600.jpg'srcset='/images/hero-800.jpg 800w, /images/hero-1600.jpg 1600w'sizes='100vw'alt='Sunrise over the city skyline'width='1600'height='900'loading='eager'decoding='async'fetchpriority='high'></picture><!-- Inline --><imgsrc='/images/inline-640.jpg'alt='Detail of the workshop bench'width='640'height='427'loading='lazy'decoding='async'>
Pattern 2: Product Listing Grid
Card images: Set aspect-ratio to match product image. Use loading='lazy'. Start loading 400–800px early.
Hover zoom: Preload larger image on hover with small delay to prevent jitter.
Is the media above the fold and essential to first impression?
Yes: Do not lazy load. Preload and set fetchpriority='high'.
No: Proceed to next question.
Is the media critical for immediate task completion?
Yes: Consider eager load or small rootMargin to load early.
No: Use native loading='lazy' or IntersectionObserver.
Is the media decorative only?
Yes: You can lazy load, but ensure dimensions are reserved.
Is the network often slow for your audience?
Yes: Increase rootMargin and use lighter formats/placeholders.
Is the media a heavy third-party embed?
Yes: Use thumbnail placeholders and load on interaction.
Implementation Checklist
For images:
Do not lazy load the hero/LCP image.
Add fetchpriority='high' and consider a preload for the LCP image.
Use width/height or CSS aspect-ratio to reserve space.
Use loading='lazy' and decoding='async' on non-critical images.
Use picture with AVIF/WebP and accurate srcset/sizes.
Provide accessible alt text.
Consider blur-up or palette placeholders.
For video:
Use poster images and preload='metadata' or 'none'.
Defer heavy players; use thumbnails with play buttons.
Lazy load iframes (loading='lazy') or load on intersection/click.
Include captions and controls as needed.
For performance and SEO:
Test LCP and CLS in Lighthouse/WebPageTest and RUM.
Ensure lazy content exists in the DOM or has noscript fallback.
Use a CDN for media with auto format/quality.
Preconnect to your image CDN to reduce handshake latency.
Common Mistakes and How to Fix Them
Lazy loading the hero image
Symptom: LCP is high; users see a blank area or placeholder for too long.
Fix: Set loading='eager' and fetchpriority='high' on the hero, and optionally preload.
Layout shifts on image load (CLS)
Symptom: Content jumps when images appear.
Fix: Add width/height or aspect-ratio. Ensure placeholders have the same dimensions.
Pop-in on scroll
Symptom: Users see content appear too late.
Fix: Increase rootMargin for IntersectionObserver or rely on native lazy with additional prefetch strategies.
Overuse of spinners
Symptom: Interface feels slow and busy.
Fix: Switch to blur-up or palette backgrounds. Use skeletons sparingly.
Incorrect sizes/srcset
Symptom: Large images downloaded on mobile; network waste.
Fix: Tune sizes attribute to reflect actual CSS layout widths.
Broken SEO for images
Symptom: Images not showing in Google Images; missing in rich results.
Fix: Keep images in DOM with real src; use noscript fallback if swapping attributes; audit structured data.
Lazy loading icons or tiny assets
Symptom: Weird flickers on UI icons.
Fix: Inline critical icons as SVG or load eagerly. Lazy loading is unnecessary for tiny images.
Background images without reserved space
Symptom: Section height collapses until image loads.
Fix: Use aspect-ratio or fixed heights; better yet, use for contentful images.
Autoplaying videos with sound
Symptom: Startle and annoyance; possible browser blocks.
Fix: Only autoplay muted; let the user unmute. Consider deferring playback until visible.
Heavy embeds tanking INP
Symptom: Input delays when third-party iframes load.
Fix: Load embeds on interaction. Reduce main-thread contention.
Advanced Tips for Pros
Priority Orchestration Across the Page
Only your true LCP image should have fetchpriority='high'. Avoid overusing it.
Use preload sparingly; each preload increases connection competition.
Defer decoding-heavy images until the browser is idle if they are far off-screen.
Hybrid Lazy Strategies
Use native loading='lazy' for baseline behavior and add IntersectionObserver for background images or special thresholds.
For carousels/sliders, prefetch the next and previous slides’ images during idle time.
CDNs and On-the-Fly Optimization
If using Cloudinary/Imgix/Cloudflare Images/Fastly IO, set:
Format auto (f=auto) to serve AVIF/WebP where supported.
DPR-aware sizing for Retina screens (dpr=2, width adjusted accordingly).
Quality auto (q=auto) to balance fidelity and performance.
Generate LQIP or blurhash placeholders at build time, and map them to your image component props.
Handling Safari and Edge Cases
Native lazy loading for images and iframes is widely supported. Still, test in Safari and Firefox for any differences in thresholds and preload behavior.
Printing: Ensure print styles display lazy images. A print media query can force visibility by setting content to visible or overriding lazy patterns.
Analytics Without Performance Penalties
Avoid event listeners on each placeholder button in massive grids. Delegate from a parent container to keep memory low.
Batch IntersectionObserver work: Use a single observer instance where possible.
Measuring Impact on Core Web Vitals
LCP: Verify the LCP element is the hero image or a visible block of text. Ensure it’s eagerly loaded, sized correctly, and prioritized.
CLS: Confirm dimensions are always present and no unexpected ad/third-party elements cause layout shifts.
INP: Ensure heavy embeds and image decoding don’t trigger long tasks during user interactions. Async decoding and deferral help.
Use PerformanceObserver to collect field data:
try{const po =newPerformanceObserver((list)=>{for(const entry of list.getEntries()){// Send entry.value, entry.startTime, etc. to your analytics endpoint}}); po.observe({type:'largest-contentful-paint',buffered:true}); po.observe({type:'layout-shift',buffered:true});}catch(e){// Older browsers may not support}
Frequently Asked Questions (FAQs)
Should I lazy load every image?
No. Never lazy load your hero/LCP or any above-the-fold images that shape the first impression. Lazy load images below the fold and non-critical media.
Does lazy loading hurt SEO?
Not when implemented correctly. Use native loading='lazy' so the actual src is in the DOM. If you swap data-src via JS, add noscript fallbacks. Keep alt text, filenames, and image sitemaps.
How do I prevent layout shifts (CLS) with lazy loading?
Reserve space. Always include width and height on images or use CSS aspect-ratio. Provide placeholders that match the final aspect.
What threshold should I use for IntersectionObserver?
Start with rootMargin around 400–600px and threshold 0–0.1. Adjust based on asset weight, device, and your audience’s network conditions.
What about background images?
Prefer for content images. If you must use background images, reserve space and set background-image on intersection to avoid layout shifts.
Should I preload images when I also lazy load them?
Only preload critical images like the hero/LCP. Preloading lazy images can create unnecessary competition for bandwidth.
How do I handle YouTube embeds efficiently?
Use a poster thumbnail with a play button and load the iframe when the user clicks or when near viewport. Apply loading='lazy' to the iframe.
Do I need decoding='async'?
It’s a low-cost win for non-critical images to keep the main thread responsive. Use it broadly on lazy images.
Which image formats should I use?
Prefer AVIF or WebP with a JPEG fallback. Use the picture element and fine-tuned srcset/sizes for responsive delivery.
Is native lazy loading enough?
For many cases, yes. Add IntersectionObserver where you need nuanced control, background-image support, or different thresholds.
How many images can I lazy load on a page?
As many as you need, but avoid firing all requests at once. The browser will prioritize, but you can stagger loading and tune thresholds.
Can lazy loading improve INP?
Indirectly. By deferring heavy work and decoding, you leave more main-thread budget free during user interactions. Also avoid loading heavy embeds during interaction frames.
Final Thoughts: Lazy Loading That Users Don’t Notice
The best lazy loading is invisible. Users should feel that content is already there, unfolding smoothly as they explore. That requires:
Disciplined prioritization: Eager load only what’s essential.
Reliable space reservation: Lock aspect ratios to eliminate shifts.
Thoughtful placeholders: Favor blur-up and palette-inspired colors over spinners.
Accurate responsive images: srcset and sizes that match reality.
Responsible video strategy: Thumbnails and late-loading players.
Continuous measurement: Confirm improvements in both lab and field metrics.
Implement the patterns in this guide and you’ll deliver a faster site with happier users and healthier Core Web Vitals — without compromising UX or SEO.
Call to Action
Want a drop-in image component with blur-up placeholders and native lazy loading? Build one using the patterns above or integrate your framework’s image component with LQIPs.
Need help tuning lazy loading for your stack? Create a small performance budget and test plan, then iterate with Lighthouse and WebPageTest. Gather real-user data with PerformanceObserver to validate wins.
Make your media invisible until it matters — and make your performance wins obvious everywhere else.