Многие из нас привыкли к тому, что быстрый и стабильный интернет это данность в 2023 году, поэтому оптимизацией вебсайтов под этот случай особо не занимаются. Однако все еще остаются сценарии, когда это не так: например, в дороге между населенными пунктами или в некоторых помещениях, которые либо находятся под землей, либо плохо пропускают сигнал по какой-то другой причине.

Для нашего проекта combat-sport.club как раз актуальна ситуация, когда взвешивание спортсменов перед проведением соревнований нередко происходит в каком-нибудь подвальном помещении с плохой связью, и тяжелый SPA с большим количеством медиа может грузиться очень долго. В свою очередь это влияет и на возможность работать с платформой и в целом на удовлетворенность пользователей.

Можно считать это как продолжение серии моих статей про оптимизацию в целом: раз и два.

В этой статье я рассмотрю один из методов оптимизации сайта для пользователей с медленной скоростью интернета - Network Information API. Это API с большим набором различной информации о сети, но пока не с самой лучшей поддержкой среди браузеров. Тем не менее это не повод не использовать его для тех пользователей, чей браузер это поддерживает - а это около 73% глобальных пользователей. Примеры кода будут на Vue.

Компонент для картинок и сервис imagekit

Начну с того, что у нас на проекте есть компонент AppImage, который отвечает за все изображения, а для их хостинга используется сервис imagekit.io. Опуская не имеющие значения детали, в упрощенном виде AppImage выглядит вот так:

<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>

В нем есть функция getImageById, которая обращается к сервису imagekit с id изображения и параметрами w и h для запроса этого изображения в определенных размерах:

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

Помимо этого, у сервиса есть параметр q, который принимает значения от 1 до 100 и отвечает за качество отдаваемой картинки. Например, вот две картинки с q=90 и q=20, вес которых соответственно составляет 31kb и 3kb:

Именно этот параметр я и буду менять в зависимости от скорости интернета у пользователя. Если же вы подобным сервисом не пользуетесь, то хотя все будет сложнее, но все же вы можете отдельно подготовить картинки разного качества и подставлять их.

Используем effectiveType для определения скорости сети

Нам нужно написать сервис, который будет отслеживать информацию о сети и вслед за этим менять качество картинок. Назовем сервис handleNetwork и объявим переменные, которые нам нужны:

import { ref } from 'vue'

export function handleNetwork() {
  const imagesQuality = ref(90)
  const effectiveType = ref(undefined)
}

effectiveType это свойство Network Information API, доступное через navigator.connection.effectiveType. Оно может иметь несколько значений: slow-2g, 2g, 3g, 4g, каждое из которых соответствует определенной скорости интернета, даже если пользователь использует wifi или проводной интернет. Вот таблица с небольшим пояснением:

Именно на эти значения мы и будем ориентироваться, меняя переменную imagesQuality.

Для того чтобы проинициализировать и отслеживать изменения значения effectiveType нам нужно добавить слушатель на событие change для navigator.connection, а также создать функцию updateNetworkState, которая будет обрабатывать обновления.

Важно: обязательно добавляйте проверку на то, что браузер поддерживает это API с помощью простой проверки 'connection' in navigator:

function updateNetworkState() {
  ...
}

if (navigator && 'connection' in navigator) {
    navigator.connection.addEventListener('change', updateNetworkState)
}

updateNetworkState()

Теперь напишем функцию updateNetworkState. В ней тоже добавим проверку на поддержку API и конструкцию switch, которая будет задавать значения для imagesQuality:

function updateNetworkState() {
    if (!navigator || !('connection' in navigator)) return

    effectiveType.value = navigator.connection.effectiveType

    switch (effectiveType.value) {
      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
    }
}

И добавляем return с imagesQuality, чтобы получить доступ к этой переменной в компонентах. В итоге получаем такой код:

import { ref } from 'vue'

export function handleNetwork() {
  const imagesQuality = ref(90)
  const effectiveType = ref(undefined)

  function updateNetworkState() {
    if (!navigator || !('connection' in navigator)) return

    effectiveType.value = navigator.connection.effectiveType

    switch (effectiveType.value) {
      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
    }
  }

  if (navigator && 'connection' in navigator) {
    navigator.connection.addEventListener('change', updateNetworkState)
  }

  updateNetworkState()

  return {
    imagesQuality: imagesQuality.value,
  }
}

