Lazy loading - Frontend • Jonathan Rixhon

Image representing the  post

Lazy loading - Frontend

In this tutorial, we'll explore how to implement lazy loading for images using HTML, vanilla JavaScript, and a touch of CSS. The Backend tutorial will be available on this website soon !

Tags

All the code is available on GitHub, versioned into branches, here.

What Are We Doing?

Small optimizations can make a big impact. By lazy loading images on our website, we can reduce the initial loading time and avoid looking at the ugly image loading animation.

Lazy loading essentially instructs the browser to load the page separately from its contents, ensuring a smoother user experience.

Thanks Johnny, so now how we're gonna do it ?

In this blog, we don't do things halfway. We're going to build a small Laravel blog that implements image lazy loading with vanilla JavaScript, some SCSS, for the frontend, and a complete image conversion system for the backend (well, two systems to be honest).

To create this article, I followed this tutorial and applied it across this website !

So, what do we need to make it work?

  1. A way to tell the browser to lazy load the image,
  2. A technique to hide the image while it's loading.
  3. Optional: A hyper-lightweight version of the image to use for a loading animation (backend developers, this is where you come in).
  4. Make it your way to better fit your needs.

Browser Lazy Loading

There are two main approaches to implement lazy loading:

  1. Use the loading="lazy" HTML attribute.
  2. Use a data-src attribute to store the image URL and inject it into the src attribute using JavaScript once the DOM is loaded.

The loading="lazy" method has decent browser support, so we'll use this solution. However, if you follow this article, you should be comfortable with both approaches.

<img src="https://jonathanrixhon.dev/storage/rickroll.png" loading="lazy" alt="DON'T FORGET TO COMPLETE THIS">
<!-- OR -->
<img src="" data-src="https://jonathanrixhon.dev/storage/rickroll.png" alt="No, seriously, complete it…">

Hiding the Image

Well, have you ever eard about css opacity? Now you have.

To implement lazy loading, we need to select our image and hide it. Then, we need a way to reveal it once it's fully loaded. You can use an HTML class or another attribute for this purpose.

I prefer using an HTML attribute because I like to reserve classes for styling. I view this attribute more like a disabled attribute for a button it's more of a status than a style. This also allows us to separate the lazy loading logic from the rest of the styles.

In your HTML file:

<img  data-lazyload="loading"  >
<!-- When loaded -->
<img  data-lazyload="loaded"  >

In your app.scss (or .css):

// Global style for lazyload images
[data-lazyload] {
    transition: opacity 200ms ease-in-out;
}

// Hide image while loading
[data-lazyload="loading"] {
    opacity: 0;
}

// Show image when loaded
[data-lazyload="loaded"] {
    opacity: 1;
}

Let's manage the loading state with a bit of JavaScript that will check when the image is loaded and change data-lazyload="loading" to data-lazyload="loaded".

In your app.js :

// Wait for page loading
window.addEventListener('load', () => {
    // Get all lazy images and create a LazyImage object for each
    document.querySelectorAll('[data-lazyload]').forEach(image => new LazyImage(image));
});

// Your awsome LazyImage Class
class LazyImage {
    constructor(image) {
        this.img = image;
        this.init();
    }

    init() {
        // For those who prefer the data-src version.
        if (this.img.dataset.src) {
            this.img.src = this.img.dataset.src;
        }

        // The complete property tells us if the image is loaded
        // If the image is not loaded, add an event listener to trigger when it is loaded
        this.img.complete
            ? this.loaded()
            : this.img.addEventListener("load", e => this.loaded(e))
    }

    loaded() {
        // Change the data-lazyload="loading" to data-lazyload="loaded".
        this.img.setAttribute('data-lazyload', 'loaded');
    }
}

Blurry Backgrounds

Remember when we mentioned using an hyper-lightweight version of the image for a smooth transition? Well, this will slightly modify our code.

We need to display a very small version of the image (20px wide should ok) that downloads in no time. Instead of using a second img tag, we'll use this small image as a background image. To keep things semantic and style-friendly, wrap the img in a picture tag.

I prefer not setting styles directly in the HTML. Instead, I use CSS variables, as they can modify multiple properties at once and having a fallback value if not set.

The loading="lazy" version:

<!-- picture tag around our lazy image, with a style "--img" CSS variable used as a background image -->
<picture data-lazyload="loading" style="--img: url(https://jonathanrixhon.dev/storage/rickroll-lazy.png);">
    <img src="https://jonathanrixhon.dev/storage/rickroll.png" loading="lazy">
</picture>

The data-src version:

<!-- picture tag around our lazy image, with a style "--img" CSS variable used as a background image -->
<picture data-lazyload="loading" style="--img: url(https://jonathanrixhon.dev/storage/rickroll-lazy.png);">
    <img data-src="https://jonathanrixhon.dev/storage/rickroll.png">
</picture>

In your app.scss :

[data-lazyload] {
    display: block;

    // Set the background image
    background-image: var(--img, none);
    background-color: grey;
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
    transition: all 200ms ease-in-out;

    // Set transition for img
    img {
        display: block;
        transition: all 200ms ease-in-out;
    }
}

// Hide and blur image while loading
[data-lazyload="loading"] {
    filter: blur(4px);
    img {
        opacity: 0;
    }
}

// Show image when loaded, no need for filter as it's only for the loading state
[data-lazyload="loaded"] {
    img {
        opacity: 1;
    }
}

In your app.js:

window.addEventListener('load', () => {
    document.querySelectorAll('[data-lazyload]').forEach(picture => new LazyImage(picture));
});

class LazyImage {
    constructor(picture) {
        // Work with the image container
        this.picture = picture;

        // This works for both versions; modify the querySelector if only using one
        this.img = this.picture.querySelector('[loading="lazy"], [data-src]');

        if (this.img) this.init();
    }

    init() {
        // For those who prefer the data-src version.
        if (this.img.dataset.src) {
            this.img.src = this.img.dataset.src;
        }

        this.img.complete
            ? this.loaded()
            : this.img.addEventListener("load", e => this.loaded(e))
    }

    loaded() {
        this.picture.setAttribute('data-lazyload', 'loaded');
    }
}

Conclusion

Pretty easy, right? Now you can customize it however you like. With the provided selectors, you can target exactly what you need, and since your image is inside a <picture> tag, you have the flexibility to get creative!

You can use ::before and ::after elements to create awesome effects, such as loading spinners or moving gradients over the image. You could even integrate an Intersection Observer for further enhancements.

Comment from CRAZY VOID

I did this on my websites and became instantly rich, 10/10

Comment