Допустим, у вас есть классная страница и вы добавляете фоновое изображение:

.hero {
  /* ???? */
  background-image: url('/image.png');
}

С точки зрения производительности страницы — это не лучший вариант. И на то есть несколько причин. 

Почему использовать background-image в CSS — не лучшая идея

При всём разнообразии размеров экрана и разрешений, не бывает так, чтобы у каждого из посетителей сайта загружалось одно и то же изображение, только если вы не используете SVG. 

Можно, конечно, использовать медиазапросы — вручную указать диапазон размеров экранов и изображений:

/* ???? */
.hero { background-image: url('/image.png'); }
@media only screen and (min-width: 768px) {
  .hero { background-image: url('/image-768.png'); }
}
@media only screen and (min-width: 1268px) {
  .hero { background-image: url('/image-1268.png'); }
}

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

Также есть полезная функция image-set. Она позволяет указывать размеры изображения для разных разрешений:

/* ???? */
.hero {
  background-image: image-set(url("/image-1x.png") 1x, url("/image-2x.png") 2x);
}

Да, функция даёт некоторые преимущества, но, вообще-то, нужно учитывать одновременно размер экрана и его разрешение.

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

Этот подход также упускает важные нюансы: ленивую загрузку (Lazy Loading), текущую поддержку браузерами форматов нового поколения, подсказки приоритета (Priority Hints), асинхронное декодирование и многое другое.

А ещё у нас остаётся актуальной проблема с цепочками запросов.

С тегом изображения ссылка на src находится прямо в HTML. В этом случае браузер получит исходный HTML-код, поищет картинки и начнёт загружать изображения с высоким приоритетом.

При загрузке изображений в CSS и использовании внешних таблиц стилей (link rel=”stylesheet” вместо встроенных стилей) браузер должен просканировать HTML, получить CSS, определить условие, что background-image применяется к элементу. Только после этого он сможет загрузить картинку. Получается долго.

Процесс можно ускорить благодаря встраиванию CSS, предварительной загрузке изображений и предварительного подключения к доменам. А можно использовать ряд преимуществ тега img в HTML, о которых пойдёт речь ниже.

Исключение из правил

Во всех правилах есть исключения.

Чтобы замостить фон очень маленьким изображением, лучше воспользоваться background-repeat. Тег img для размножения картинки не подходит.

Для любого другого изображения больше 50px не рекомендуется задавать размер в CSS — во всех этих случаях лучше использовать тег img

Преимущества тега img

Нативная ленивая загрузка изображений. Атрибут loading=lazy, добавленный к элементу img, откладывает загрузку элементов до тех пор, пока они не попадут в область просмотра.

<!-- ???? -->
<img 
  loading="lazy"
  ... 
>

Ленивая загрузка даёт высокую производительность, полностью реализована браузерами, не требует JS и поддерживается всеми современными браузерами.

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

P. S. loading=lazy также работает с элементами iframe.  

Оптимальный размер для всех размеров экрана и разрешений. Атрибут srcset подставляет наиболее подходящее изображение в зависимости от размеров экрана и разрешения. Он работает гораздо круче, чем image-set в CSS, потому что позволяет использовать дескриптор ширины w

<img 
  srcset="
    /image.png?width=100 100w,
    /image.png?width=200 200w,
    /image.png?width=400 400w,
    /image.png?width=800 800w
  "
  ...
>

Srcset учитывает не только размер, но и разрешение. Например, если сейчас  изображение отображается шириной 200px на устройстве с плотностью пикселей 2х, то с указанным атрибутом srcset браузер загрузит изображение 400w с шириной 400px, потому что именно оно идеально отобразится на дисплее с плотностью 2x. То же изображение на плотности 1x будет отображаться с разрешением 200w.

Поддержка современных форматов. Обернув тег img в picture, можно указать современные и более оптимальные форматы, такие, как webp. Поддерживающие эти форматы браузеры предпочтут их, прочитав условие в теге source:

<picture>
  <source 
    type="image/webp"
    srcset="
      /image.webp?width=100 100w,
      /image.webp?width=200 200w,
      /image.webp?width=400 400w,
      /image.webp?width=800 800w
    " />
  <img ... />
