Adaptive image loading based on network speed

In this article, we'll explore a concept based on Network Information API, Service Workers and Cloudinary to enable web applications to serve lower quality images (if necessary) in order to speed up the perceptual load time of the web app.

The web has seen incredible growth over the past decade - an increase that enabled remarkable technological advancement. There are developer tools and frameworks out there that can do magical things and unfortunately, therein lies the problem. With this explosion of new tools as well as myriad device types, we are faced with another problem - performance.

In the era of JavaScript frameworks, there is an ever increasing number of resources that a website requires. When one stops to think about it, these days we need multiple JavaScript files, CSS files and other resources, as well as media assets such as images and videos.

Creating a website that performs well is a constant challenge as there are many factors to take into consideration such as the connection speed of the user and the capabilities of the device used to access the site.

There are a lot of great resources out there that describe how performance can affect conversion, how it can deter users from staying on the site.

Please note that the Network Information API is in a draft proposal state at the time of writing this article.

Get the code

Please visit this GitHub repository to get the source code discussed in this article along with setup instructions.

Serving images

Asset management is always an important factor in web applications, primarily because recent years have seen an increase in visually appealing websites. More images are added to websites, but this comes with an undesired effect - often leading to slower load times.

There are web applications that could lose profit if images do not load fast enough - think about e-commerce websites. The profitability of such sites depends on garnering a reasonable conversion rate. A few seconds in delay when loading product information could cause a potential buyer to leave the site prematurely.

Luckily, Cloudinary offers two simple yet powerful techniques for optimizing image performance effortlessly. These techniques are called q_auto and f_auto.

Automatic Quality and Format

Adding the q_auto flag to any image URL served via Cloudinary will automatically adjust the image and serve a compressed version of the image without losing visual quality. This is a great start to optimising a web application.

Different browsers support different image formats - image formats that have different ways of compressing images. For example, Chrome uses WebP while IE/Edge uses JPEG-XR. Using the f_auto flag, Cloudinary can deliver the best format based on the browser.

Please read this great article that discusses automatic image optimisation using Cloudinary as well as explaining adaptive browser format delivery.

Network Information API

This Web API is an experimental one with limited support from browsers, but it is undoubtedly an exciting proposal. The API essentially provides information about the connection via an interface called NetworkInformation.

The interface itself gives information about the type of connection (e.g. cellular or wifi) as well as the speed of the connection (e.g. 4g or 3g).

We can get started with the API right away: here's sample code we can place in between <script> tags and capture information about the network itself.

Please use Google Chrome (version 63 or above) for executing this code as it has the most support for the Network Information API.

if (navigator && navigator.connection) {
  console.log(navigator.connection);
}

The above code returns a data format similar to the following:

NetworkInformation {
    downlink: 3.75,
    effectiveType: "4g",
    onchange: null,
    rtt: 150,
    saveData: false
}

The effectiveType property tells us the type of connection a particular user has to access the web application.

Serving images via Service Workers

This article is not going to explain Service Workers in detail. To follow this article we only need to know that Service Workers allow us to interrupt the request/response cycle, i.e. they act as a proxy between the client request requests and the response. (With a potential to modify the requests/responses.)

Please take a look at this free handbook on PWAs - including a discussion on Service Workers to learn more.

Serve the right image using Service Worker

As mentioned earlier, Service Workers allow us to interrupt the request/response cycle and because of this, we can easily modify the response to be a lower quality image based on the network connection. This gives us immensely powerful image delivery performance opportunities. Let's take a look at how this would be implemented.

First, let's create a sample application that will display a single image:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title></title>
  <meta name="description" content="">
  <meta name="author" content="Tamas Piros">
</head>

<body>
<img src="https://res.cloudinary.com/tamas-demo/image/upload/pwa/hungarywp.jpg">
</body>
</html>

Loading this image takes a minimal amount of time as the size of the image is 107 KB.

Note that when serving images, Cloudinary also uses a worldwide CDN network to serve the image from a location closest to our physical location. Therefore, on subsequent loads of the same image, we get a much faster response because the image gets cached at the CDN layer.

