DEV Community

Cover image for Optimizing Images for Users on Slow Connections with the Network Information API
Ilia
Ilia

Posted on

Optimizing Images for Users on Slow Connections with the Network Information API

Most of us are used to treating fast, stable internet as a given. But there are still scenarios where it isn't: on the road between towns or inside buildings that block the signal for one reason or another.

In this article I'll look at one optimization technique that uses the Network Information API. It's an API with a large set of information about the connection, though browser support isn't great yet. That's not a reason to skip it for users whose browsers do support it — roughly 76% of users globally. The code examples are in Vue.

The image component and imagekit

I'll start with some background. Our project has an AppImage component responsible for all images, and we host them with imagekit.io. Leaving out the details that don't matter here, a simplified AppImage looks like this:

<script lang="ts" setup>
import { computed } from 'vue'
import { useMedia } from '@/composables/media.ts'

interface IProps {
  imgId?: string
  src?: string
  height: number | string
  width: number | string
  ...
}

const props = withDefaults(defineProps<IProps>(), {
  imgId: '',
  src: '',
  ...
})

const { getImageById } = useMedia()

const srcAttrs = computed(() => {
  if (props.src) {
    return {
      src: props.src,
    }
  }

  return {
    src: getImageById(props.imgId, +props.width, +props.height, props.type),
    srcset: `${getImageById(props.imgId, +props.width * 2, +props.height * 2, props.type)} 2x`,
  }
})
</script>

<template>
  <img
    v-bind="srcAttrs"
    :height="height"
    :width="width"
  >
</template>
Enter fullscreen mode Exit fullscreen mode

It contains a getImageById function that hits imagekit with the image id and the w and h parameters to request that image at particular dimensions:

const getImageById = (
    id: string,
    width: number,
    height: number,
    ...
  ): string => {
    ...
    return `${BASE_URL}/${id}${width ? `?tr=w-${width}` : ''}${width && height ? `,h-${height}` : ''}`
  }
Enter fullscreen mode Exit fullscreen mode

The service also has a q parameter, which takes values from 1 to 100 and controls the quality of the returned image. Here, for example, are two images at q=90 and q=20, weighing 31 KB and 3 KB respectively:

That's the parameter I'll be changing based on the user's connection speed. If you don't use a service like this, it'll be more work, but you can still prepare images at different quality levels separately and swap them in.

Using effectiveType to detect connection speed

We need a service that tracks connection information and changes image quality in response. Let's call it handleNetwork and declare the variables we need:

import { ref } from 'vue'

const imagesQuality = ref(90)
const isSlowConnection = ref(false) // we will use it later

export function handleNetwork() {
  return { imagesQuality }
}
Enter fullscreen mode Exit fullscreen mode

The default value for imagesQuality is set to maximum quality. We'll only degrade on an explicit signal.

The Network Information API gives us the effectiveType property, available via navigator.connection.effectiveType. It can take several values — slow-2g, 2g, 3g, 4g — each corresponding to a particular connection speed, even when the user is on wifi or a wired connection. Here's a table with a short explanation:

These are the values we'll key off when changing imagesQuality.

To initialize and track changes to effectiveType, we need to add a listener for the change event on navigator.connection, plus an updateNetworkState function to handle the updates.

Important: always check that the browser supports the API, with a simple 'connection' in navigator check:

function updateNetworkState() {
  ...
}

if ('connection' in navigator) {
  updateNetworkState()
  navigator.connection.addEventListener('change', updateNetworkState)
}
Enter fullscreen mode Exit fullscreen mode

Now let's write updateNetworkState. It's just the switch that sets imagesQuality — the support check is already done above, at the only place this function is called from:

function updateNetworkState() {
    switch (navigator.connection.effectiveType) {
      case 'slow-2g':
      case '2g':
        imagesQuality.value = 1
        break
      case '3g':
        imagesQuality.value = 20
        break
      case '4g':
        imagesQuality.value = 90
        break
      default:
        imagesQuality.value = 90
        break
    }
}
Enter fullscreen mode Exit fullscreen mode

