Оптимизация изображений для пользователей с медленным интернетом с помощью Network Information API
Многие из нас привыкли к тому, что быстрый и стабильный интернет это данность в 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;
}
}
}
Выглядеть это будет вот так:
И действительно, во втором случае у нас даже не будет запроса на загрузку изображения, что в какой-то степени облегчит и так тяжелую работу для медленного соединения.
Другие возможности
Я показал всего два примера использования Network Information API для оптимизации изображений, но возможности использования намного больше - можно загружать плейсхолдеры вместо видео, можно вообще не загружать изображения на 2g, можно попробовать не загружать какие-то неосновные шрифты, можно даже убирать или подменять целые компоненты... и так далее.
Но надеюсь, что хотя бы этими примерами вызвал интерес к этому API и обратил внимание на проблемы с плохим интернетом.