Image lazy loading is one of the more popular approaches of optimizing websites due to the relatively easy implementation and large performance gain. With lazy loading we load images asynchronously, meaning that we load images only when they appear in the browser's viewport.
Almost a year ago, native lazy loading for images and iframes was released for Chrome and other major browsers. The point of the feature is to give browsers control when to request an image or iframe resource, which makes dev jobs a bit easier. Up to that point, only option was to use various JavaScript plugins which monitored the viewport changes and loaded resources dynamically. Now browsers can do that natively!
At the time of writing this article, around 73% of currently used browsers support this feature which is not bad, but we don't want to make the website image content inaccessible and unusable to 27% of potential users.
So this puts us in an interesting situation:
- We want to use the benefits of native lazy loading for browsers that support it
- We want to use a JS plugin as fallback for lazy loading for browsers that don't support it
- We don't want to load the lazy loading JS plugin if the browser supports native lazy loading.
- Support both
img
andsource
elements is mandatory
The "loading" attribute
We have three possible values that we can use for loading
attribute.
-
auto
- default value. Same as not setting the attribute. -
eager
- load the resource immediately. -
lazy
- load the resource once it's in the viewport.
Although it depends on the use-case, generally we want to use eager
value for resources above the fold and lazy
value for resources below the fold.
Modern approach
We need to write a script that will run after the HTML document is loaded. I've used Jekyll and added the script as an include that was appended to the end of the HTML body
element. This is the most effective way of running JavaScript functions to avoid render blocking.
Image markup
We want our JavaScript function to start the image loading process based on the native lazy loading feature support. To achieve that we'll add the path to our images to data-src
instead of src
. But we shouldn't leave src
empty, so we'll use 1x1px transparent image placeholder. Our markup for img
elements will look something like this
<img
src="/path/to/placeholder/image.png"
data-src="/path/to/full/image.jpg"
alt="Image description"
class="lazyload"
loading="lazy"
/>
Please note that class="lazyload"
is used by the lazyload fallback plugin. I've used lazysizes that uses this particular class name.
Additionally, we want to support picture
element that contains multiple source
element and fallback img
element.
<picture>
<source data-srcset="path/to/image.webp" type="image/webp" />
<source data-srcset="path/to/image.jpg" />
<img loading="lazy"
class="lazyload"
src="path/to/placeholder/image.png"
data-src="path/to/image.jpg"
alt="Image description"
/>
</picture>
Feature detection
We need to detect if user's browser supports native lazy loading. Luckily, we can do that using JavaScript directly.
if ("loading" in HTMLImageElement.prototype) {
/* Native lazy loading is supported */
} else {
/* Native lazy loading is not supported */
}
Final JavaScript code
For native lazy loading, we only need to assign data-src
value to src
value for img
and source
elements and let the browser handle the rest.
For unsupported browsers, we only need to load the JavaScript plugin and, optionally, run it (if not done automatically). I've used lazysizes but any plugin will work, just make sure that the markup is correct (class names, data elements, etc.).
So the final JavaScript code will look something like this:
<script>
if ("loading" in HTMLImageElement.prototype) {
var images = document.querySelectorAll('img[loading="lazy"]');
var sources = document.querySelectorAll("source[data-srcset]");
sources.forEach(function (source) {
source.srcset = source.dataset.srcset;
});
images.forEach(function (img) {
img.src = img.dataset.src;
});
} else {
var script = document.createElement("script");
script.src = "/link/to/lazyload.js";
document.body.appendChild(script);
}
</script>
Boosted performance & Lighthouse score
On my personal website I've used a JavaScript plugin for image lazy loading for all browsers. After implementing this modern approach, I've eliminated one JavaScript file that is being loaded and parsed on website load which in turn boosted my Lighthouse score and overall performance!
More image optimization techniques for maximum performance
Lazy loading is one of many ways to optimize image performance on the web. I've wrote this in-depth posts that covers other important techniques and aspects of image optimization for the web like web-specific image formats, using CDN, progressive images, etc.
Optimizing images for the web - an in-depth guide
Adrian Bece for PROTOTYP ・ Oct 1 '19
Improving website performance by eliminating render-blocking CSS and JavaScript
Adrian Bece for PROTOTYP ・ Aug 31 '20
These articles are fueled by coffee. So if you enjoy my work and found it useful, consider buying me a coffee! I would really appreciate it.
Thank you for taking the time to read this post. If you've found this useful, please give it a ❤️ or 🦄, share and comment.
Top comments (9)
Interesting approach with the "1x1px transparent image placeholder" Adrian, it gave me some idea 👍
If that would be interesting, in our eco system we also published a Web Component to lazy load images.
If will first try to use native lazy loading, if not supported by the browser, will fallback on the intersection observer technique and if not supported, will display the image instantly.
Also supports src-set, svg and it is possible to define an img to display in case of error (sh*t happens).
I had to check its source code to remember what I implemented (😅). Instead of a blank pixel I hide (
opacity: 0
) the image and when loaded toggle its opacity to render, kind of "smooth", transition.Thanks again for the blog post, I might add a variable to set
lazy
(currently fix) oreager
as you explained above.Wow! Thank you very much. Glad you've found it useful. I haven't worked with Web components, I'll have to check them out at some point.
Regarding the 1x1 placeholder, you want an image to have a src set in any moment. I think that either the HTML validator throws an error or something breaks. Either way, it's semantically correct to have something there.
Cheers!
Thank you Adrian for the really clear add-ons explanation, it makes sense.
I have to give a try (again) to check how our Web Components handles this scenario, good point.
How does this allow the web crawlers to catalog things? Would you consider this something for ALL images / or just images in a infinite-scroll or otherwise web-app-ish type thing vs the core content of the web document?
In theory - you could load EVERYTHING but the document title and some h1's after page load and boost your score to 100%, right? ;) but then you'd have the same problem as a SPA.
Using the
with-preview
Custom Element is also an option 😉github.com/WebReflection/with-prev...
Nice approach. I'll have to try this in my next project.
Lazysizes also has support for native lazy loading as well.
Quite interesting way. Did you consider to use Observable for lazy loading (e.g: loading only when the image must be shown/render?
Thank you. I went with native lazy loading (browser implementation) and avoid JS altogether for best performance and less dependencies. But for fallback JS plugin, you can use the Observable. I think that more popular plugins use them by default.