Changing image quality

Back to AppImage: we import our service and do two things.

1) Pass the quality variable as a new parameter to getImageById

2) If imagesQuality is below 20, drop the srcset attribute holding the 2x image — on a slow connection there's no point downloading higher-resolution images

<script lang="ts" setup>
...
const { imagesQuality } = handleNetwork()
const quality = imagesQuality.value
...

const srcAttrs = computed(() => {
  if (props.src) {
    return { src: props.src }
  }

  ...

  return {
    src: getImageById(..., quality),
    srcset: quality > 20 ? `${getImageById(..., quality)} 2x` : undefined,
  }
})
</script>
Enter fullscreen mode Exit fullscreen mode

One detail matters here: handleNetwork is called at setup level rather than inside the computed, and the value is read out of imagesQuality once.

If you call the service inside the computed, or put imagesQuality.value in there, then when the connection degrades, src recalculates for every image already on screen. A different q means a different URL: the browser fires a batch of fresh requests over the connection that just got worse, to replace images that are already downloaded and already visible. You end up with an optimization that makes things worse at exactly the moment the connection slows down.

The updated getImageById with a quality parameter:

const getImageById = (
    id: string,
    width: number,
    height: number,
    quality: number,
    ...
  ): string => {
    ...
    return `${BASE_URL}/${id}${width ? `?tr=q-${quality},w-${width}` : ''}${width && height ? `,h-${height}` : ''}`
  }
Enter fullscreen mode Exit fullscreen mode

Done. You can check it in the Network tab of your dev tools: switch the profile to 3G and then, say, go to the next page of pagination — the new images come down at reduced quality, while the ones already rendered stay put and aren't requested again.

Optimizing background-image

The next opportunity is turning off background images set through CSS. Background images are usually purely decorative and carry no valuable information, so on a slow connection they can be sacrificed and skipped in favor of a solid color.

For that we'll use the isSlowConnection flag we declared at the very beginning. We extend the switch so it changes this variable alongside image quality:

switch (navigator.connection.effectiveType) {
  case 'slow-2g':
  case '2g':
    imagesQuality.value = 1
    isSlowConnection.value = true
    break
  case '3g':
    imagesQuality.value = 20
    isSlowConnection.value = true
    break
  case '4g':
    imagesQuality.value = 90
    break
  default:
    imagesQuality.value = 90
    isSlowConnection.value = false
}
Enter fullscreen mode Exit fullscreen mode

And below that we add a class toggle on body — you can name the class whatever you like, but it's better to use a distinctive prefix to avoid style conflicts — which lets us target the right styles with a CSS selector:

function updateNetworkState() {
  switch (navigator.connection.effectiveType) {
    case 'slow-2g':
    case '2g':
      imagesQuality.value = 1
      isSlowConnection.value = true
      break
    case '3g':
      imagesQuality.value = 20
      isSlowConnection.value = true
      break
    case '4g':
      imagesQuality.value = 90
      break
    default:
      imagesQuality.value = 90
      isSlowConnection.value = false
  }

  document.body.classList.toggle('cs-slow-connection', isSlowConnection.value)
}
Enter fullscreen mode Exit fullscreen mode

For instance, we have a background image on the competition page. Let's write styles so that when the cs-slow-connection class is present, the background image is replaced with a solid fill in a similar color:

.event-header {
  &::before {
    background-image: url("/img/event-default-img.webp");
    background-position: center;
    background-size: cover;
  }
}

body.cs-slow-connection {
  .event-header {
    &::before {
      background-color: #e4e4e4;
      background-image: none;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It looks like this:


4g


slow 3g

In the second case there's no image request at all, which takes at least some of the load off an already struggling connection.

Other possibilities

I've shown just two examples of using the Network Information API to optimize images, but the possibilities go much further. You can load placeholders instead of video, skip images entirely on 2g, try not to load non-essential fonts, even swap out or drop whole components — and so on.

But I hope these examples at least sparked some interest in this API, and drew attention to the problems that come with a bad connection.

Top comments (0)