Допустим, у вас есть классная страница и вы добавляете фоновое изображение:
.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)
Spaceoddity
00.00.0000 00:00+3Опять набор вредных советов на эту тему... Да сколько можно?
Во-первых, это семантически разные сущности. background-image может быть и у элемента img. А ещё <img> - по умолчанию строчный элемент, незнание этого факта приводит к разного рода "сюрпризам".
Во-вторых, применимость того или иного метода зависит от вводных - есть "финты", которые можно сделать только через background (не только background-repeat - есть и несколько фоновых изображений, и background-clip и т.п.). А есть и проблемы - "ой, а почему у меня img раздвигает flexbox?". Здесь куча неочевидных моментов, которые, разумеется, не описываются фразой "всегда используйте <img>".
В-третьих, почему все так настойчиво советуют повсеместно использовать loading="lazy"? Допустим, у меня невысокая скорость интернета, я прокручиваю веб-страницу, срабатывает Intersection Observer API и... изображение только начинает загружаться. Т.е. на якобы загруженной странице мне предлагают любоваться неспешной подгрузкой изображений. Очень "отзывчивое" решение...
Vassam
00.00.0000 00:00Ну для этого есть фоллбэк на Base64 инлайн хтмл копию изображения, сжатую в десятки раз и подставляемую через стиль с background-image, который отобразит очень мутную и размытую браузером картинку, и оная будет заменена на актуальное изображение после подгрузки. А по поводу lazy-загрузок, имхо, предъявлять надо браузерам, пусть они сами разбираются, когда подгружать. К примеру, ближнее к вьюпорту изображение предзагружать сразу после загрузки страницы, дальнее - стартовать только если пользователь начал скроллить.
Spaceoddity
00.00.0000 00:00+2Что-то вы совсем не о том...
и оная будет заменена на актуальное изображение после подгрузки.
Вот спасибо - теперь вместо пустого пространства буду любоваться на мыльную картинку.
А может стоит просто заранее подгрузить необходимые ресурсы, а не городить велосипед с base64?
А по поводу lazy-загрузок, имхо, предъявлять надо браузерам
Замечательный совет! Хорошо, "предъявили браузерам", дальше что? Проблему нашу это как-то решает?
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 , в ближайшее время допишу документацию.
NicolaiCherezov
00.00.0000 00:00+5Простое правило(за редким исключением):
-bg для декора,
-img для контента
johnfound
00.00.0000 00:00+2Ну, мне совершенно не нравится ленивая загрузка. Небольшие изображения и так грузятся быстро. А большие изображения нужно ждать долго, когда прокрутил страницу и хочешь читать. То есть, ленивая загрузка рушит UX.
ilvetrov
00.00.0000 00:00У фона также можно использовать srcset, sizes и ленивую загрузку — сделал для этого NPM пакет под React. И синтаксис как у тега img:
<LazyBackground src="..." srcSet="..." sizes="..."> content </LazyBackground>
Оно ещё должно асинхронно декодировать изображение. Вообще полный фарш.
Под капотом работает через загрузку виртуального изображения через
new Image()
. Потом вставляется на страницу в backgroundImage, подтягивая изображение из кэша браузера. Поэтому есть один минус — при разработке с отключённом кэшем фон загружается дважды.Надеюсь, в тему статьи! Если есть вопросы, буду рад на них ответить в Telegram или в GitHub issues.
namikiri
Вообще, background вместо img используют всякие мерзонькие сайты, пытающиеся воспрепятствовать сохранению картинок, чаще всего таким грешат соцсети. Неприятненько, неприятненько. Ещё и пару-тройку слоёв сверху накидывают, чтоб уж наверняка, и там на click ставят preventDefault().
NickyX3
До появления object-fit etc background-image и его размеры и позиция были единственным вариантом сделать cover/contain.