Lazy Loading Images

Native Lazy Image Loading With JavaScript Fallback

Scroll down for a live example.

This technique uses the browser-native loading="lazy" attribute with an IntersectionObserver JavaScript fallback. Very old browsers that don't support either will load images normally.

The HTML

Indicating native image dimensions with the width & height attributes and an inline placeholder image prevent content reflow while the data-src attribute stores the path of the real/final image:

HTML
<img src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20IMAGEWIDTH%20IMAGEHEIGHT'%3E%3C/svg%3E" data-src="REAL IMAGE PATH" width="IMAGE WIDTH" height="IMAGE HEIGHT" alt="existing alt text" loading="lazy">

<!-- no-JS fallback -->
<noscript><a href="REAL IMAGE PATH">view image</a></noscript>

(If the <img> is inside a link, place the <noscript> fallback outside the <a>.)

The CSS

Image placeholders can be styled as desired:

CSS
/*--- optional styling for placeholder images ---*/
img[data-src] {background-color:rgba(0,0,0,.1)}

The JavaScript

A little vanilla JavaScript detects support for loading="lazy" or IntersectionObserver (or neither) and updates the HTML accordingly:

JS
// FALLBACK FOR NATIVE (loading="lazy") LAZY IMAGE LOADING

// target <img>s with data-src attribute
var lazyimages = document.querySelectorAll('img[data-src]');

// IntersectionObserver IS supported AND native lazy loading is NOT (newer but not newest browsers)
if ('IntersectionObserver' in window && !('loading' in HTMLImageElement.prototype)) {
    // lazy load images
    var imageObserver = new IntersectionObserver(function(entries, observer) {
        entries.forEach(function(entry) {
            if (entry.isIntersecting) {
                var image = entry.target;
                image.src = image.dataset.src;
                image.removeAttribute('data-src');
                imageObserver.unobserve(image);
            }
        });
    }, {rootMargin:'500px 0px'}); // 500px buffer

    lazyimages.forEach(function(image) {
        imageObserver.observe(image);
    });
}

// native lazy loading IS supported OR IntersectionObserver is NOT (very new or very old browsers)
else {
    // replace src value with data-src value
    for (var i = 0; i < lazyimages.length; i++) {
        lazyimages[i].src = lazyimages[i].dataset.src;
        lazyimages[i].removeAttribute('data-src');
    }
}

Minified:

JS
var lazyimages=document.querySelectorAll("img[data-src]"); if("IntersectionObserver"in window && !("loading"in HTMLImageElement.prototype)){ var imageObserver=new IntersectionObserver(function (entries,observer) { entries.forEach(function(entry){ if(entry.isIntersecting){var image=entry.target; image.src = image.dataset.src; image.removeAttribute("data-src"); imageObserver.unobserve(image)}})},{rootMargin:"500px 0px"}); lazyimages.forEach(function(image){imageObserver.observe(image)})} else{for(var i=0; i< lazyimages.length; i++){lazyimages[i].src=lazyimages[i].dataset.src; lazyimages[i].removeAttribute("data-src")}}
Browser Support

The audience of any particular website will vary, but as of 2020 many if not most users can take advantage of native lazy loading while some will use the JavaScript fallback and a few will load images right away.

  • ~70% support loading="lazy"
    Chrome, Firefox, Edge (Blink), Opera

  • ~21% support IntersectionObserver (but not loading="lazy")
    Safari, Edge (pre-Blink)

  • ~9% support neither
    IE, older Safari & other old browsers

(In the interest of simplicity and forward-leaning development, this setup does away with the EventListener JavaScript method that was common prior to IntersectionObserver and is sometimes still used as a fallback.)

iframe Options

This setup can also support <iframe>s by adding an iframe[data-src] selector, however many uses of <iframe>s may have a better alternative, for example on-demand loading for embedded videos like YouTube.

Live Example

Lazy loading these images saves up to 240KB of initial-load page weight.