</picture>

При желании можно задать поддержку дополнительных форматов, например, AVIF:

<picture>
  <source 
    type="image/avif"
    srcset="/image.avif?width=100 100w, /image.avif?width=200 200w, /image.avif?width=400 400w, /image.avif?width=800 800w, ...">
  <source 
    type="image/webp"
    srcset="/image.webp?width=100 100w, /image.webp?width=200 200w, /image.webp?width=400 400w, /image.webp?width=800 800w, ...">
  <img ...>
</picture>

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

Первый — указать атрибуты ширины width и высоты height для вашего изображения. Необязательно, но можно установить для height значение auto в CSS, чтобы картинка правильно реагировала на изменения размеров экрана:

<img 
  width="500" 
  height="300" 
  style="height: auto" 
  ...
>

Второй способ — использовать в CSS свойство aspect-ratio, чтобы автоматически задать соотношение сторон. С этой опцией вам не нужно знать точную ширину и высоту вашего изображения:

<img style="aspect-ratio: 5 / 3; width: 100%" ...>

aspect-ratio отлично сочетается с object-fit и object-position, которые очень похожи на background-size и background-position для фоновых изображений.

.my-image {
  aspect-ratio: 5 / 3;
  width: 100%;
  /* Fill the available space, even if the 
     image has a different intrinsic aspect ratio */
  object-fit: cover; 
}

Асинхронное декодирование изображений. Дополнительно можно указать свойство decoding="async" для изображений, что позволит браузеру переместить декодирование изображения из основного потока. Подойдёт для изображений за пределами загружаемого экрана.

<img decoding="async" ... >

Ресурсные подсказки и директивы. Один из последних и продвинутых вариантов — атрибут fetchpriority. Он подсказывает браузеру, какие изображения «важны» для взаимодействия с пользователем в начале процесса загрузки:

<img fetchpriority="high" ...>

Также свойство fetchpriority может понизить приоритет загрузки второстепенных изображений, находящихся на последующих экранах или других страницах карусели:

<div class="carousel">
  <img class="slide-1" fetchpriority="high">
  <img class="slide-2" fetchpriority="low">
  <img class="slide-3" fetchpriority="low">
</div>

Добавление alt-текста. Атрибут alt повышает SEO-оптимизацию и доступность контента, поэтому не стоит им пренебрегать:

<img
  alt="Builder.io drag and drop interface"
  ...
>

Изображения, которые добавлены чисто для красоты: абстрактные формы, цвета, градиенты, — можно пометить атрибутом role:

<img role="presentation" ... >

Атрибут sizes. После рендеринга изображения браузер узнаёт его фактический размер, умножает на плотность пикселей и подбирает максимально близкое по размеру изображение в srcset. Но браузеры, подобные Chrome, используют сканер предзагрузки для первоначальной загрузки страницы. Сканер ищет теги img в HTML и начинает загрузку с них.

Всё это происходит ДО того, как страница отобразилась. CSS ещё не получен, и нет указаний, как и какого размера должно отображаться изображение. 

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

Здесь-то нам и пригодится атрибут sizes:

<img 
  srcset="..."
  sizes="(max-width: 400px) 200px, (max-width: 800px) 100vw, 50vw"
  ...
>

Он сообщает браузеру, насколько большим должно быть наше изображение при разных размерах экрана. Это может быть точное значение в пикселях либо в зависимости от окна: например, 500px или 50vw (изображение занимает примерно 50% ширины экрана).

В примере выше экран шириной 900px будет выполнять только последнее условие, так как впереди стоящие условия предназначены для экранов меньше 800px. Для экрана шириной 900px будет отображаться изображение на 50vw (оно будет заполнять только половину экрана).

Поскольку 50vw * 900px = 450px, браузер будет стремиться к изображению шириной 450px для дисплея с плотностью пикселей 1x, к изображению шириной 900px для дисплея с плотностью пикселей 2x. Затем он будет искать наиболее близкое совпадение в srcset и использовать его как изображение для предварительной загрузки.

Примеры оптимизированных для загрузки изображений

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

