Lazy loading explained: How to speed up your website without compromising UX



This content originally appeared on Level Up Coding – Medium and was authored by Aliyu Salim

As a developer, it’s not just the skills of writing code that matter

What separates a polished, professional website from a beginner project isn’t just code, it’s the optimization techniques that make it fast, accessible, and enjoyable to use.

These skills can mostly be spotted in features seen in the works of experienced developers, one of which is optimization for speed without compromising important aspects such as UX

Speed powers SEO, UX reduces bounce, together they decide your site’s survival

One of the most widely adopted and effective (if used well) methods of speed optimization which does not compromise UX is lazy loading.

This article clearly discusses lazy loading, what it is, why it matters, what it can be applied to, how it works, real-world examples, best practices, pitfalls, and tools & libraries.

Photo by Олег Мороз on Unsplash

What is lazy loading?

Lazy loading is a performance optimization technique where certain parts of a webpage (like images, videos, or scripts) are loaded only when needed, instead of during the initial page load.

This is the direct opposite of eager loading (where every asset of the webpage loads at once/initially).

By not loading every asset of the webpage immediately the initial load speed becomes faster and additional assets are loaded when the user scroll/interact and this speed boost brings in the reward of better SEO.

Imagine opening Netflix and instead of streaming the movie you picked, it pre-downloads every single title in its library before playing anything. That’s eager loading. Lazy loading avoids that.
Lazy-loading vs Eager loading

Why lazy loading matters?

Lazy loading can help bring the following benefits to your website:

Performance Boost

Website performance is boosted when non-critical resources are deferred, so the browser can prioritize loading essential assets first. This improves:

Time-to-Interactive (TTI): the time it takes for pages to become usable/responsive upon load.

Core Web Vitals (LCP, FID, CLS): key metrics Google uses to measure user experience.

Better User Experience

Faster, smoother pages reduce friction. Users don’t have to wait for resources they may never interact with.

This helps reduce bounce rates and increases engagement (scroll depth, time on site).

SEO Benefits

Page speed is a ranking factor, and lazy loading makes pages lighter and faster.

Note: Googlebot (Google crawlers/bot that finds and indexes web pages) supports lazy loading if implemented correctly (i.e using proper src, alt, and not relying only on JavaScript).

Bandwidth Savings

Not all users have high-speed internet or unlimited data.

If you’ve ever tried loading an image-heavy blog on 3G, you know the frustration of staring at blank boxes.

Lazy loading ensures users only download what they need. Which saves data for mobile users and makes your site usable in low-connectivity or high-latency environments.

Accessibility Improvements

Lazy loading, when done correctly, can actually improve accessibility by making content load progressively instead of all at once. This prevents screen readers from being overwhelmed with too many elements at page load.

But note: poor lazy loading (e.g., missing alt text or not rendering elements until interaction) can hurt accessibility. The key is to combine lazy loading with proper semantic HTML and descriptive attributes so assistive technologies can interpret content smoothly.

What can be lazy-loaded?

Images (the most common use case):

Images often account for most of a page’s weight. Deferring them until needed reduces initial load time.

Examples: product images in e-commerce websites, blog post images, etc.

Videos

Embedded videos (YouTube, Vimeo) load heavy scripts which slow down initial render.

<iframe src="https://www.youtube.com/embed/xyz" loading="lazy"></iframe>

Strategy: Replace the embed with a lightweight thumbnail + play button, then load the player only on interaction.

Note: Some libraries (e.g., lite-youtube-embed) handle this automatically.

IFrames (Maps, Ads, Social Media)

Iframes load external resources that can block rendering and increase network requests.

Examples: Google Maps embeds, ad slots, Twitter posts, Instagram embeds.

<iframe src="https://maps.google.com" loading="lazy"></iframe>
Useful Tip: For maps, consider loading a static image first and only load the interactive map on click.

Code Bundles

Large JavaScript bundles increase Time to Interactive (TTI). Code splitting ensures only the code needed for the current route or component is shipped.

Examples:

  • React: Lazy load components.
const Chart = React.lazy(() => import('./Chart'));
  • Angular: Route-based module loading with loadChildren.
  • Vue: Async component loading with dynamic import().
Useful Tip: Split bundles by routes and rarely used features (e.g dashboards, charts, modals).

Other Resources

  • Animations: Heavy Lottie files or CSS animations can be deferred until visible.
  • Charts & Graphs: Load libraries like Chart.js or D3.js only when the chart section enters the viewport.
  • Third-Party Scripts: Social share buttons, analytics, or A/B testing scripts should be delayed until after critical content loads.
