Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В этой статье я расскажу про 8 методик оптимизации загрузки изображений, которые уменьшают необходимую пропускную способность сети и нагрузку на процессор при выводе на экран. Приведу примеры аннотированного HTML, чтобы вам было легче воспроизвести. Какие-то методики уже давно известны, а какие-то появились относительно недавно. В идеале, ваш любимый механизм публикации веб-документов (например, CMS, генератор статичных сайтов или фреймворк для веб-приложений) должен всё это реализовывать из коробки.
В совокупности методики оптимизируют все элементы Google Core Web Vitals с помощью:
- минимизации основных проблем содержимого (Largest Contentful Paint (LCP)) за счёт уменьшения размеров, кеширования и ленивой загрузки;
- сохранения нулевого накопительного сдвига макета (Cumulative Layout Shift (CLS));
- уменьшения задержки первого ввода (First Input Delay (FID)) за счёт снижения потребления процессора (для основного потока исполнения).
Чтобы увидеть в действии работу всех методик, посмотрите на исходный код загрузки этого изображения:
https://www.industrialempathy.com/img/remote/ZiClJf.jpg
<img loading="lazy" decoding="async" style="background-size: cover; background-image: none;" src="/img/remote/ZiClJf.avif" alt="Sample image illustrating the techniques outlined in this post." width="4032" height="2268">
Методики оптимизации
Отзывчивый макет
Это понятная методика позволяет изображению занимать доступное горизонтальное пространство с сохранением соотношения сторон. В 2020-м браузеры научились резервировать корректное количество вертикального пространства под изображение до его загрузки, если в элементе
img
указаны атрибуты width
и height
. Это позволяет избежать накопительного сдвига макета.<style>
img {
max-width: 100%;
height: auto;
}
</style>
<!-- Providing width and height is more important than ever. -->
<img height="853" width="1280" … />
Ленивая отрисовка
Вторая методика сложнее. Новый CSS-атрибут
content-visibility: auto
говорит браузеру не думать о размещении изображения, пока оно не будет готово. У этого подхода несколько достоинств, главное из которых в том, что пока браузер не получит размытое изображение-заглушку или само изображение, он не будет его декодировать, экономя ресурсы процессора.Больше не нужен contain-intrinsic-size
В более ранней версии статьи объяснялось, как с помощью
contain-intrinsic-size
избежать эффекта CLS при использовании content-visibility: auto
. Но в Chromium 88 это больше не нужно в случае с изображениями, для которых указаны width
и height
. По состоянию на 27 января 2021 года content-visibility: auto
ещё не реализован в других браузерных движках, вероятно, они последуют примеру Chromium. Так что да, теперь с этим гораздо проще!<style>
/* This probably only makes sense for images within the main scrollable area of your page. */
main img {
/* Only render when in viewport */
content-visibility: auto;
}
</style>
AVIF
AVIF — самый свежий графический формат, который получил поддержку в браузерах. Сейчас он поддерживается в Chromium и по флагу в Firefox. Safari с ним пока не работает, но поскольку Apple является участником группы, разработавшей формат, в будущем этот браузер тоже должен поддерживать AVIF.
Этот формат примечателен тем, что он значительно превосходит JPEG. И этим выгодно отличается от формата WebP, изображения в котором не всегда меньше JPEG и который может повышать потребление ресурсов из-за отсутствия поддержки прогрессирующей загрузки.
Для реализации прогрессирующего расширения для AVIF можно воспользоваться
элементом picture
.Фактически элемент
img
вложен в picture
. Это может запутать, потому что img
иногда называют запасным решением для браузеров, не поддерживающих picture
, но по сути этот элемент помогает лишь с выбором src
, а своего макета у него нет. Отрисуется именно элемент img
, к нему вы и будете применять стиль.До недавнего времени было довольно сложно внедрять на серверной стороне AVIF-изображения, но свежие версии библиотек вроде sharp сильно облегчили эту задачу.
<picture>
<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.avif 1920w,
/img/Z1s3TKV-1280w.avif 1280w,
/img/Z1s3TKV-640w.avif 640w,
/img/Z1s3TKV-320w.avif 320w
"
type="image/avif"
/>
<!-- snip lots of other stuff -->
<img />
</picture>
Загрузка правильного количества пикселей
В приведённом выше коде есть атрибуты
srcset
и sizes
. С помощью селектора w
они говорят браузеру, какой нужно брать URL в зависимости от физического количества пикселей, которое потребуется для отрисовки изображения на конкретном устройстве. Это количество зависит от ширины изображения, которая вычисляется на основе атрибута sizes
(представляющего собой выражение из медиа-запроса).Благодаря этому браузер всегда будет загружать наименьшее возможное изображение, обеспечивающее наилучшее качество на конкретном устройстве. Либо он может выбрать самое маленькое изображение, если пользователь включил режим экономии данных.
Запасное решение
Браузерам, которые поддерживают только старые форматы изображений, можно с помощью
srcset
предоставить больше исходных элементов:<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.webp 1920w,
/img/Z1s3TKV-1280w.webp 1280w,
/img/Z1s3TKV-640w.webp 640w,
/img/Z1s3TKV-320w.webp 320w
"
type="image/webp"
/>
<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.jpg 1920w,
/img/Z1s3TKV-1280w.jpg 1280w,
/img/Z1s3TKV-640w.jpg 640w,
/img/Z1s3TKV-320w.jpg 320w
"
type="image/jpeg"
/>
Кеширование и неизменяемые URL
Встраивайте в URL изображения хеш количества байтов, которые занимает картинка. В примере выше я сделал это с помощью
Z1s3TKV
. При изменении изображения поменяется и URL, а значит вы можете применять бесконечное кеширование картинок. Кеширующие заголовки должны выглядеть так: cache-control: public,max-age=31536000,immutable
.immutable
— семантически правильное значение cache-control
, но сегодня оно мало поддерживается браузерами (я смотрю на тебя, Chrome). max-age=31536000
— запасной способ кеширования в течение года. public
нужен для того, чтобы ваш CDN кешировала изображение и доставляла его с границы сети. Но этот подход можно использовать только в том случае, если он не нарушает ваших правил соблюдения приватности.Ленивая загрузка
Добавив
loading=«lazy»
в элемент img
мы говорим браузеру начать извлекать изображение лишь тогда, когда оно готово быть отрисовано.<img loading="lazy" … />
Асинхронная расшифровка
Добавив
decoding=«async»
в элемент img
мы разрешаем браузеру расшифровывать изображение вне основного потока, чтобы эта процедура не помешала пользователю. Заметных недостатков у этого решения быть не должно, за исключением того, что оно не всегда применима по умолчанию в старых браузерах.<img decoding="async" … />
Размытая заглушка
Размытая заглушка — это инлайненное изображение, дающее пользователю какое-то представление о полноценной картинке, которая будет позднее загружена, без передачи данных по сети.
https://www.industrialempathy.com/img/blurry.svg
Несколько замечаний по реализации:
- Заглушка инлайнится как
background-image
изображения. Эта методика позволяет отказаться от второго HTML-элемента буквально прячет заглушку, когда загружается основное изображение, для этого не нужен JavaScript. - URI данных основного изображения обёртывается в URI данных SVG-картинки. Это делается потому, что размытие выполняется на уровне SVG, а не с помощью CSS-фильтра. То есть размытие выполняется однократно для каждого изображения при растеризации SVG, а не для каждого макета. Это экономит ресурсы процессора.
<img
style="
…
background-size: cover;
background-image:
url('data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http%3A//www.w3.org/2000/svg\'
xmlns%3Axlink=\'http%3A//www.w3.org/1999/xlink\' viewBox=\'0 0 1280 853\'%3E%3Cfilter id=\'b\' color-interpolation-filters=\'sRGB\'%3E%3CfeGaussianBlur stdDeviation=\'.5\'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type=\'discrete\' tableValues=\'1 1\'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter=\'url(%23b)\' x=\'0\' y=\'0\' height=\'100%25\' width=\'100%25\'
xlink%3Ahref=\'data%3Aimage/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAs0lEQVQI1wGoAFf/AImSoJSer5yjs52ktp2luJuluKOpuJefsoCNowB+kKaOm66grL+krsCnsMGrt8m1u8mzt8OVoLIAhJqzjZ2tnLLLnLHJp7fNmpyjqbPCqLrRjqO7AIeUn5ultaWtt56msaSnroZyY4mBgLq7wY6TmwCRfk2Pf1uzm2WulV+xmV6rmGyQfFm3nWSBcEIAfm46jX1FkH5Djn5AmodGo49MopBLlIRBfG8yj/dfjF5frTUAAAAASUVORK5CYII=\'%3E%3C/image%3E%3C/svg%3E');
"
…
/>
(Опциональная) JavaScript-оптимизация
Браузеры могут быть вынуждены растеризовать размытую заглушку, даже если изображение уже загружено. Проблему можно решить, убрав растеризацию при загрузке. Кроме того, если у вашего изображения есть прозрачные области, то эта оптимизация становится обязательной, иначе сквозь картинку будет просвечивать заглушка.
<sсript>
document.body.addEventListener(
"load",
(e) => {
if (e.target.tagName != "IMG") {
return;
}
// Remove the blurry placeholder.
e.target.style.backgroundImage = "none";
},
/* capture */ true
);
</sсript>
Дополнительно
Полезный инструмент, реализующий все описанные оптимизации: eleventy-high-performance-blog