How do you make images load lazily only when they are in the viewport?

Simple solution that does not depend on JQuery:

    <script type="text/javascript">
        refresh_handler = function(e) {
        var elements = document.querySelectorAll("*[realsrc]");
        for (var i = 0; i < elements.length; i++) {
                var boundingClientRect = elements[i].getBoundingClientRect();
                if (elements[i].hasAttribute("realsrc") && boundingClientRect.top < window.innerHeight) {
                    elements[i].setAttribute("src", elements[i].getAttribute("realsrc"));
                    elements[i].removeAttribute("realsrc");
                }
            }
        };

        window.addEventListener('scroll', refresh_handler);
        window.addEventListener('load', refresh_handler);
        window.addEventListener('resize', refresh_handler);
    </script>

<img loading="lazy" does it without any Javascript

We are now getting more and more support for this standardized no-JavaScript method, which is very exciting!

  • https://caniuse.com/#feat=loading-lazy-attr
  • https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-loading

You can see this at work in the code snippet below.

To see that the loading is actually lazy, open Chrome DevTools in the Network tab.

Then, as you scroll down the snippet, you will see that the images only load when you see them.

I've also added an optional JavaScript button to show that you can change lazy back to the default eager from JavaScript, and images will then start to load immediately.

document.getElementById('load-now').addEventListener('click', function(){
  for (const img of document.getElementsByTagName('img')) {
    img.loading = 'eager';
  }
});
.separator {
    height: 1000px;
    width: 100px;
    border: 5px solid red;
}
img {
    height: 340px;
    border: 5px solid black;
}
  #load-now {
  border: 5px solid black;
  }
<div id="load-now">Click me to load all images now!</div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/5/56/Donald_Trump_official_portrait.jpg"></div>
<div class="separator"></div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/8/8d/President_Barack_Obama.jpg"></div>
<div class="separator"></div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/d/d4/George-W-Bush.jpeg"></div>
<div class="separator"></div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/d/d3/Bill_Clinton.jpg"></div>
<div class="separator"></div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/9/90/George_H._W._Bush%2C_President_of_the_United_States%2C_1989_official_portrait_%28cropped%29.jpg"></div>
<div class="separator"></div>
<div><img loading="lazy" height="340" src="https://upload.wikimedia.org/wikipedia/commons/1/16/Official_Portrait_of_President_Reagan_1981.jpg"></div>

One really cool thing about this method is that it is fully SEO friendly, since the src= attribute contains the image source as usual, see also: Lazy image loading with semantic markup

Tested in Chromium Chromium 81 and Firefox 77.0.1, both worked and loaded lazily.

IntersectionObserver minimal runnable example

This is a JavaScript method that would work before img loading="lazy" was implemented.

This is essentially the technique used at: https://appelsiini.net/projects/lazyload/ which was mentioned at: https://stackoverflow.com/a/2322042/895245

Web APIs have evolved so much now that it is not hard to code it from scratch!

var observer = new IntersectionObserver(
    (entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0.0) {
                img = entry.target;
                if (!img.hasAttribute('src')) {
                    alert('will load the image!!!');
                    img.setAttribute('src', img.dataset.src);
                }
            }
        });
    },
    {}
)
for (let img of document.getElementsByTagName('img')) {
    observer.observe(img);
}
.separator {
    height: 1000px;
    width: 100px;
    border: 5px solid red;
}
img {
    height: 340px;
    border: 5px solid black;
}
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/5/56/Donald_Trump_official_portrait.jpg"></div>
<div class="separator"></div>
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/8/8d/President_Barack_Obama.jpg"></div>
<div class="separator"></div>
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/d/d4/George-W-Bush.jpeg"></div>
<div class="separator"></div>
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/d/d3/Bill_Clinton.jpg"></div>
<div class="separator"></div>
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/9/90/George_H._W._Bush%2C_President_of_the_United_States%2C_1989_official_portrait_%28cropped%29.jpg"></div>
<div class="separator"></div>
<div><img data-src="https://upload.wikimedia.org/wikipedia/commons/1/16/Official_Portrait_of_President_Reagan_1981.jpg"></div>

Full page demo: https://cirosantilli.com/web-cheat/js-image-load-viewport.html

GitHub upstream: https://github.com/cirosantilli/cirosantilli.github.io/blob/1f637bf4791b115777300f48f427f0a6bb409fc1/web-cheat/js-image-load-viewport.html

This technique is just a combination of:

  • What is the best JavaScript code to create an img element
  • How can I tell if a DOM element is visible in the current viewport?

Tested in Chromium 76.

Change load order to nearest first

This is the last use case missing after loading="lazy" for me: a method that downloads eagerly, but changes the download order to download first on viewport, then below, and then above: Change loading order of images already on page

Maybe we can do something with querySelectorAll() to solve jQuery find next/prev elements of a certain class but not necessarily siblings and then remove loading=lazy from images in the JavaScript! This would both degrade gracefully, and be SEO friendly.

The last issue is how to get the first visible element though:

  • How to get the first DOM element that is visible in a viewport?
  • How to select the last element on viewport

I haven't seen a very good solution for this yet.

Lazy load video

Not sure why, but neither Chromium 81 nor Firefox 77.0.1 can lazy load video, now sure why did they do it just for img?

Chromium 81 did implement it for iframe however, which is what YouTube embeds use, while Firefox 77.0.1 didn't: lazy load iframe (delay src http call) with jquery


Replace your images with placeholders (e.g. just change the "src" attribute to something else so the image won't load, but the url will still be accessible), and then bind the window scroll event to a function which will find all images at the current scroll position, and swap the image src into a real img tag.

Here's the code. It's untested, but this should be the basic idea:

<img src="" realsrc="/myimage.png" />

$(document).ready(function(){

  $(window).scroll(function(){
    $('img[realsrc]').each(function(i){
      var t = $(this);
      if(t.position().top > ($(window).scrollTop()+$(window).height()){
        t.attr('src', t.attr('realsrc')); // trigger the image load
        t.removeAttr('realsrc'); // so we only process this image once
      }
    });
  })

});

http://www.appelsiini.net/projects/lazyload
https://github.com/tuupola/jquery_lazyload

Demo:
http://www.appelsiini.net/projects/lazyload/enabled.html