Daniel Stokes

Simple Progressive Image Loading Guide

February 19th 2021 4:31pm
Updated: February 19th 2021 5:54pm

Web Dev

Images are typically the largest network load for a webpage and can often cause delays or layout shifting which have a negative affect on user experience. For my website I wanted to tackle this issue and so here is my solution.

See my photography page for an example of this in action.

The basics of my approach is to wrap my images in a div that is the same size as my images. This div has a special class applied called image-lazy which is picked up by our javascript to execute the loading. The div also has two data- attributes that store the low res and high res source paths for the resulting image. When the page loads we query the page for our special divs and create whats called a intersection observer for them to trigger the loading when we scroll near them.


My approach uses a two stage loading one low res and then another high res that is transitioned in but you could easily just do the one.

page.html

	<div class="image-lazy" data-lowsrc="lowres.jpg" data-highsrc="highres.jpg">
</div>
	

styles.css

	.image-lazy img{
  filter: blur(15px);
  transition: filter 0.5s;
  clip-path: border-box;
  pointer-events: none;
}

.image-lazy img.loaded{
  filter: blur(0px);
}

@keyframes placeHolderShimmer{
    0%{
        background-position: -468px 0
    }
    100%{
        background-position: 468px 0
    }
}
  .image-lazy:not(.loaded) {

    background: rgb(238,238,238);
    background: linear-gradient(70deg, rgba(238,238,238,1) 8%, rgba(221,221,221,1) 25%, rgba(238,238,238,1) 45%);
    animation-duration: 1.25s;
    animation-fill-mode: forwards;
    animation-iteration-count: infinite;
    animation-name: placeHolderShimmer;
    animation-timing-function: linear;
    background-repeat: repeat;
    -webkit-background-size: 800px 104px;
background-size: 1200px 100%;
  }
	

app.js

	let lazyImages = document.querySelectorAll(".image-lazy");
let options = {
    root: null,
    rootMargin: '500px',
    threshold: 0
}


let callback = (entries, observer) => {
    entries.forEach(entry => {
        if(entry.isIntersecting){
            let img = document.createElement("img");
            img.classList.add("img-responsive");
            img.width="100%";
            img.addEventListener("load", function(){
                entry.target.classList.add("loaded");
                if(this.src != this.dataset.src){
                    let item = this;
                    var newImage = new Image;
                    newImage.addEventListener('load', function(e){
                        item.src = item.dataset.src;
                        item.classList.add("loaded")
                        newImage.remove();
                    });
                    newImage.src = this.dataset.src;
                }
            })
            img.src = entry.target.dataset.lowsrc;
            img.dataset.src = entry.target.dataset.highsrc;
            entry.target.insertBefore(img, entry.target.firstChild);
            observer.unobserve(entry.target);
        }
    });
};

let observer = new IntersectionObserver(callback, options);
lazyImages.forEach( (e) => {
    observer.observe(e);
});
	

You could easily tweak this to load more images by increasing options rootMargin variable. If you would prefer to load a les pixelated starting image than I did then you might want to tone down the blur css filter.

Comments