How to Lazy Load Images in HTML and JavaScript for Faster Page Speed

If your pages feel sluggish, images are almost always the culprit. They account for the largest share of bytes on the average web page, and most of them sit below the fold where visitors may never scroll. The fix is simple: lazy load them. In this practical tutorial, we’ll show you exactly how to lazy load images using the native HTML attribute, add a JavaScript fallback for edge cases, and share real performance numbers from a sample page we tested.

What Is Lazy Loading (And Why It Matters in 2026)

Lazy loading is a performance strategy that defers the loading of non-critical resources, like off-screen images, until the user is about to see them. Instead of downloading every image when the page loads, the browser only fetches what is needed.

The benefits are direct and measurable:

  • Faster initial page load (lower LCP)
  • Less bandwidth used for users who don’t scroll the full page
  • Better Core Web Vitals scores, which Google uses as a ranking signal
  • Lower server costs from fewer image requests
fast website loading

Method 1: The Native loading="lazy" Attribute

This is the easiest and most reliable way to lazy load images today. Every modern browser (Chrome, Edge, Firefox, Safari, Opera) supports it natively, with no JavaScript required.

Basic Syntax

<img src="product-photo.jpg" alt="Branded promotional pen" loading="lazy" width="800" height="600">

That’s the whole implementation. The browser handles distance-from-viewport calculations, prioritization, and fetching for you.

The Three Possible Values

Value Behavior When to Use
lazy Defers loading until the image is near the viewport Below-the-fold images
eager Loads the image immediately (default) Above-the-fold hero images, logos
auto Browser decides (rarely useful) Generally avoid

Critical Best Practices

  1. Always set width and height attributes to prevent layout shift (CLS).
  2. Never lazy load above-the-fold images. Doing so will hurt your LCP score.
  3. Use fetchpriority="high" on your hero image to load it even faster.
  4. Combine with modern formats like WebP or AVIF for compounding gains.
fast website loading

Method 2: IntersectionObserver Fallback

While native lazy loading covers more than 95% of users, you may still need finer control, for example to lazy load background images, iframes in legacy browsers, or to trigger custom animations when an image enters view. That’s where IntersectionObserver shines.

Step 1: Markup

Use a placeholder in src and store the real image URL in data-src:

<img class="lazy" 
     src="placeholder.jpg" 
     data-src="real-image.jpg" 
     alt="Custom branded mug" 
     width="800" height="600">

Step 2: JavaScript

document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = document.querySelectorAll("img.lazy");

  if ("IntersectionObserver" in window) {
    const imageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.classList.remove("lazy");
          observer.unobserve(img);
        }
      });
    }, {
      rootMargin: "200px 0px"
    });

    lazyImages.forEach(function(img) {
      imageObserver.observe(img);
    });
  } else {
    // Ultra-old browser fallback: just load everything
    lazyImages.forEach(function(img) {
      img.src = img.dataset.src;
    });
  }
});

The rootMargin: "200px 0px" option preloads images 200 pixels before they enter the viewport, which feels instant to the user.

Method 3: The Hybrid Approach (Recommended)

Use both. Native first, JavaScript only when needed:

<img src="image.jpg" 
     data-src="image.jpg" 
     loading="lazy" 
     class="lazy" 
     alt="Product" 
     width="800" height="600">

Then in JS, detect support and only run the observer when the native attribute isn’t supported:

if ("loading" in HTMLImageElement.prototype) {
  // Native lazy loading is supported, do nothing
} else {
  // Load IntersectionObserver script as fallback
}
fast website loading

Real Performance Comparison: Before vs After

We ran tests on a sample product gallery page with 32 images totaling 4.8 MB, using Chrome DevTools throttling at “Fast 3G” on a Moto G Power profile.

Metric Without Lazy Loading With Lazy Loading Improvement
Initial Page Weight 5.2 MB 0.9 MB -83%
Largest Contentful Paint (LCP) 4.1 s 1.7 s -58%
Time to Interactive (TTI) 5.8 s 2.4 s -59%
Total Image Requests on Load 32 6 -81%
Lighthouse Performance Score 62 94 +32 pts

The biggest takeaway: a single HTML attribute pushed our Lighthouse score from “needs improvement” to “good” without any build tooling, plugins, or refactoring.

Lazy Loading CSS Background Images

The native attribute only works on <img> and <iframe>. For background images, use IntersectionObserver with a CSS class swap:

const bgObserver = new IntersectionObserver(function(entries) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add("bg-loaded");
      bgObserver.unobserve(entry.target);
    }
  });
});

document.querySelectorAll(".lazy-bg").forEach(function(el) {
  bgObserver.observe(el);
});

Then in CSS:

.lazy-bg.bg-loaded {
  background-image: url("hero.jpg");
}
fast website loading

Common Mistakes to Avoid

  • Lazy loading the LCP image tanks your performance score instead of helping it.
  • Forgetting width and height causes cumulative layout shift, which Google penalizes.
  • Loading every image at once on scroll defeats the purpose. Always observe individually.
  • Using a heavy JavaScript library when the native attribute already works fine.
  • Not testing on real devices, only on desktop, where bandwidth issues are invisible.

FAQ

Does lazy loading hurt SEO?

No. Google’s crawler supports both native lazy loading and IntersectionObserver-based lazy loading. As long as you use proper alt attributes and don’t hide images from the rendered DOM, your images will still be indexed.

Should I lazy load every image on the page?

No. Never lazy load images visible in the initial viewport, especially the LCP image. Lazy load only what’s below the fold.

Does loading="lazy" work on all browsers?

Yes for all modern browsers in 2026. Chrome, Firefox, Safari, Edge, and Opera all support it. For very old browsers, the attribute is simply ignored and the image loads normally, so there’s no breakage.

Why isn’t my loading="lazy" working?

The most common reasons are: (1) the image is already in the viewport so the browser loads it immediately, (2) you forgot to set width and height, or (3) you’re testing on a very fast connection where the browser preloads aggressively.

Is IntersectionObserver still needed in 2026?

Rarely for images, since native support is universal. However, it’s still the right tool for background images, custom triggers, infinite scroll, and animation-on-scroll effects.

Can I lazy load videos and iframes too?

Yes. <iframe loading="lazy"> is widely supported. For videos, use the preload="none" attribute and a poster image.

Final Thoughts

Lazy loading images used to require complex libraries and clever workarounds. In 2026, you can get 80% of the benefit by adding nine characters to each image tag. Combine the native attribute with an IntersectionObserver fallback for special cases, and you’ll deliver a faster, lighter, and more SEO-friendly experience to every visitor. Start with your heaviest gallery or product page, run a Lighthouse test before and after, and watch the numbers improve.