/

loading images in a grid

High quality images can take a few seconds to load, even on fast internet connections, but users expect websites to be interactive in much under that. Browsers typically handle this by loading in the text and layout information first, while loading the images in the background. When an image is ready for display, it appears on the page.

While this works great for most scenarios, this random popping into view can be jarring when there are many images in view at once, such as in a grid layout. When load times are slow, the problem is only made worse.

Loading before optimization (throttled connection)

My Implementation

The solution is to load in placeholder boxes immediately, both to signify the presence of content and to avoid a layout shift when the images load. Then, once all the images have finished loading, fade them in over the placeholders with a quick animation. This ends up being much more pleasing, even on slower connections.

After optimization (same throttling speed)

The images we want to load in this “batch” are each contained in a div with a light gray background. Since the images themselves start completely transparent, these containers are what we see when the page first loads.

<div class="image-container">
    <img class="batch-load" src="">
</div>
.image-container {
    background-color: #ebebeb;  /* Image containers are given a light gray background */
}

img.batch-load {
    opacity: 0;                 /* Images start completely transparent */
    transition: opacity 0.4s;   /* When they load in, they are animated slightly */
}

The JS waits until all the images are loaded before showing them to the user. If they all haven’t finished loading in a set amount of time (2.5 seconds in this case), we should just show what we have so far.

// Stolen from https://stackoverflow.com/questions/11071314/
// Triggers when all the batch-loaded images have finished loading
Promise.all(Array.from(document.querySelectorAll('.batch-load')).filter(img => !img.complete).map(img => new Promise(resolve => {
    img.onload = img.onerror = resolve;
}))).then(() => {
    setImageOpacity(1)   // Makes the images visible
});

// If it's taken 2.5s and the images haven't finished loading, just show whatever we have
setTimeout(function() {
    setImageOpacity(1)   // Makes the images visible
}, 2500);

function setImageOpacity(opacity) {
    document.querySelectorAll('.batch-load').forEach(function(image) {
        image.style.opacity = opacity;
    });
}

Finally, if JS execution is disabled, we want to make sure that the images are still visible, so this bit gets added to the site footer just in case.

<noscript>
    <style>
        img {
            opacity: 1 !important;
        }
    </style>
</noscript>

This method won’t play well if you are trying to lazy load the images as well, so it shouldn’t be used on huge lists of images. Modifying the effect to only apply to images that are first visible to the user (maybe the first 10 images or so) may help with longer pages.