<img>. Доклад Яндекса
Коротко представлюсь. Возможно, вы слышали о подкасте «Веб-стандарты» — иногда там можно услышать мой голос. И если новости в пабликах «Веб-стандартов» выходят с опечатками — скорее всего, это я. Работаю я в Яндекс.Поиске, разработчиком интерфейсов.
Сегодняшний доклад — по мотивам другого доклада. В 2019 году я успел вскочить в последний вагон и на последнем Web Standards Days прочитал доклад про ссылку.
Интригующее название было. Тогда из зала прозвучал вопрос: «Когда будет про следующие теги?» Сегодня вы смотрите доклад именно про следующий тег, о котором мне хотелось рассказать.
1995
Начнем с истории. Шёл 1995 год. Тогда впервые появился тег img в стандарте HTML 2.0. Вы можете найти спецификацию — в то время они писались гораздо более сухо, чем сейчас. Но там есть интересные моменты.
В стандарте HTML 2.0 можно найти, что атрибутов у img тогда было не то чтобы много.
Был атрибут SRC
, который дошёл до нас. Вы, кстати, можете увидеть тут у файла расширение xbm
— X BitMap. Вы такой формат, возможно, и не застали. Я не застал. Был атрибут ALIGN
, который позволял добавлять выравнивание этой картинке. Был ALT
, он уже тогда был важен. Были всякие прикольные штуки вроде карт ISMAP
, я про них ещё расскажу.
Кстати, интересный факт: в стандарте 1995 года есть пометка, что можно, а что не надо передавать в атрибут SRC
, и там не рекомендуется указывать HTML-файлы. Видимо, кто-то пытался.
До стандарта HTML 2.0 были альтернативы того, каким образом выводить картинку, но победил именно тег IMG
. И мы сейчас с ним живём.
2020
В 2020 году стандарт немножко поразноцветнее, и в нём гораздо больше подробностей.
Давайте поговорим про тег
, и начнём с простой конструкции, которую вы, скорее всего, когда-нибудь писали:
Этого достаточно, чтобы начать работать с картинкой. JavaScript, например, может взять этот код и дополнить. Возможностей много.
Но по-хорошему нужно добавить хотя бы атрибут src
. Он про то, что нужно куда-то сходить, что-то скачать, а то, что скачается, каким-то образом отобразить.
Когда вы так пишете, то даёте инструкцию браузеру сходить за ресурсом по относительному адресу
cats.png
. Браузер отправляет запрос с HTTP-заголовками, эти заголовки можно на сервере обрабатывать и принимать какие-то решения. GET /img/cats.png HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) ...
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8080/img/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7
Например, мы получили заголовок
Accept
. Он про то, с какими форматами изображений браузер умеет работать. Вы можете увидеть: браузер отправил информацию, что он умеет работать с WebP. Даже если запрошен был файл в формате PNG, можно всё-таки в качестве ответа подсунуть WebP, сэкономив трафик. Раз браузер говорит, что умеет, — пусть работает.Есть и другие сущности, которые можно получить из HTTP-заголовков. Их удобно использовать, чтобы экономить трафик, принимать другие решения.
Атрибут src
— мощная и сложная штука, потому что это один из немногих способов внутри HTML дернуть какой-нибудь URL. Ссылка — первый способ, но ссылка — это когда ты кликаешь сам, осознанно. А здесь пользователь ничего не нажимает, но куда-то идет запрос. Здесь речь и про безопасность тоже. Та ещё задача.
Форматы графики могут быть разные: gif, jpeg, ico, png, apng, tiff, svg, webp, avif и так далее. Это самые популярные из них. TIFF, кстати, до сих поддерживается в Safari. Другие браузеры я не проверял. Но официально поддержки вроде как нет.
Мы уже доросли до того, что AVIF — это поддерживаемый браузерами формат графики.
Про форматы изображений я в 2018 году читал отдельный доклад.
Он был про то, как любым способом доставлять картинки (в том числе через
background
), как и что сжимать.Вот еще один способ дёрнуть какой-нибудь URL без HTML:
const img = new Image();
img.onload = function() { /* ... */ };
img.onerror = function() { /* ... */ };
img.src = 'path/to/image.png';
Вы можете создать в JavaScript объект
Image
. Как только вы ему зададите src
, браузер попытается этот src
скачать. И по-хорошему вы должны добавить обработчик ошибок и обработчик загрузки.Получается, XHR не нужен! :)
Картинка — отличный способ дёрнуть URL, если вам не нужно обрабатывать нечто сложное. Всякие fetch
, разные счётчики, метрики или что там у вас на страницах — всё это вы можете отправлять, дёргая картинку. Возможно, кто-то из вас разбирался, как работают Facebook-пиксели и прочие счётчики. Там применяется как раз такой способ: если скрипт выключен — дёрни картинку. По этой картинке можно из HTTP-заголовков получить много полезной информации.
Нужно понимать, когда у картинки при загрузке вызывается обработчик onerror
в коде выше. Это произойдет в нескольких случаях:
- Когда
src
вы задали пустой:. Тогда дёрнется
onerror
, потому что браузер не знает, что показывать. - Если вы укажете путь к текущей странице. Получается рекурсивный вызов:
. В браузерах есть механизм защиты: не будет никакой рекурсии, он просто сразу бросит ошибку и не станет издеваться над собой.
- Если формат не поддерживается. Например, если вы из Internet Explorer решили загрузить формат WebP.
- Если вообще нет никаких данных о размерах картинки, браузер тоже не знает, как ему эту картинку нарисовать. Размеры могут прийти либо в метаданных, про них расскажу чуть позже, либо вы их задаёте атрибутами. Если в метаданных нет размеров и в атрибутах вы их тоже не задали, то браузер не знает, как и сколько резервировать места на странице, и бросает ошибку.
- Последнее — поломанное изображение, что-то с сетью или с самой картинкой. В целом, если что-то с сетью, скорее всего, какие-нибудь пакеты не дойдут и будет беда.
Окей, допустим, что картинка загрузилась. Это такой счастливый пример для разработчика — всё хорошо выглядит, нормально загрузилось.
А вот такой пример мы же тоже в жизни видели, да?
Почему-то ресурсы не доходят, и браузер рисует какую-то странную иконку. Что нужно делать?
Атрибут
ALT
был ещё в первой версии спецификации IMG
. Это замещение картинки для невизуальных браузеров или на случай, если картинка сломалась.Если вы просто зададите alt
, это уже не будет какая-то иконка в вакууме. Это будет текст. И если вы копируете фрагмент страницы и внутри находится картинка с alt
, этот текст попадает в буфер обмена. Делать так даже в какой-то мере удобно, если там что-то информативное. Когда картинку нельзя скопировать, то можно скопировать хотя бы текст, описывающий эту картинку.
Есть важный нюанс: IMG
— заменяемый элемент. Это значит, если картинка загрузилась, то всякие псевдоэлементы вы ему задать не можете — потому что IMG так себя ведет. Но если картинка не загрузилась, там появляется целый shadow-root, который вы можете посмотреть в Chrome DevTools, если поставите галочку «Show user agent shadow DOM» в настройках. Так можно увидеть, что вместо картинки показывается полноценный новый HTML. Вы можете добавлять туда псевдоэлементы before и after. И это можно использовать.
Например, Ире Адеринокун предлагает интересный способ.
img {
font-family: 'Helvetica';
color: darkred;
text-align: center;
min-height: 3em;
display: block;
position: relative;
}
img::before {
content: "Картинка поломалась :(";
display: block;
}
img::after {
content: "(url: " attr(src) ")";
display: block;
}
Когда вы указываете картинке, например, стили про текст, шрифты, цвета, то вы на самом деле можете таким образом, во-первых, стилизовать этот текст в alt
. Во-вторых, добавить сюда ::before
и ::after
. И дать пользователю или разработчику понять, что картинка не загрузилась.
Например, для разработчика — возможно, для дебага — будет полезно добавить, как здесь в примере, img∷after
. И вы можете при помощи CSS-функции attr()
достать src
из картинки и показать сообщение: вот эта картинка сломалась, почини, пожалуйста.
Кажется, это даже можно автоматизировать. Например, парсить страницу через Puppeteer или еще чем-нибудь. Если какие-то картинки сломались, находите, какие именно, даже таким визуальным способом, и где-то их собираете. Хотя, конечно, профилировать запросы в сеть в этом смысле гораздо удобнее. Но есть и такой способ.
Но всё равно такой плейсхолдер выглядит не очень, да? А что, если мы его стилизуем?
img::after {
content: "эмодзи" " " attr(alt);
z-index: 1;
line-height: 3em;
color: rgb(100, 100, 100);
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ccc;
}
Если у нас есть доступ к before и after, там же можно много чего натворить. Например, добавить after, приподнять его z-индексом над картинкой, чтобы не было видно стандартной иконки. И стилизуйте как хотите. Я эмодзи вставил — работает.
Главное — фантазия. У Лин Фишер, например, фантазии много, и есть сайт a.singlediv.com, где она на одном div
делает целые анимированные произведения искусства. У вас есть before и after, целых два псевдоэлемента, которые можно стилизовать. Задумайтесь.
Но, допустим, мы вообще не хотим сломанных картинок. Можно ведь использовать сервис-воркер! Как с ними быть?
Еще один пример от Ире Адеринокун.
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
// обработка ответа
})
.catch((err) => {
// обработка ошибки
})
)
});
В сервис-воркере есть возможность, если мы вешаем обработчик на событие
fetch
и видим, что запрос сработал хорошо, просто вернуть его результат браузеру. Сходили за ресурсом — он есть — вернули.Но есть два случая: либо мы сходили за ресурсом и ответ — не ок, либо на каком-то из этапов выбросилась ошибка. Это, скорее всего, значит, что пользователь в этот момент находится офлайн. Что можно сделать?
Можно определить, был ли это вообще поход за картинкой или не за картинкой, сделать вспомогательную функцию для этого.
function isImage(fetchRequest) {
return fetchRequest.method === "GET"
&& fetchRequest.destination === "image";
}
Можно добавить на установку сервис-воркера поход за картинкой
broken.png
, которая, например, будет плейсхолдером для всех сломанных картинок. Положить её в кэш, когда интернет ещё есть.self.addEventListener('install', (e) => {
self.skipWaiting();
e.waitUntil(
caches.open("precache").then((cache) => {
cache.add("/broken.png");
})
);
});
Затем мы просто берем непонятные нам случаи — когда либо что-то с запросом, либо выбрасывается ошибка — и возвращаем из кэша уже то, что лежит на локальном устройстве пользователя.
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
if (isImage(e.request)) {
return caches.match("/broken.png");
}
})
.catch((err) => {
if (isImage(e.request)) {
return caches.match("/broken.png");
}
})
)
});
Вместо непонятной дефолтной иконки мы можем вернуть стильный плейсхолдер. Не стесняйтесь призвать дизайнера в помощь, чтобы сделать это красиво, а не своими силами рисовать плейсхолдеры при помощи CSS.
Конечно, можно пойти дальше и вообще все картинки закэшировать, чтобы они всегда возвращались. Но это не так интересно.
Хорошо. У нас есть атрибут alt
. Мы его заполнили, подписали картинку.
Что будет, если мы атрибуту
alt
зададим пустое значение? Во-первых, браузер не будет показывать эту сломанную иконку, когда картинка не загрузится. Вы даже не увидите, что у вас что-то не так. Не загрузилось, размеров нет, будет пусто. Во-вторых, для невизуальных браузеров это ещё и призыв не озвучивать картинку.
Таким образом, если у вас декоративное изображение, вы можете его спрятать.
Доступность
Картинки — визуальная штука. Людям с хорошим зрением, конечно, хорошо в интернетах сидеть и всё видеть, но я рекомендую вам сходить на weblind.ru, — отличный ресурс, где собраны рекомендации, как минимальными усилиями сделать ваши сайты чуть более доступными.
Короткая выдержка оттуда. Есть изображения информативные и декоративные. Информативные — это которые про контент, когда пишется текст, следом идёт картинка, которая дополняет этот текст, и вы в alt
можете описать, что на этой картинке. Описывайте так, что если бы вы читали этот текст-описание, то как будто картинки даже не и было, можно её себе представить.
Декоративные — не несут особого смысла и, скорее всего, сделаны для красоты. Их можно спокойно выбросить, и ничего с текстом не случится.
У сайта «Веб-стандартов» есть CONTRIBUTING.md для тех, кто пишет статьи, переводы и так далее. У нас есть рекомендации, как всё это делать лучше. Совместно с Татьяной Фокиной сделали классные советы, которыми я хочу с вами поделиться.
- Если картинка декоративная — ставится пустой
alt
, но ставится обязательно. - Если картинка со смыслом, то в
alt
вы пишете ёмкое описание изображения. Не надо коротко — «картинка», «город». Опишите, что там находится. Избегайте повторения предложений, которые уже есть на странице, потому что скринридеры читают текст, а потом вы точно такой же вставляете вalt
, зачем? Он прочитается два раза. - Совет, который иногда даже вызывает холивары, хотя я не понимаю, почему. В конце описания в
alt
ставьте точку. Когда скринридер читает и видит точку, он делает паузу перед тем, как читать что-то дальше. Так вот,alt
— это предложение. Не стесняйтесь ставить там точку, если это реально что-то контентное.
Про декоративные изображения. Как их ещё можно скрывать от скринридеров?
Можно унести их в background-image
. Это тот случай, когда можно сделать какой-нибудь div
вместе img
, тогда он спрячется, и всё.
Про пустой alt
мы уже поговорили.
Можно добавить атрибут role="presentation"
, всё это присыпать aria-hidden="true"
, чтобы наверняка.
Это хорошие практики. Если действительно что-то мешает чтению картинки на слух, используйте их.
Можно ещё вашу картинку обернуть в figure
.
Так вы можете ёмкое описание картинки вставить в
alt
. А в figcaption
описать сам файл. Например, указать фотографа или источник фотографии. figcaption
— он скорее не про описание картинки, а про описание файла.Если вы хотите запихнуть в картинку нечто сложное, например график, то я рекомендую посмотреть в сторону SVG и выставить для него правильную роль.
Не забывайте про
role="img
, это важно.Можно добавить aria-label
, aria-described-by
и полностью описать всё, что у вас на графике есть, полезно и подробно, при помощи desc
, который есть у SVG.
Здесь я рекомендую посмотреть замечательный доклад Сергея Кригера о том, как делать доступные графики, — на случай, если вам действительно нужно сделать график и описать его, чтобы все могли им пользоваться.
Вернёмся к котятам.
Я иногда видел советы —, а зачем alt
, давайте писать всё в атрибут title
. Что такое title
? Это когда вы наводите на картинку, проходит сколько-то секунд в зависимости от браузера, и появляется маленький тултипчик.
Рекомендация: не делайте так. Это не только моя рекомендация. Во-первых, как часто вы ловили себя на том, что специально наводите курсор на картинку, чтобы прочитать, что на ней? Я себя только во время подготовки к докладу попросил так сделать. Во-вторых,
title
не участвует в построении дерева доступности. Многие скринридеры его банально не читают. Да, есть нюансы, но в целом эта штука кажется немного бесполезной, если есть alt
. Есть атрибут, который помогает, используйте его. Размеры
Мы уже немного говорили о размерах. В браузере мы каким-то образом задаём ширину:
width=500
. Обратите внимание, что мы указываем не »500 px». Не нужно задавать единицы измерения.Итак, вы указываете это значение ширины. Тогда возникает вопрос: откуда браузер берёт второй размер, высоту?
А если задать только высоту, как браузер рассчитывает ширину? Откуда он её берет?
Здесь можно копнуть глубже. Например, вы можете расковырять ваши картинки как бинарные файлы и посмотреть в спецификации.
В спецификации PNG первые восемь байт вообще прибиты гвоздями, эти байты и говорят, что это PNG-файл. Дальше всякая служебная информация, и есть кусочек, я его подсветил выше, который говорит: ширина и высота вот такие. Прямо в первых, буквально заголовочных кусках файла есть размеры.
С GIF всё ещё проще, потому что сам формат проще.
Тут можно сходу сказать, что у этой гифки размеры 500×275. Можно прямо в текстовом редакторе посмотреть.
С JPEG — сложнее. Cходу такой же красивый и наглядный пример вам собрать не смог, потому что там много интересного, но не очень очевидного. С WebP даже не пытался.
Но для любых графических форматов есть правило: в самом верху файла, буквально с первыми пакетами при скачивании приходит размер. Как только браузер делает запрос за картинкой, и картинка начинает приходить, браузер тут же выцепливает эти размеры и резервирует с их учётом под картинку место.
Очень рекомендую доклад Полины Гуртовой «Картинки как коробки. Что же там внутри?». Полина классно рассказала, как вообще всё внутри устроено, про разные форматы, в том числе про PNG, о котором я только что говорил. Есть видео и расшифровка на Хабре, обязательно почитайте.
Если в метаданных картинки нет этих самых размеров, то мы можем их задать сами: width
, height
выставили, и счастье.
Тут важно задавать правильные размеры. Я думаю, все вы видели эти ужасные случаи, когда прямоугольная картинка из-за криво заданных размеров зачем-то вписывается в квадрат, и получается нечто непропорциональное. Поэтому здесь хорошо бы, конечно, прикрутить какую-то автоматику. Но если вы делаете это руками, скорее всего в бизнес-процессах есть этап добавления картинки где-нибудь в вашей админке, если контентом занимается контент-менеджер. Там вы можете получить размеры, сохранить их в базу и потом подставлять эти размеры в клиентский код. Например, подставлять физические размеры картинки, как есть. А уже дальше в CSS вычислить, что с ними делать.
Есть ещё интересный атрибут intrinsicsize
, он про пропорции. Если мы не хотим задавать четкие значения width
, height
, а хотим делать картинки 16×9 или 400×300, то можем задать это таким атрибутом.
Но на самом деле нет, уже не можем. Авторы спецификаций обсуждали-обсуждали и пришли к тому, что, кажется, можно сделать более элегантно.
Подход такой. Раз есть атрибуты
width
и height
, которые вроде как рекомендуется указывать всегда, в CSS можно использовать свойство aspect-ratio
, которое их получает функцией attr()
, или прибивает гвоздями, что у этой картинки пропорции должны быть 16×9 или 500×500.Поддержка у этого свойства достаточно хорошая, на мое удивление. Уже можно, даже нужно, 66% пользователей будут счастливы.
Есть нюанс: вы можете задавать не конкретные пропорции, а только маппинги на width
и height
. И так умеют уже 89% браузеров. Поэтому вперёд!
Загрузка
Дальше можно поиграться с тем, как вообще эту картинку получить.
Для начала можем задать, какую информацию мы отправляем на сервер. Вы же иногда подключаете картинки из каких-нибудь CDN или с внешних ресурсов, не только у себя их держите.
Так вот, referrerpolicy
— это атрибут, который говорит, например: тут отправляй домен, а тут вообще не отправляй заголовок referrer
.
res.cloudinary.com
:method: GET
:path: /.../picture.png
:scheme: https
accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7
cache-control: no-cache
pragma: no-cache
referer: http://example.com/
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) …
В данном случае я чётко говорю браузеру: отправляй, пожалуйста, только домен и все. Больше ничего не надо, полный URL отправлять не надо. В конце концов это тоже информация, которую тот, кто хранит картинку, может использовать.
Если вы доверяете своему CDN, отправляйте всё что угодно, конечно. Но лучше аккуратно. По умолчанию стоит такое значение referrerpolicy
, которое говорит, что с http-сайта сходить за https-картинкой можно. А вот наоборот не отправляются заголовки, когда это не безопасно.
Ещё один вариант — это когда вы ходите за картинкой на другой домен.
Если просто рисовать картинку через тег — да, всё нарисуется. Но если вы хотите, например, получить и нарисовать эту картинку где-нибудь в
canvas
, то вы таким образом получаете доступ чуть ли к этим бинарным данным, к исходнику картинки.const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById('source');
ctx.drawImage(image, 0, 0);
// DOMException: Failed to execute 'getImageData'
// on 'CanvasRenderingContext2D':
// The canvas has been tainted by cross-origin data.
Понятно, что мы, разработчики, можем эту картинку скачать себе любым другим способом и расковырять её у себя локально. Но если вы не настроили cross-origin запрос, то от браузера получите ошибку DOMException. Она будет говорить: простите, данные с другого домена — нельзя. И в этом случае нужно добавить атрибут
crossorigin
картинке, чтобы всё заработало. У него есть в том числе и значение. По умолчанию это анонимный CORS-запрос. Он такой идёт на сервер: «Ну, пожалуйста, можно я поковыряюсь в байтиках?» И если сервер в ответ: «А, давай! Пожалуйста, на тебе Accept, бери всё и ковыряй», — то в canvas вы сможете поиграться с данными.
На самом деле это история не только про canvas. Например, вы хотите бинарные данные в LocalStorage положить. Подходов и применений много, но вам нужно получить доступ к бинарникам.
Яркий пример — CodePen. Если вы там когда-нибудь рисовали на canvas что-то из внешних изображений, то, возможно, сталкивались с ошибкой доступа к данным. Я сталкивался. Приходилось хранить картинки в base64, потому что у меня бесплатный аккаунт.
Ещё есть атрибут load
.
И он вроде как классный, но есть нюансы. У него три значения:
auto
— значение по умолчанию, браузер сам решает, когда загружать картинку.eager
— значит, загружать сразу. Когда HTML-парсер находит картинку, он сиюминутно отправляет за ней запрос.lazy
— это когда мы говорим браузеру: «Мне когда-нибудь эта картинка понадобится, но давай ты сам определишь, когда именно, и тогда за ней и сходишь». Полезно для экономии трафика.
На самом деле атрибут обалденный. Если по умолчанию он включается и ставится
lazy
везде, кроме, конечно, первого viewport, то вы сходу получаете экономию трафика и пользователю, и себе. Вам же, наверное, нужно оплачивать инфраструктуру. Это бесплатная польза минимальными усилиями.Но, например, в исходном коде Chromium можно найти такие интересные настройки.
Offline — 8000
Slow 2G — 8000
2G — 6000
3G — 2500
4G — 1250
Когда соединение офлайн или очень медленное, то браузер загружает все картинки в пределах 8000 пикселей от текущего вьюпорта. При этом на самом быстром соединении — в пределах 1250 пикселей.
Причем значения постоянно меняются. Я так понимаю, разработчики браузеров проводят замеры, эксперименты, в интернете идут холивары, какие числа более правильные, потому что каждый браузер немножко по-своему их подбирает.
Чем хуже соединение, тем больше картинок качается. Это не очень очевидно, и вам нужно понимать, что это не про экономию трафика, а именно про скорость соединения.
Зачем так сделано — объяснение простое. Пользователь обычно страницы скроллит. И на медленном соединении, пока он доскроллит, уже всё должно быть хорошо, хорошо видны картинки. У пользователя должен быть потрясающий опыт пользования вашей страницей.
Чем медленнее соединение, тем раньше за картинкой нужно сходить. На быстром соединении оно вроде как быстро загрузится, зачем париться?
Но эти значения — 1250 — достаточно большие. Используйте на свой страх и риск, если вы в первую очередь задумывались про экономию трафика, а не про скорость.
В официальной статье про lazy loading на web.dev описан способ, как это сделать с поддержкой браузеров, которые не умеют в нативную ленивую загрузку.
Если умеют — вы сможете в прототипе
HTMLImageElement
найти свойство loading
. Тогда, например, в атрибут data-src
можно положить URL картинки, как мы делали еще до появления этого свойства, а потом его перекинуть в src
. Если нет, то взять библиотечку, которая сделает то же самое, но уже без браузерных механизмов. Если вновь поковыряться в коде того же Chromium, там точно так же создается Intersection Observer, как если бы вы это делали руками. И точно так же, но с более внутренними механизмами, делается всякая магия: «Ага, здесь мы определяем, сколько до вьюпорта, дальше качаем».
Если у пользователя JS выключен, loading
ставьте сколько хотите и какой хотите — всё равно будет применено значение auto
. Это сделано для приватности данных, потому что если JS выключен, картинкой вы всё ещё можете по скорости соединения и порядку скачивания при ленивой загрузке вычислить, что это за пользователь. Фингерпринтинг. А если пользователь выключает JS, то он, может быть, не хочет фингерпринтинг.
Есть ещё одна интересная штука — атрибут decoding
.
Это когда вы уже прямо тюните сайт под перформанс.
- Вы можете выставить значение
sync
. Это когда у вас рисуется что угодно, например текст. И картинка тут же декодируется, чтобы нарисоваться рядом. async
— это сказать браузеру: «Когда будут ресурсы, тогда и декодируй».- И
auto
— это, опять же, пусть решает браузер, значение по умолчанию.
У Эдди Османи есть отличное объяснение в твиттере, почему этот атрибут в целом важен:
В голове у нас процесс такой: картинка загрузилась, потом нарисовалась. На самом деле в серединке, между «загрузилось» и «нарисовалось», есть декодинг и ресайз картинки, не нулевые по времени процессы.
Декодинг картинки на среднестатистическом устройстве Moto G — это 300 миллисекунд в примере Эдди, заметно даже глазом. Стоит задуматься, что большие картинки декодируются дольше и, может, их надо позже декодировать, если не хотим зря тратить ресурсы. Важно: желательно не давать браузеру и ресайзить тоже. Пускай сразу приходит только самое нужное.
Помните: когда у вас выполняется JS, он блокирует основной поток. Будем считать, что браузер однопоточный, и вы блокируете рендеринг. Если вы выполняете долгий JS, то и картинка не нарисуется. Потому что браузер в этот момент занят, он ваш while (true)
обрабатывает.
Штука, которая вызывает много вопросов по первости: как работают srcset
и sizes
? Я в несколько подходов разбирался и думаю, что наконец разобрался.
Есть такая запись.
- В
srcset
вы задаете картинки, а рядом — маппинг на физический размер этих изображений. Что, значит физический? Это значит, что вы нажимаете в контекстном меню в системе «Информация о файле» и сколько пикселей лежит в этом файле, столько и нужно указывать.srcset
— про физическую ширину. - Дальше в
sizes
вы указываете размер области под картинку, которая должна загрузиться. Берете медиавыражения и указываете, что, например, если у viewport ширина 320 пикселей, значит, мне нужно выделить область под картинку в 280 пикселей. Вы одновременно таким образом задаетеwidth
у картинки.
Что дальше сделает браузер? Он из
srcset
постарается выбрать самое подходящее по размеру изображение. Здесь самое интересное. Спеки четко не прописаны, инструкция — грузить изображение, которое явно больше. То есть браузеры решают сами, как это оптимально сделать. И в разных браузерах поведение разное.
Chrome, например, выбирает картинку больше. То есть ближайшую не меньшую, так будет правильнее. Вот вы указали sizes
. Например, у вас сработало: «Бери 280 пикселей» и Chrome постарается найти ближайшую картинку: 320. Если выбор пал на размер 440, какую картинку выбрать по ширине: 320 или 480? Он выберет 480, потому что ближайшая не меньшая.
В srcset
ещё можно указывать плотность пикселей.
»2x» — это Retina, сюда мы можем засунуть наши изображения повышенной чёткости и отдельно их загружать. Прелесть подхода: если HTML-парсер не понимает инструкцию
srcset
, но умеет в src
, то он загрузит src
. Это как fallback, будет работать во всех браузерах, даже самых старых.Важно: чтобы с такой адаптивностью работать, не забывайте использовать метатег viewport
, где вы будете чётко задавать, как соотносится физическая и логическая ширина на устройстве.
Иначе вы будете очень долго дебажить, почему вы просите одну картинку загрузить, а она вообще не та.
Вы можете позамерять, насколько эффективно вы используете картинки на странице. Например, я зашел на сайт GDG Russia при помощи пакета imaging-heap от Filament Group.
Там есть очень интересный логотип — всегда приходит размером 1706 пикселей. При этом этот логотип всегда занимает не больше 20 пикселей. В целом там трафика немного, картинка достаточно оптимизирована, проглядывается конструктор сайтов Tilda. Но imaging-heap позволяет посмотреть, насколько эффективно картинки используются на странице. Я им часто пользуюсь. Он умеет не только смотреть в теги img
, но ещё и в background-image
умеет залазить. Обязательно воспользуйтесь, поинспектируйте хотя бы свой сайт.
Давайте расширим img
. Уже давно есть тег picture
, обёртка над img
, своего рода прокси, который в себя принимает кучу всяких штук и по факту потом всё равно прокидывает все это в img
.
Вы можете добавить разные
source
. Кстати, вопрос: почему source
обрабатываются сверху вниз? Применяется первый срабатывающий source
, дальше не применяется. Почему не последний? Объяснение тоже простое. Как работает HTML-парсер в браузере? Просто идёт по коду слева направо и парсит его. Когда ваши страницы приходят чанками, то браузер получит кусочек, в нём найдёт picture
, и если по source
сразу понятно, что он подходит, браузер сразу сможет отправить запрос за подошедшей картинкой, не дожидаясь следующего HTML-чанка. Клёво. Поэтому source
работает сверху вниз: что первое пришло, то и применится.
Прелесть в том, что мы можем поддерживать разные форматы. Не так уж давно зарелизился формат AVIF, в Chrome он уже поддерживается.
У Джейка Арчибальда есть потрясающая статья, которая объясняет, почему этот формат клёвый, в каких случаях он работает хорошо. Вы можете увидеть, что для некоторых картинок AVIF сжимает лучше, чем SVG. У меня в голове это п