Меняем качество изображений

Возвращаемся к компоненту AppImage и импортируем туда наш сервис. Здесь мы сделаем следующее:

1) Передадим переменную imagesQuality как новый параметр функции getImageById

2) Если imagesQuality меньше 20, то оставляем атрибут srcset с 2х изображением пустым, т.к. при медленном интернете нет смысла загружать картинки более высокого разрешения

<script lang="ts" setup>
...
import { handleNetwork } from '@/services/network'

...

const srcAttrs = computed(() => {
  const { imagesQuality } = handleNetwork()

  ...

  return {
    src: getImageById(..., imagesQuality),
    srcset: imagesQuality > 20 ? `${..., imagesQuality)} 2x` : '',
  }
})
</script>

Обновленная функция getImageById с параметром quality:

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}` : ''}`
  }

Готово! Теперь в зависимости от качества сети у нас будут подгружаться картинки разного качества. Убедиться в этом можно переключая эти значения во вкладке Network инструментов разработчика в браузере:

Оптимизируем background-image

Следующая возможность для оптимизации это отключение фоновых изображений, которые мы задаем через CSS, т.к. как правило фоновые изображения используются чисто в декоративных целях, не неся какой-то ценной информации - следовательно при медленном интернете ими можно пожертвовать и не загружать, используя вместо них заливку цветом.

Для этого допишем наш сервис, добавив туда 'флаг' isSlowConnection , который поможет нам определять, когда показывать или не показывать фоновые картинки:

const isSlowConnection = ref(false)

Далее дополним конструкцию switch, чтобы помимо качества картинок там менялось и значений нашей новой переменной:

switch (effectiveType.value) {
      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
        isSlowConnection.value = false
        break
      default:
        imagesQuality.value = 90
        isSlowConnection.value = false
        break
}

А ниже добавим переключение класса (назвать его можно как угодно, но лучше с каким-то особым префиксом, чтобы не столкнуться с конфликтом стилей) на body, с помощью которого мы сможем через CSS селектор определять нужные стили:

document.body.classList.toggle('cs-slow-connection', isSlowConnection.value)

Например, у нас есть фоновое изображение на странице соревнования. Напишем стили, чтобы при наличии класса cs-slow-connection вместо фоного изображения была просто заливка похожим цветом:

.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;
    }
  }
}

Выглядеть это будет вот так:

4g
4g
slow 3g
slow 3g

И действительно, во втором случае у нас даже не будет запроса на загрузку изображения, что в какой-то степени облегчит и так тяжелую работу для медленного соединения.

Другие возможности

Я показал всего два примера использования Network Information API для оптимизации изображений, но возможности использования намного больше - можно загружать плейсхолдеры вместо видео, можно вообще не загружать изображения на 2g, можно попробовать не загружать какие-то неосновные шрифты, можно даже убирать или подменять целые компоненты... и так далее и тому подобное.

Но надеюсь, что хотя бы этими примерами вызвал к этому интерес и обратил внимание на проблемы с плохим интернетом.

Комментарии (6)


  1. olku
    15.07.2023 21:03

    А качество ещё не загруженных картинок меняется при переключении телефона между медленными и быстрыми сетями? Для SPA может быть актуально.


    1. sadiolem Автор
      15.07.2023 21:03
      +2

      да, запросы должны отмениться


  1. Finesse
    15.07.2023 21:03
    +6

    Просто для полноты. Есть ещё один подход: Client Hints. Суть в том, что сначала сервер говорит браузеру Accept-CH: ECT в заголовках ответа или<meta http-equiv="Accept-CH" content="ECT" /> в HTML-коде, а затем браузер добавляет заголовок ECT (effective connection type) во все следующие запросы. Сервер может отдавать ту или иную версию изображения в зависимости от значения заголовка. JavaScript не задействован никак. Если картинки отдаются с другого домена, смотрите сюда.

    Поддержка браузерами такая же как у navigator.connection.


  1. AndrewShmig
    15.07.2023 21:03

    Спасибо за статью!


  1. AMDmi3
    15.07.2023 21:03

    Сомнительная фича, как минимум требующая намного больше логики для правильной реализации. Например, что если изображение с высоким качеством уже закэшировано, зачем качать заново низкокачественное.


    1. sadiolem Автор
      15.07.2023 21:03

      Я полагаю, что тогда можно добавить такую проверку; остальное остается актуальным