Skip to content

Checking Network Strength to Progressively Load Better Images With Cloudinary

In my last article, I discussed how you could use the IntersectionObserver API to progressively load a higher-quality version of an image when it was in the viewport. This was, in my opinion, a great use of web standards to provide a better experience where possible, while still providing a good experience by default. While that’s a basic tenant of progressive enhancement, I think it’s even better when combined with Cloudinary APIs to handle the heavy lifting of creating different quality versions of your media. While researching that article, I came across another option I’d like to explore today: the Network Information API.

The Network Information API provides information about — wait for it — the user’s connection to the internet. As an API, it’s various properties you can check and an event listener that can fire on changes to their connection.

The API is mostly supported in Chromium browsers (Chrome and Edge), but not supported in Safari. If you want to see more about possible support in Safari, you can check out this seven-year-old bug report and cross your fingers. (As an aside, for a while now Safari has basically been the “New IE,” at least in my opinion. I want to give Apple credit, though, as over the last year, they’ve made some great strides in getting Safari updated with more features.) I say “mostly” as, while you can check some things in Chromium browsers, you can’t check everything according to the spec. For our demo today, we’ll be fine, but it’s something to keep in mind. For further details, I’d check the CanIUse page for the API as well as MDN’s compatibility table

With that in mind, let’s look at the properties you can check. All of the below can be found in the navigator.connection object.

  • downlink and downlinkMax. These two properties return the effective and max estimated bandwidth for downloads.
  • effectiveType. Gives a label to the estimated network connection. Returns one of slow-2g, 2g, 3g, or 4g.
  • rtt. Returns an estimate of round-trip time for the connection. This represents the duration, in milliseconds, from when a browser sends a request to a server and gets a response
  • saveData. Returns true if the device making the connection is using a ‘reduced data’ setting.
  • type. Returns a more broad category of connection. For example, ethernet, bluetooth, and more, but this is barely supported so don’t get to excited.

And as mentioned above, you can also listen for a change event to handle a user’s connection switching from one type to another.

While this is probably assumed, be aware that these are all “read-only” properties. You can’t magically upgrade the speed of a user’s connection.

If we assume only Chromium, we can only safely use downlink, effectiveType, and rtt. Here’s an example of what this is returning for me, right now, in an office with Wi-Fi:

{
	"downlink": 10, 
	"effectiveType": "4g",
	"rtt": 100,
	"saveData": false
}
Code language: JSON / JSON with Comments (json)

As I said, I’m currently connected to Wi-Fi, but according to the spec, the highest value possible with effectiveType is “4g”. With that in mind, we can use a check for “4g” as a way of recognizing a user with great connection. Let’s take a look at a demo utilizing this.

For our demo, we’ll begin with a simple example of the Unsplash API. The Unsplash API is a simple way to search their intensive catalog of media and render those results on your site. For this first iteration, our demo will simply provide a form field for searching, a button to perform the search, and then render the results.

To make things a bit easier, and to help keep this demo live within the constraints of Unsplash’s API limits, the form will both default to cats for a search and if that keyword is used, it will store results in your LocalStorage. Obviously, you wouldn’t do this in production and you should feel free to change the search term, but if you do, please consider getting your own developer key.

Finally, I also made use of the excellent Masonry library by David DeSandro.

Alright, let’s first look at the HTML:

<input type="search" id="search" value="cats"> <button id="searchBtn">Search</button>
<div id="results" class="grid"></div>
Code language: HTML, XML (xml)

As you can see, it’s pretty short. But here’s where the magic happens in the JavaScript:

const UNSPLASH_KEY = 'N5Kzqu4gnAFeJGIXYLGxRY5ejv8ryh1PvT32XaPftMM';

/*
In order to keep myself (and others) under Unsplash's limits, I'll cache the result of cats. 
*/
const CACHING = true;

let $search = document.querySelector('#search');
let $results = document.querySelector('#results');
document.querySelector('#searchBtn').addEventListener('click', doSearch, false);