The general principle for choosing what to lazy load is:
If it’s not essential on page load, it can probably be lazy-loaded. From images to JavaScript bundles to third-party embeds, lazy loading ensures your users see what matters first, faster, smoother, and with fewer distractions.

How lazy loading works (How it can be used in projects)

A. Native HTML Lazy Loading

Modern browsers now support lazy loading natively with a simple attribute loading.

Supported Elements (elements it can be added to): <img> and <iframe>.

How it works: Add the loading=”lazy” attribute to an element that is supported, and the browser defers loading until the element is near the viewport.

<!-- Image -->
<img src="product.jpg" alt="Product" loading="lazy">

<!-- Iframe -->
<iframe src="https://maps.google.com" loading="lazy"></iframe>

Pros:

  • Extremely simple (one attribute).
  • No JavaScript required.
  • Widely supported in modern browsers (Chrome, Edge, Firefox, Safari 15+).

Cons:

  • Limited to images and iframes.
  • Browser decides when to load (less fine-grained control).
  • Doesn’t allow animations, preload strategies, or advanced conditions.

Use Case: Best for static websites, blogs, or image-heavy pages where minimal configuration is needed.

B. JavaScript: Intersection Observer API

For more control, developers use the Intersection Observer API. This allows you to detect when an element enters the viewport and trigger custom logic (e.g., loading images, animations, or third-party content).

Example: Lazy Loading Images

<img data-src="large-image.jpg" alt="Sample" class="lazy">
<script>
const images = document.querySelectorAll("img.lazy");

const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Replace placeholder with real source
img.classList.remove("lazy");
observer.unobserve(img); // Stop observing once loaded
}
});
});

images.forEach(img => observer.observe(img));
</script>

Pros:

  • Works for any element, not just <img> and <iframe>.
  • Full control: you decide when/how content loads.
  • Can add custom effects (fade-in, animations).
  • Extendable: works for infinite scroll, charts, ads, etc.
  • Backwards compatible with polyfills.

Cons:

  • Requires JavaScript.
  • Slightly more complex to implement.
  • Polyfill needed for IE11 or very old browsers.

Use Case: Perfect for custom apps where you want animations, deferred scripts, or non-standard lazy loading.

C. Framework-Level Lazy Loading

Modern frameworks (React, Vue, Angular, etc.) provide built-in mechanisms for lazy loading JavaScript code bundles and components. This helps reduce the size of the initial bundle, improving Time to Interactive (TTI).

React: React.lazy() + Suspense

import React, { Suspense } from "react";

const Chart = React.lazy(() => import("./Chart"));

function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
</div>
);
}

How it works: React.lazy() lets you create a component that loads its code only when it’s needed, while Suspense provides a fallback UI (like a loading spinner) to display while that code is being downloaded. Together, they enable code-splitting to improve your app’s initial load time.

Benefit: Smaller initial JS bundle, faster first render.

Vue: Async Components

const Chart = () => import('./Chart.vue');

export default {
components: {
Chart
}
}

How it works: Vue dynamically imports code only when a component is needed for rendering

Benefit: Lightweight entry bundle, defers heavy components.

Angular: Route-Based Lazy Loading

const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];

How it works: Angular splits feature modules and loads them only when a user navigates to that route.

Benefit: Faster initial load, especially for large enterprise apps.

Key Takeaway

  • Native HTML lazy loading >> simplest option, works with images/iframes.
  • Intersection Observer API >> maximum flexibility and control for advanced scenarios.
  • Framework-level lazy loading >> reduces JS bundle size and boosts performance in SPAs.
Note that: In practice, most apps use a combination of these techniques: native HTML lazy loading for media + Intersection Observer for custom elements + framework-level lazy loading for code splitting.

Real-world examples of lazy loading

The effectiveness of lazy loading can further be seen by looking at real companies examples, how they apply it and what results they achieved.

1. YouTube >> Deferring Heavy Video Players

What they lazy load: Video iframes (YouTube players).

How they implemented it: Instead of loading the full video player immediately, YouTube shows only a thumbnail preview + play button. The actual <iframe> loads only after the user clicks play.

Results: Using techniques like Lite YouTube Embed, page weight per video dropped by 500KB+, and initial load speed improved by up to 50%.

Takeaway: Defer heavy embeds until interaction. Users don’t need a video player until they actually decide to watch.

2. Pinterest >> Endless Image Grid

What they lazy load: Pins (thousands of high-resolution images).

How they implemented it: Progressive lazy loading with placeholders. Pins load in stages. First a low-quality preview, then full resolution when visible.