Even though the image in question is relatively lightweight, we can still improve on the performance. Remember when we said that we could provide the best format to browsers? Let's change our <img> element's source so that it points to a URL where selecting the right format for our browser is done automatically by adding the f_auto flag:

  <img src="https://res.cloudinary.com/tamas-demo/image/upload/f_auto/pwa/hungarywp.jpg">

Note that we are specifying a jpg image in the application but Cloudinary delivers a webp format because that's the best format for Chrome. (This is the result of using the f_auto flag.)

We now get a much faster load since the image size is smaller - it's only ~60 KB. We have reduced the size of the image by nearly 50% without losing visual quality.

Now even though we have achieved a faster load, we still need to do some work since on a slow 3G connection we are still required to load this image, and that would take more time because of the size of the image.

Let's add a Service Worker to our application:

// app.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log(`Service Worker registered! Scope: ${registration.scope}`);
      })
      .catch(err => {
        console.log(`Service Worker registration failed: ${err}`);
      });
  });
}
// sw.js
self.addEventListener('fetch', event => {
    event.respondWith(
      fetch(event.request.url, { headers: event.request.headers })
    );
  }
);

The first code block is responsible for registering the Service Worker while the second one is the actual Service Worker code. For the time being it's kept to be very simple for demonstration purposes - it listens to fetch events, intercepts them and responds to them (without modifying anything).

The Service Worker will return the image after the initial load as we can see this indiciated in the screenshot above.

Let's now extend our Service Worker and modify the quality of the images using the q_auto setting via Cloudinary:

// sw.js
self.addEventListener('fetch', event => {
  if (/\.jpg$|.png$|.gif$|.webp$/.test(event.request.url)) {
    const connection = navigator.connection.effectiveType;
    let imageQuality;
    const format = 'f_auto';
    switch (connection) {
      case '4g':
        imageQuality = 'q_auto:good';
        break;
      case '3g': 
        imageQuality = 'q_auto:eco';
        break;
      case'2g':
      case 'slow-2g':
        imageQuality = 'q_auto:low';
        break;
      default:
        'q_auto:best';
        break;
    }

    const imageURLParts = event.request.url.split('/');
    imageURLParts.splice(imageURLParts.length - 2, 0, `${imageQuality},${format}`);
    const finalImageURL = new URL(imageURLParts.join('/'));
    event.respondWith(
      fetch(finalImageURL.href, { headers: event.request.headers })
    );
  }
});

Note that the imageURLParts.length - 2 call is relevant to this code as it matches the Cloudinary folder path. You may need to update this to suite your Cloudinary folder name.

In the code above, we are checking whether the requested resource is an image and if it is, we check for the network speed as well. If the network speed is 3G then we reduce the quality of the image by applying q_auto:eco, if it's 2G or slower we reduce the quality by applying q_auto:low otherwise we leave the q_auto:best flag.

q_auto:best - Uses a less aggressive algorithm. Generates bigger files with potentially better visual quality.
q_auto:good - Ensures a relatively small file size with good visual quality.
q_auto:eco - Uses a more aggressive algorithm, which results in smaller files of slightly lower visual quality.
q_auto:low - Uses the most aggressive algorithm, which results in the smallest files of low visual quality.

Last but not least we extend our fetch() handler as well so that the newly created URL is requested from the server.

As an extra we also add the f_auto flag automatically so we can remove that part of the URL in the HTML file:

<img src="https://res.cloudinary.com/tamas-demo/image/upload/pwa/hungarywp.jpg">

If we now load the site using a slow 3G connection we'll see that the file size of the image is only 38.3 KB - that's more than 60% lighter than what we had seen earlier. If we try to request the file via WiFi (or a fast 4G connection), we get a better quality image, again around 60 KB in size.

Conclusion

In this article, we saw how we could enable adaptive image size (based on quality and format automation) in tandem with the Network Information API and Service Workers. We have great hope for Network Information API to become available in all browsers soon so we can leverage it to create sites that perform faster and greatly enhance the user experience especially on slow connections.