tl;dr
The code is pretty easy to grasp. Read the comments and you should be good to go.
<div class="img-lazy-wrapper"> <!-- We need a wrapper for the placeholder while image lazy loads -->
<img
src="/assets/lazy-bear@528w.96b21f25.png" <!-- Compress your image -->
alt="A lazy bear" <!-- Don't forget the alt attribute -->
loading="lazy" <!-- Native browser lazy loading -->
width="528" <!-- Specify width and height so the browser can allocate the appropriate space for the image -->
height="560"
class="img-lazy"
onload="this.parentNode.style.backgroundColor = 'transparent';this.style.opacity = 1;">
<!-- Transition: Fade the image in and the background out -->
</div>
<style>
.img-lazy {
opacity: 0; /* Start with image hidden */
transition: opacity 1s; /* Transition to make the appearance smoother */
max-width: 100%; /* Avoid image punching out of the screen */
height: auto; /* Necessary to keep aspect ratio if width is reduced by max-width */
}
.img-lazy-wrapper {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content; /* Make the wrapper borders stick to the image. Otherwise, would stretch horizontally */
transition: background-color 1s; /* Make disappearance smoother */
background-color: var(--color-img-placeholder, rgba(200, 200, 200, 0.3)) /* Basic placeholder */
}
</style>
<noscript><style>
/* Overrides opacity and background-color in case we have no JS to get them it back */
.img-lazy { opacity: 1; }
.img-lazy-wrapper { background-color: transparent }
</style></noscript>
Intro
Us developers really like to reinvent the wheel. Lazy loading images has been a topic of choice to do that.
Nevertheless, we couldn't easily find an elegant, native, lightweight solution we like to implement by default on poko. So here is our solution for now.
Our prerequisites
-
Use the platform as much as possible
-
Should provide a minimum viable experience on older browsers and without javascript
-
Progressively enhanced (image placeholder while loading, transitions, ...)
A solution
-
Compress images
-
Determine the dimensions of each image
-
Activate native lazy loading in browser
-
Progressively enhance with some styles and two lines of javaScript.
1. Compress images
There are many ways to do it and your development pipeline might have a mature native solution already.
If you are not a tech person and you are not sure your CMS or website builder handles it for you, there are many tools online to compress images.
In our case, using Astro, we simply used astro-imagetools
to compress images automatically while importing them. We might improve this later, for example generating multiple image sizes and formats, reducing the load even further for modern browsers. Currently, it checks our box though.
2. Determine the dimensions of each image
This is necessary to make use of some native browser image handling. Providing width
and height
attributes to <img>
tags allows the browser to reserve space for your image before it loads. It is necessary to avoid having content jump around when images load. It used to not be enough to set these up but now it is.
Again, there are multiple ways to get dimensions and your development pipeline might just provide this information by default. In case it doesn't, we have found a nice and small npm package called probe-image-size
. It allows for 'probing' an image locally or remotely.
import probeImageSize from "probe-image-size";
const imageData = await probeImageSize(url);
console.log(imageData);
// {
// url: 'https:// ...',
// width: 528,
// height: 560,
// type: 'png',
// mime: 'image/png',
// wUnits: 'px',
// hUnits: 'px',
// length: 47951
// }
3. Activate native lazy loading in browser
This is the easy part. Simply set the attribute on your HTML <img>
element: <img ... loading="lazy" ... >
The HTML for the image becomes:
<img
src="/assets/lazy-bear@528w.96b21f25.png"
alt="A lazy bear"
loading="lazy"
width="528"
height="560">
4. Progressively enhance
Now in modern browsers, we have got a compressed image that is lazy loaded and an allocated space on the page before that. That is great!
Now we'd like to create a transition effect so that visitors understand that something is loading on the reserved space.
On the image itself, we can use the onload
attribute to 'do something' when the image is loaded.
The simplest placeholder we can think of is color. So let's color up the reserved space while the image is loading then fade out the color and fade in the image.
First some styles on the <img>
and a wrapper <div>
for the color.
<style>
.img-lazy {
opacity: 0;
transition: opacity 1s;
max-width: 100%;
height: auto;
}
.img-lazy-wrapper {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
transition: background-color 1s;
background-color: var(--color-img-placeholder, rgba(200, 200, 200, 0.3))
}
</style>
Some explanations:
-
max-width
andheight
is to allow scaling the image according to its context (the browser window or a parent element) -
width: fit-content;
(and its prefixed versions) is to avoid having the wrapper div stretch wider than the image. Instead it will stick to its content's dimensions. -
We load the page with 0
opacity
for the image (it will fade in while it loads) and abackground-color
on the wrapper div (it will fade out when the image loads). -
transition
is to create the fading effect.
Note: we would very much like to avoid the wrapper
<div>
but could not find an elegant alternative. Please share if you have one.
Our HTML becomes
<div class="img-lazy-wrapper">
<img
src="/assets/lazy-bear@528w.96b21f25.png"
alt="A lazy bear"
loading="lazy"
width="528"
height="560"
class="img-lazy"
onload="this.parentNode.style.backgroundColor = 'transparent';this.style.opacity = 1;">
</div>
Note the onload
attribute on the <img>
tag. That is the only javaScript we need to make the transition from placeholder to image.
Of course, if we rely on javaScript to display the image, we should have a fail safe mechanism for visitors without JS. In this case, we simply remove the transition effect.
<noscript><style>
/* Overrides opacity and background-color in case we have no JS to get them it back */
.img-lazy { opacity: 1; }
.img-lazy-wrapper { background-color: transparent }
</style></noscript>
Compatibility and final thoughts
According to my limited research (and the caniuse website), around 80% of users (in May 2022) should benefit from the whole technique. 20% of visitors will have a limited experience, either image is not lazy loaded, or content is jumping on image load, or the wrapping div will be wider than the image, or there will be no background transition effect... something like that. In any case, they won't have trouble seeing the content and that is the point.
We love solutions like these which are lightweight and use the platform as much as possible. Technical debt is reduced to a minimum. It makes them future proof and compatibility ratio will only increase with time.
Moreover, with poko we are not forcing this on anyone since using this component is optional.