<picture>
  <source 
    type="image/avif"
    srcset="/image.avif?width=100 100w, /image.avif?width=200 200w, /image.avif?width=400 400w, /image.avif?width=800 800w" />
  <source 
    type="image/webp"
    srcset="/image.webp?width=100 100w, /image.webp?width=200 200w, /image.webp?width=400 400w, /image.webp?width=800 800w" />
  <img 
    src="/image.png"
    srcset="/image.png?width=100 100w, /image.png?width=200 200w, /image.png?width=400 400w, /image.png?width=800 800w"
    sizes="(max-width: 800px) 100vw, 50vw"
    style="width: 100%; aspect-ratio: 16/9"
    loading="lazy"
    decoding="async"
    alt="Builder.io drag and drop interface"
  />
</picture>

Для загрузки изображений с наивысшим приоритетом, например, основного содержимого страницы, из приведённого кода удаляем loading="lazy" и decoding="async" и добавляем fetchpriority="high":

      style="width: 100%; aspect-ratio: 16/9"
-     loading="lazy"
-     decoding="async"
+     fetchpriority="high"
      alt="Builder.io drag and drop interface"

Для векторных форматов, например, SVG, не нужно указывать несколько размеров и форматов. Полностью удаляем теги <picture> и <source>, а также атрибуты srcset и sizes

<!-- for SVG -->
<img 
  src="/image.svg"
  style="width: 100%; aspect-ratio: 16/9"
  loading="lazy"
  decoding="async"
  alt="Builder.io drag and drop interface"
/>

Для высокоприоритетных SVG применяются те же правила: удаляем loading и decoding, по желанию добавляем fetchpriority="high" для основного контента.

И, наконец, мы добрались до фонового изображения. Описанные в этой статье способы оптимизации изображений можно применить к любому типу изображений: фон, передний план и т. д. Но чтобы заставить img вести себя как background-image, нужно добавить немного CSS — абсолютное позиционирование и свойство object-fit:

<div class="container">
  <picture class="bg-image">
    <source type="image/webp" ...>
    <img ...>
  </picture>
  <h1>I am on top of the image</h1>
</div>
<style>
  .container { position: relative; }
  h1 { position: relative; }
  .bg-image { position: absolute; inset: 0; }
  .bg-image img { width: 100%; height: 100%; object-fit: cover; }
</style>
Такое большое количество дополнительного HTML плохо сказывается на производительности?

Скорее нет, чем да.

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

Во-вторых — помним про gzip. Дополнительная разметка, которую вы добавите для каждого изображения, быстро становится избыточной, а gzip с этим отлично справляется.

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

Вариант полегче

В наши дни редко требуется писать весь этот код вручную. Фреймворки NextJS и Qwik, платформы Cloudinary и Builder.io упрощают задачу и предоставляют готовые компоненты изображений.

<!-- ???? -->
<Image 
  src="/image.png" 
  alt="Builder.io drag and drop interface" />

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

Обратите внимание, что в большинстве случаев вам всё равно нужно указывать высокий приоритет изображения:

<!-- High priority image -->
<Image 
  priority
  src="/image.png" 
  alt="Builder.io drag and drop interface" />

Если используете атрибут sizes, придётся также прописать его вручную:

<!-- Manually speify sizes -->
<Image 
  sizes="(max-width: 500px) 200px, 50vw"
  src="/image.png" 
  alt="Builder.io drag and drop interface" />

