Wednesday, June 12, 2019

Lazy Load Images with Javascript

A website's page speed is an important factor that should be taken into consideration when optimizing a website. Lazy loading images definitely plays a role in improving page speed by loading images asynchronously.

Lazy Load Images with Javascript
Lazy loading images is an approach used to load an image only when its visible in viewport. All images which are off the screen won't load. This approach reduces the load time of a page and increases a loading speed. Lets start writing code

index.php

Contains html code for our page and using a for loop we are going to show same image twenty times with class "lazy-image" added to it. We are not assigning any "src" attribute to image but rather we use "data-src" attribute which will hold the real path of the image. We will use this "data-src" to load image's src using javascript if the image is visible to viewport.
<!DOCTYPE html>
<html>
    <head>
        <title>Lazy Load Images with Javascript - Demo</title>
        <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <div class="main-container">
            <div class="section">
                <?php for ($i = 1; $i <= 20; $i++) { ?>
                    <img class="lazy-image" data-src="images/lazy-image.jpg" alt="Lazy Load Image <?php echo $i;?>" title="Lazy Load Image <?php echo $i;?>" />
                <?php } ?>
            </div>
        </div>
    </body>
    <footer>
        <script type="text/javascript" src="js/javascript.js"></script>
    </footer>
</html>

javascript.js

We are using event handlers which is most compatible way to lazy load images. Select all the images with class "lazy-image" and loop through all the images, next we check if any of image is visible in viewport we load the src of image from its dataset src.
document.addEventListener("DOMContentLoaded", function () {
    let active = false;
    let lazy_images = [].slice.call(document.querySelectorAll(".lazy-image"));

    const lazyLoad = function () {
        if (active === false) {
            active = true;

            setTimeout(function () {
                lazy_images.forEach(function (lazy_image) {
                    if ((lazy_image.getBoundingClientRect().top <= window.innerHeight && lazy_image.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazy_image).display !== "none") {
                        lazy_image.src = lazy_image.dataset.src;
                        lazy_image.classList.add("visible");
                        lazy_image.removeAttribute("data-src");

                        lazy_images = lazy_images.filter(function (image) {
                            return image !== lazy_image;
                        });

                        if (lazy_images.length === 0) {
                            document.removeEventListener("scroll", lazyLoad);
                            window.removeEventListener("resize", lazyLoad);
                            window.removeEventListener("orientationchange", lazyLoad);
                        }
                    }
                });

                active = false;
            }, 200);
        }
    };

    document.addEventListener("scroll", lazyLoad);
    window.addEventListener("DOMContentLoaded", lazyLoad);
    window.addEventListener("resize", lazyLoad);
    window.addEventListener("orientationchange", lazyLoad);
});

Another way to lazy load images is use javascript's intersection observer which easier way but it has cross browser compatibility issues and requires more work around to fix compatibility.
document.addEventListener("DOMContentLoaded", function() {
    var lazyImages = [].slice.call(document.querySelectorAll(".lazy-image"));

    if ("IntersectionObserver" in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    lazyImage.src = lazyImage.dataset.src;
                    lazyImage.classList.add("visible");
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        },{rootMargin: "0px 0px -120px 0px"});

        lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
        });
    }
});

Now if you dont want to add image-lazy class to existing images or if you feel its frustrating to add this class to all of images to lazy load. This small piece of code will do the job for you, place this code right before lazy load code, This code will add "lazy-image" class to all of images.
(function(document){
        let images = [].slice.call(document.querySelectorAll("img"));
        images.forEach(function (image) {
            image.dataset.src = image.src;
            image.src = "";
            image.classList.add("lazy-image");
        });
 })(document);

style.css

Styles for our html page and lazy images.
*{
    box-sizing: border-box;
}
html,body{
    margin: 0px;
    padding: 0px;
}
body{
    background: #f0f0f0;
    font: normal normal 14px Open Sans,Verdana, Arial;
}
.main-container{
    max-width: 1024px;
    margin: 0px auto;
}
.lazy-image{
    max-width: 100%;
    height: auto;
    margin: 10px auto;
    border: 5px solid #fff;
    box-shadow: 0px 0px 5px rgba(0,0,0,0.5);
    opacity: 0;
    display: block;
    transition: opacity ease-in .8s;
}
.lazy-image.visible{
    opacity: 1;
}