Results: Engineers reported a 40% reduction in initial page load size and a 15% boost in SEO traffic due to faster speeds.

Takeaway: For image-heavy sites, combine lazy loading with progressive enhancement to save bandwidth without hurting visual quality.

3. Instagram & Twitter (X) >> Infinite Scrolling Feeds

What they lazy load: Posts, images, and videos in feeds.

How they implemented it: Content loads dynamically as the user scrolls, preloading just before it enters the viewport.

Results: Instagram noted that adjusting their preload threshold increased average session length by 15%, while Twitter Lite reported a 30% faster TTI (Time to Interactive).

Takeaway: Smart preloading keeps the experience seamless. Users should never notice content “catching up.”

4. Medium >> Blurred Image Placeholders

What they lazy load: Blog post images.

How they implemented it: Each image is replaced with a tiny blurred preview first. The full-resolution image fades in once it’s ready.

Results: This reduced perceived wait time dramatically, keeping engagement high even on slower networks. Many blogs and portfolios now copy Medium’s technique.

Takeaway: Use low-quality previews or skeleton screens to signal progress and reduce bounce on media-heavy sites.

5. Shopify Stores >> Product Grids

  • What they lazy load: Product images across category pages.
  • How they implemented it: Only the first few visible products load eagerly; the rest lazy load as shoppers scroll.
  • Results: Stores saw 35% reductions in product grid payloads, especially on mobile, where data usage is critical.
  • Takeaway: E-commerce wins when shoppers see products quickly, defer what’s offscreen to keep the first impression fast.

Key Takeaway

From YouTube’s video embeds to Pinterest’s massive grids, lazy loading has become a standard performance pattern. The core principle is always the same:

load what matters now, defer the rest until the user needs it.

Best Practices for Lazy Loading

Lazy loading can be powerful, but only if implemented carefully. Let’s see how to do that.

1. Use Placeholders (Blurred Images, Skeleton Screens)

This ensures that users do not see a blank space while content loads. Placeholders signal that content is coming.

Techniques:

  • Blurred image previews (tiny low-res image that sharpens as the full image loads).

Example: how Medium uses blurred image placeholders that transition smoothly to full resolution.

  • Skeleton screens (grey/colored shapes resembling the final layout): You’ve probably seen skeleton screens on Facebook or LinkedIn. Those grey placeholders that look like a wireframe of the content. They reassure you that something is happening instead of leaving you staring at a blank space..

Example: Facebook popularized skeleton screens to signal content structure while lazy-loading. A Nielsen Norman Group study confirmed this approach improves perceived performance compared to spinners.

<!-- HTML -->
<article class="card">
<div class="skeleton media" aria-hidden="true"></div>
<div class="skeleton line"></div>
<div class="skeleton line short"></div>

<!-- Real content (hidden until ready) -->
<img data-src="cover.jpg" alt="Article cover" class="lazy hidden" width="1200" height="630"/>
<h3 class="hidden">Improving perceived performance with skeletons</h3>
<p class="hidden">Skeleton screens reassure users while content streams in.</p>
</article>

<!-- JS -->
<script>
const cardIo = new IntersectionObserver((entries, obs) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
const card = e.target;
const img = card.querySelector('img.lazy');
img.src = img.dataset.src;
img.addEventListener('load', () => {
card.querySelectorAll('.skeleton').forEach(el => el.remove());
card.querySelectorAll('.hidden').forEach(el => el.classList.remove('hidden'));
}, { once: true });
obs.unobserve(card);
}
}, { rootMargin: '150px' });

document.querySelectorAll('.card').forEach(c => cardIo.observe(c));
</script>