async function doSearch() {
	let term = $search.value.trim();
	if(term === '') return;
	console.log(`search for ${term}`);
	let data;
	// Logic for caching
	if(CACHING && term === 'cats' && localStorage['us_cats']) {
		console.log('Returning cache');
		data = JSON.parse(localStorage['us_cats']);
	} else {
		let res = await fetch(`https://api.unsplash.com/search/photos?page=1&query=${encodeURIComponent(term)}&client_id=${UNSPLASH_KEY}&per_page=30`);
		data = await res.json();
	}
	//console.log(data);
	if(data.total === 0) return;
	let results = '';
	data.results.forEach(d => {
		results += `<div class="grid-item"><img src="${d.urls.small}"></div>`;
	});
	$results.innerHTML = results;

	let msnry = new Masonry($results,{
			itemSelector: '.grid-item',
			columnWidth: '.grid-item'
	});
	imagesLoaded( $results ).on( 'progress', function() {
		msnry.layout();
	});
	
	
	if(CACHING && term === 'cats') {
		console.log('saving cache');
		localStorage['us_cats'] = JSON.stringify(data);
	}

}
Code language: JavaScript (javascript)

From the top, we begin by caching a few DOM selectors and adding an event listener to our button. When a search is performed, we alternate between checking for a cached version (Again, only if you searched for cats, and honestly, why search for anything else?) and hitting the Unsplash API.

Once we have our results, we iterate over them and make use of the small version of each result. Their API returns multiple different sizes but for our purposes, the small URL is fine.

We then render this out and use the Masonry library to make it pretty. Try it yourself:

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

For our Cloudinary version, we’ll make use of two simple transformations. First, we’ll use Fetch to enable Cloudinary to use the remote asset, which will give us access to Cloudinary’s CDN and caching as well.

Next, we’ll use Cloudinary’s quality transformation to return a lower-quality image by default, and a higher-quality one when the Network Information API suggests it’s acceptable.

The changes for this start in our search handler where instead of returning the URL, I wrap it in a call to a new function:

data.results.forEach(d => {
	results += `<div class="grid-item"><img src="${getOptimizedImage(d.urls.small)}"></div>`;
});
Code language: HTML, XML (xml)

If it’s hard to see in there, the new function call is getOptimizedImage, and we pass the URL that Unsplashed returned. Now let’s look at that new function:

function getOptimizedImage(u) {
	if(canDoHighRes()) {
		return `https://res.cloudinary.com/raymondcamden/image/fetch/q_100/${encodeURIComponent(u)}`;	
	} else {
		return `https://res.cloudinary.com/raymondcamden/image/fetch/q_30/${encodeURIComponent(u)}`;	
	}
}
Code language: JavaScript (javascript)

This function makes use of another new function (you’ll see in a second) to determine if a higher resolution image makes sense. If it does, it returns the image at 100% quality, and if not, down to 30%. Now let’s look at the final function:

function canDoHighRes() {
	if(navigator?.connection?.effectiveType === '4g') return true;
	return false;
}
Code language: JavaScript (javascript)

This simply checks effectiveType and if it’s the highest possible value, returns true. You can try out this version below:

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

How does this work when tested? I opened up the demo in Firefox, where the Network Information API’s effectiveType value won’t be available. Even at lower quality, the results are still great.

Firefox’s render

In Firefox’s devtools, it reported 29 images at a total of 243K. Pretty good for so many images.

Running the same test in Chrome shows pretty much the same thing, but much crisper:

Chrome’s result

The screenshot probably doesn’t do it justice, but it does look better. Of course, that’s because the higher-resolution images were loaded. In devtools, the size now is 875kb.

As a reminder, all modern devtools now support throttling in the network panel. In Chrome, I switched to “Slow 3G” for a throttle, ran the search again, and this time the lower-res images were used. (As a developer pro-tip, don’t forget to stop throttling when you’re done.)

So unlike the previous article which made use of the IntersectionObserver API, the Network Information API is much less supported and not quite as useful. Using it to load much higher (or even larger) images could be useful, but just keep in mind the level of support before making use of it in your own projects.

Back to top

Featured Post