Подытожим:

  • По возможности используйте img в HTML вместо background-image в CSS. 

  • Применяйте ленивую загрузку, srcset, теги picture и другие рекомендации из статьи, чтобы максимально оптимизировать загрузку изображений. 

  • Используйте проверенные фреймворки (NextJS или Qwik) и платформы (Cloudinary или Builder.io) для упрощения и ускорения своей работы.

  • Помните об атрибутах высокого и низкого приоритета загрузки изображений, настраивайте их соответствующим образом.

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


  1. namikiri
    00.00.0000 00:00

    Вообще, background вместо img используют всякие мерзонькие сайты, пытающиеся воспрепятствовать сохранению картинок, чаще всего таким грешат соцсети. Неприятненько, неприятненько. Ещё и пару-тройку слоёв сверху накидывают, чтоб уж наверняка, и там на click ставят preventDefault().


    1. NickyX3
      00.00.0000 00:00

      До появления object-fit etc background-image и его размеры и позиция были единственным вариантом сделать cover/contain.


  1. Spaceoddity
    00.00.0000 00:00
    +3

    Опять набор вредных советов на эту тему... Да сколько можно?

    Во-первых, это семантически разные сущности. background-image может быть и у элемента img. А ещё <img> - по умолчанию строчный элемент, незнание этого факта приводит к разного рода "сюрпризам".

    Во-вторых, применимость того или иного метода зависит от вводных - есть "финты", которые можно сделать только через background (не только background-repeat - есть и несколько фоновых изображений, и background-clip и т.п.). А есть и проблемы - "ой, а почему у меня img раздвигает flexbox?". Здесь куча неочевидных моментов, которые, разумеется, не описываются фразой "всегда используйте <img>".

    В-третьих, почему все так настойчиво советуют повсеместно использовать loading="lazy"? Допустим, у меня невысокая скорость интернета, я прокручиваю веб-страницу, срабатывает Intersection Observer API и... изображение только начинает загружаться. Т.е. на якобы загруженной странице мне предлагают любоваться неспешной подгрузкой изображений. Очень "отзывчивое" решение...


    1. Vassam
      00.00.0000 00:00

      Ну для этого есть фоллбэк на Base64 инлайн хтмл копию изображения, сжатую в десятки раз и подставляемую через стиль с background-image, который отобразит очень мутную и размытую браузером картинку, и оная будет заменена на актуальное изображение после подгрузки. А по поводу lazy-загрузок, имхо, предъявлять надо браузерам, пусть они сами разбираются, когда подгружать. К примеру, ближнее к вьюпорту изображение предзагружать сразу после загрузки страницы, дальнее - стартовать только если пользователь начал скроллить.


      1. Spaceoddity
        00.00.0000 00:00
        +2

        Что-то вы совсем не о том...

        и оная будет заменена на актуальное изображение после подгрузки.

        Вот спасибо - теперь вместо пустого пространства буду любоваться на мыльную картинку.

        А может стоит просто заранее подгрузить необходимые ресурсы, а не городить велосипед с base64?

        А по поводу lazy-загрузок, имхо, предъявлять надо браузерам

        Замечательный совет! Хорошо, "предъявили браузерам", дальше что? Проблему нашу это как-то решает?


  1. fr0st1kk
    00.00.0000 00:00
    +2

    Интересная статья.

    К сожалению, на сегодняшний день, атрибут loading="lazy" плохо поддерживается браузерами http://joxi.ru/1A56nMBiwZLMar , в этой связи, приходится использовать дополнительные библиотеки для lazyload.
    Обращаю внимание на то, что вышеуказанный атрибут может использоватся как для тега img, так и для iframe. Для отложенной загрузки iframe, на сегодняшний день, считаю целесообразным использовать заглушку. Вот решение, которое я использую для iframe с ютуба https://github.com/fr0st1kk/add-video-youtube , в ближайшее время допишу документацию.


  1. NicolaiCherezov
    00.00.0000 00:00
    +5

    Простое правило(за редким исключением):
    -bg для декора,
    -img для контента


  1. johnfound
    00.00.0000 00:00
    +2

    Ну, мне совершенно не нравится ленивая загрузка. Небольшие изображения и так грузятся быстро. А большие изображения нужно ждать долго, когда прокрутил страницу и хочешь читать. То есть, ленивая загрузка рушит UX.


  1. ilvetrov
    00.00.0000 00:00

    У фона также можно использовать srcset, sizes и ленивую загрузку — сделал для этого NPM пакет под React. И синтаксис как у тега img:

    <LazyBackground src="..." srcSet="..." sizes="...">
      content
    </LazyBackground>

    Оно ещё должно асинхронно декодировать изображение. Вообще полный фарш.

    Под капотом работает через загрузку виртуального изображения через new Image(). Потом вставляется на страницу в backgroundImage, подтягивая изображение из кэша браузера. Поэтому есть один минус — при разработке с отключённом кэшем фон загружается дважды.

    Надеюсь, в тему статьи! Если есть вопросы, буду рад на них ответить в Telegram или в GitHub issues.