Skip to content

Automatically Loading High-Quality Images with Cloudinary and IntersectionObserver

Web performance is, or should be, one of the top concerns for any developer. This is certainly not a new issue, but it is refreshing to see our community put more and more emphasis on this topic over the past few years. Even better, it’s great to see browsers add additional support to help us in this endeavor — from devtools to performance tests and even to new APIs that make developers’ jobs easier. One of these APIs is the IntersectionObserver. This gives developers a way to determine when a particular DOM element has become visible in the browser’s viewport. In this article, I’ll give an introduction to the API and then demonstrate a way to use it with Cloudinary’s imaging APIs. ​

​ MDN defines the IntersectionObserver feature as such: ​

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

At a high level, what this means is that we can determine when an item, like an image, becomes visible based on either the viewport of the browser (the entire visible page) or within some other element (imagine a container div for example). The API can be fine-tuned to only inform you when an element is 100% visible, or some other percent. You can also add a margin around the root element being observed. ​ You begin by creating an instance of an IntersectionObserver object. This is done by passing in your options as well as a function to be called when an intersection happens. ​

const noticeImage = (entries,observer) => {
​
}
​
let options = {
  rootMargin: '0px',
  threshold: 0.3
}
​
let observer = new IntersectionObserver(noticeImage, options);
Code language: JavaScript (javascript)

​ In the above example, I did not specify a root option. This will then default to the browser’s viewport. I don’t want a margin so I’ve set it to 0. And I’ve specified a threshold of 30% for items. In other words, when 30% or more of the item is found, consider it intersected. ​ I’ve also specified an empty handler, noticeImage, that I’ll fill out in a moment.

Alright, at this point, I’ve defined an intersection and specified how to watch things within, but I’ve not actually told it what I want to observe. This is done by using the observe method on the IntersectionObserver object.

Let’s first add some HTML: ​

<div id="images">
	<img src="https://static.raymondcamden.com/images/2023/04/300_1.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/300_2.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/400_1.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/400_2.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/450.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/666.jpg">
	<img src="https://static.raymondcamden.com/images/2023/04/700.jpg">
</div>
Code language: HTML, XML (xml)

​ I’ve also got a bit of CSS to help space them out a bit: ​

#images img {
	display:block;
	margin: auto;
	padding-top: 75px;
	padding-bottom: 75px;
}
Code language: CSS (css)

​ Given that we’ve got images on our page, let’s grab all of them within that div element: ​

document.querySelectorAll('#images img').forEach(i => observer.observe(i));
Code language: JavaScript (javascript)

​ Now we’ve got our IntersectionObserver created and a set of elements it should be looking out for, let’s then flesh out our handler: ​

const noticeImage = (entries, observer) => {
	console.log('noticeImage');
	entries.forEach(e => {
		if(e.isIntersecting) {
			console.log('i noticed ', e.target.src);
			observer.unobserve(e.target);
		}
	});
}
Code language: JavaScript (javascript)

​ IntersectionObserver handlers are passed an array of items that have become visible (it’s an array since a user could scroll quickly enough for multiple items to become visible) as well as the original IntersectionObserver object. Each entry contains a number of properties you could use if desired. For now, I only check isIntersecting, and if true, I log a message out and then stop observing it. The value would be false if the item left the IntersectionObserver root area but since I know our demo is going to be doing one operation per item intersected, I only care when the value is true.

To test this, check out the CodePen below: ​

See the Pen Intersection by Raymond Camden (@cfjedimaster) on CodePen.

But you may have more luck viewing the live view of it here with your console open: https://codepen.io/cfjedimaster/live/mdGZxvw

​ Alright, now to have some fun with this by integrating Cloudinary. For our demo, we’re going to do two things: ​

  1. First, we will replace the original images with a lower quality, but still decent version. This will reduce the initial load of content and hopefully make for a better user experience.
  2. Then we will use the IntersectionObserver to make note of when one of our images is visible and replace it with a higher-quality version. What’s nice is that if for some reason the user does not have JavaScript enabled, or if something goes wrong in the code (my fault, not Cloudinary’s), then they still get an image, even if lower quality. You could also consider using an experimental API (Chromium only) like Navigator.connection and choose not to download a higher res image for people on slower networks. ​ Let’s start with the first part. My original images were all hosted like any other regular image. Here’s an example: ​
<img src="https://static.raymondcamden.com/images/2023/04/300_1.jpg">
Code language: HTML, XML (xml)

​ And the result: Cat image, raw

First, we will move it to Cloudinary’s CDNs by using the Fetch feature. This simply involves prepending my URL like so: ​

<img src="https://res.cloudinary.com/raymondcamden/image/fetch/https://static.raymondcamden.com/images/2023/04/300_1.jpg">
Code language: HTML, XML (xml)

​ The result is exactly the same: ​

Cat image, via Cloudinary

It isn’t exactly the same as Cloudinary will use its CDNs to deliver it to end users better and will cache the data as well.

Next, we’ll lower the quality a bit by using the quality transformation feature in Cloudinary. We can add a q_X parameter where the value of X can be a number from 0 to 100 (there are other options as well). For our test, we will use a 30% quality setting. Here’s that new version: ​

<img src="https://res.cloudinary.com/raymondcamden/image/fetch/q_30/https://static.raymondcamden.com/images/2023/04/300_1.jpg">
Code language: HTML, XML (xml)

​ And the result: ​ Cat image, via Cloudinary, lower quality

I can definitely tell a difference, but it’s certainly not bad and if for some reason the higher-quality version didn’t load, I think it would be fine. But let’s go ahead and add that support now. First, let’s change our handler name: ​

let observer = new IntersectionObserver(replaceImage, options);
Code language: JavaScript (javascript)

​ And now let’s define replaceImage: ​

function upgrade(u) {
	return u.replace('q_30','q_90');
}
​
function replaceImage(entries, observer) {
	console.log('replaceImage');
	entries.forEach(e => {
		if(e.isIntersecting) {
			let newSrc = upgrade(e.target.src);
			console.log('upgraded ',e.target.src);
			e.target.src = newSrc;
			observer.unobserve(e.target);
		}
	});
}
Code language: JavaScript (javascript)

​ Now we make use of a utility function, upgrade, which handles rewriting the Cloudinary URL to change our quality from 30 to 90%. We could load the original of course, but some lowering of quality is probably still preferable here. Also, note that Cloudinary has a JavaScript SDK to generate these URLs for you, but in our case, it was a very simple string replacement. You can play with this version here: ​

See the Pen Cloudinary + Intersection by Raymond Camden (@cfjedimaster) on CodePen.

And as before, if you want to observe the console messages, open it in the live view here. ​

​ As I wrap up, here are a few things to consider. ​

  • The IntersectionObserver is a cool API, but note you can also use lazy loading via a simple HTML attribute. This would be simpler, but the JavaScript solution does give you more fine grained control. All I know is that as a web developer, I love having options!
  • While not particularly important to the point of this article, I’ll note that my images were all sourced from the wonder Placekitten service. I originally used their URLs, but discovered in testing that they will return random images of the same size. I wanted consistency in my results so I downloaded copies.
Back to top

Featured Post