<!-- CSS -->
<style>
.skeleton { background: linear-gradient(90deg, #eee 25%, #f4f4f4 37%, #eee 63%); background-size: 400% 100%; animation: shimmer 1.2s infinite; border-radius: 6px; }
.skeleton.media { width: 100%; aspect-ratio: 16/9; margin-bottom: .75rem; }
.skeleton.line { height: 14px; margin: .5rem 0; }
.skeleton.line.short { width: 60%; }
.hidden { visibility: hidden; }
@keyframes shimmer { 0% { background-position: 0% 0; } 100% { background-position: -135% 0; } }
.card { max-width: 720px; margin: 1rem auto; }
</style>
Tip: For best results keep placeholders lightweight (tiny base64 images or CSS shapes).

2. Always Load Above-the-Fold Content Eagerly

The first impression matters. Images or content in the initial viewport should never be lazy-loaded, or users will see a delay.

Example: Major e-commerce sites like Amazon always load hero product banners eagerly, while deferring secondary images. This follows a best practice confirmed by Google’s Core Web Vitals documentation. Delaying above-the-fold visuals can hurt Largest Contentful Paint (LCP) and conversions.

<!-- Hero image: eager + high fetch priority + dimensions -->
<img
src="/images/hero-1600.jpg"
alt="Product hero"
width="1600" height="900" <!-- reserve space to avoid CLS -->
loading="eager" <!-- ensure immediate load -->
fetchpriority="high" <!-- hint to the browser -->
decoding="async" />

<!-- Critical CSS inline (example) -->
<style>
/* minimal critical styles that paint the first viewport */
.hero { display:grid; place-items:center; min-height:60vh; }
</style>

<!-- Defer non-critical JS so it won't block TTI -->
<script src="/bundle.js" defer></script>
Tip: Load hero images, navigation elements, and branding assets eagerly. Then, lazy-load everything below the fold.

3. Combine with Responsive Images (srcset, sizes)

Lazy loading alone doesn’t solve the problem of delivering oversized images to small screens. Combining it with responsive techniques does.

<!-- A responsive, lazy-loaded image that serves the right file per viewport -->
<img
src="/images/gallery-800.jpg" <!-- fallback -->
srcset="
/images/gallery-480.jpg 480w,
/images/gallery-800.jpg 800w,
/images/gallery-1200.jpg 1200w,
/images/gallery-1600.jpg 1600w"
sizes="(max-width: 600px) 90vw, (max-width: 1200px) 50vw, 800px"
alt="Gallery photo"
width="1600" height="1066"
loading="lazy"
decoding="async" />

<!-- Optional: prefer modern formats with a <picture> -->
<picture>
<source
type="image/avif"
srcset="/images/gallery-800.avif 800w, /images/gallery-1600.avif 1600w"
sizes="(max-width: 600px) 90vw, (max-width: 1200px) 50vw, 800px" />
<source
type="image/webp"
srcset="/images/gallery-800.webp 800w, /images/gallery-1600.webp 1600w"
sizes="(max-width: 600px) 90vw, (max-width: 1200px) 50vw, 800px" />
<img
src="/images/gallery-1600.jpg"
srcset="/images/gallery-800.jpg 800w, /images/gallery-1600.jpg 1600w"
sizes="(max-width: 600px) 90vw, (max-width: 1200px) 50vw, 800px"
alt="Gallery photo"
width="1600" height="1066"
loading="lazy"
decoding="async" />
</picture>

Example: Unsplash uses responsive image attributes (srcset, sizes) alongside lazy loading. This is the approach recommended by Google’s image optimization guide. It ensures mobile users don’t download unnecessarily large files.

Tip: Use modern formats (WebP, AVIF) for even better savings.

4. Keep Lazy Loading Progressive (Don’t Delay Too Long)

Lazy loading should feel seamless. If resources take too long to appear after scrolling, users will notice lag.

Example: How Instagram preloads the next set of images just before you scroll to them, making the feed feel instant.

<!-- Example: lazy-load cards/images slightly before they appear -->
<img data-src="/img/card-1.jpg" class="lazy" alt="Card 1" width="800" height="500" />
<img data-src="/img/card-2.jpg" class="lazy" alt="Card 2" width="800" height="500" />
<!-- … -->

<script>
// Load ~300px before elements enter view, so users never see a blank spot.
const io = new IntersectionObserver((entries, obs) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
const img = e.target;
img.src = img.dataset.src;
img.onload = () => img.removeAttribute('data-src');
obs.unobserve(img);
}
}, {
root: null,
rootMargin: '300px 0px', // <-- preload distance (tune per page)
threshold: 0.01
});

document.querySelectorAll('img.lazy').forEach(img => io.observe(img));
</script>

Tip: The key here is to load resources just before they enter the viewport (preload threshold) >>“The Intersection Observer API’s rootMargin can be used for this“.

5. Test Performance with Lighthouse, PageSpeed Insights, WebPageTest

Implementation mistakes can cancel out lazy loading benefits. Testing Performance with tools such as Lighthouse, PageSpeed Insights, and WebPageTest ensures improvements are real.

Tip: Test on both desktop and mobile, and simulate low-speed networks.

6. Consider User Conditions (Slow Networks, Low-End Devices)

Not all users are on fast Wi-Fi with powerful devices. Lazy loading helps, but it must adapt to conditions.

Example: How Twitter Lite adjusts resource loading based on connection quality, saving bandwidth for users in low-data environments.

