Lazy loading - Frontend
8th of September 2024
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?
- A way to tell the browser to lazy load the image,
- A technique to hide the image while it's loading.
- Optional: A hyper-lightweight version of the image to use for a loading animation (backend developers, this is where you come in).
- Make it your way to better fit your needs.
Browser Lazy Loading
There are two main approaches to implement lazy loading:
- Use the
loading="lazy"
HTML attribute. - Use a
data-src
attribute to store the image URL and inject it into thesrc
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