Tip: Implement adaptive loading strategies (load less on low-data mode, defer heavy scripts for weak devices), also use the Network Information API or device detection when possible. And, always test with throttled 3G/4G simulations to ensure graceful degradation.

Common pitfalls in lazy loading

Mistakes while using lazy loading that can take away its advantages and even cause more problems.

1. Lazy Loading Critical Content

This is when you apply lazy loading to critical content such as hero images, navigation icons, or key CTAs that should appear instantly.

As a result of this: Users see a blank or delayed experience; Core Web Vitals (especially LCP) worsen.

Example: eBay once experimented with aggressive lazy loading, causing hero product images on category pages to load late. This hurt conversion until they adjusted to load above-the-fold eagerly.

2. Failing to Provide Browser Fallbacks

Older browsers (like IE11, older Safari) don’t support the loading=”lazy” attribute.

As a result of this: Content may never load at all on these browsers.

To avoid this: Use polyfills or JavaScript-based lazy loading as a fallback.

3. Excessive Lazy Loading (Too Many Requests)

This is when you overuse lazy loading in such a way that every small resource triggers a request as the user scrolls.

This is a wrong practice that could cause network congestion and slow rendering.

Example: This was once a problem Medium faced when it lazy-loaded not only images but also inline SVG icons. This created hundreds of micro-requests, slowing reading sessions. They optimized by eagerly loading icons and deferring only heavy media.

To avoid this: Apply lazy loading selectively, prioritize big, heavy assets.

4. SEO Issues (Googlebot & Crawlers)

Googlebot may not index content if lazy loading is implemented incorrectly (e.g., requiring user scrolling).

This causes Content to fail to appear in search results.

Example: In 2020, some Shopify stores reported product descriptions weren’t being indexed by Google because they only rendered on scroll. For which Google later clarified implementation fixes.

The best way to avoid this is to follow Google’s SEO guidelines for lazy loading.

5. Accessibility Issues (Alt Text & ARIA)

This is when Lazy-loaded images or components are missing alt text or ARIA roles.

As a result of this: Screen reader users won’t be able to interpret the content, and this violates accessibility standards.

To avoid this: Always provide meaningful alt text and test with assistive technologies.

Bottom line:

Lazy loading is not about loading less, but about loading smart.

The best implementations strike a balance between speed, discoverability, and accessibility ensuring fast, inclusive, and SEO-friendly experiences.

Tools & Libraries for Lazy Loading

Tools & libraries that make implementing lazy loading easier across different tech stacks,

exists so you don’t need to reinvent the wheel

JavaScript Libraries

lazysizes (The most popular lazy loading library)

It automatically detects elements in the viewport and loads them. It works well with images, iframes, and even responsive images (srcset).

<!-- Install lazysizes via CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js" async></script>

<!-- Use data-src instead of src -->
<img data-src="image.jpg" class="lazyload" alt="Example">

React Lazy Load Image Component

A React-specific solution that helps developers lazy-load images and apply effects like blur transitions seamlessly.

import { LazyLoadImage } from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';

export default function Example() {
return (
<LazyLoadImage
src="image.jpg"
alt="Example"
effect="blur"
/>
);
}

Vue-Lazyload

A plugin for Vue.js that makes adding lazy loading to your components straightforward, often used in e-commerce and image-heavy apps.

// main.js
import Vue from 'vue'
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// In your component
<img v-lazy="'image.jpg'" alt="Example">

Testing & Performance Tools

To ensure lazy loading is actually improving performance, you can test your site with.

Google Lighthouse: which audits your site performance, including image optimization.

GTmetrix: Provides waterfall charts and lazy load suggestions.

WebPageTest: Helps analyze how your page loads, showing before/after lazy loading.

Tip: Before reaching for a library, try native loading=‘lazy’ attribute. Start simple, then scale up.

Conclusion

Lazy loading isn’t just a fancy optimization, it’s a practical strategy that boosts performance, user experience, and even SEO when implemented correctly. By ensuring users only download what they need, when they need it, you can dramatically reduce load times and keep people engaged.

That said, balance is key:

  • Don’t lazy load critical content (like above-the-fold text or your main hero image).
  • Do lazy load heavy, non-critical resources like images, videos, and third-party embeds.

Also: Audit your site performance with tools like Lighthouse or GTmetrix. Identify which assets would benefit from lazy loading, and start small. Use the native loading=”lazy” attribute, or a library if you need more control.

Final takeaway: Lazy loading is one of those “low effort, high reward” optimizations. Implement it thoughtfully, and your users (and Google rankings) will thank you.

Lazy loading explained: How to speed up your website without compromising UX was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding – Medium and was authored by Aliyu Salim