Простые правила при работе с растровыми изображениями на каждый день
- вторник, 12 сентября 2023 г. в 00:00:26
Привет, меня зовут Денис, я руковожу направлением разработки в Домклик. Дополнительно несу ношу лидера frontend-направления в нашей компании. Не так давно я отрефакторил систему собеседований для frontend-разработчиков, попутно тестируя технические вопросы на внешних и внутренних респондентах. И пришёл к выводу, что множество мной опрошенных разработчиков, вне зависимости от уровня, не знают или просто не обращают внимание на базовые правила при работе с картинками. В результате на просторах интернета зачастую можно найти изображения размером 200 на 200 пикселей и весом в несколько мегабайтов со смещением макета, столь раздражающим пользователей. Если вам интересно, как практически без вложений улучшить пользовательский опыт, то прошу под кат.
Содержание:
Исходные данные и выбор основного формата
Изменение размеров
Оптимизация
WebP
Тег picture и srcset
Предзагрузка
Ленивая загрузка
Сдвиг макета
Object-fit
Для того, чтобы статья была наглядной и последовательной, я взял из одного из проектов моей команды картинку размером 1600 x 484 пикселей. Оригинал в png весит 1,1 Mб, а оригинал в jpg весит 807 Kб. Здесь впору задать себе вопрос, нужен ли альфа-канал? Если нужен, берём png, в противном случае берём jpg, так как он меньше весит, а качество визуально идентично в подавляющем большинстве случаев. Для следующих двух пунктов нам потребуется инструмент для изменения размеров и оптимизации изображений. Существует колоссальное количество инструментов как онлайн, так и десктопных. Лично я использую iLoveIMG, но вас, разумеется, это никак не должно ограничивать. Для конвертации исходных изображений в формат WebP использую cloudconvert.
Начнём с того, зачем менять разрешение и когда это необходимо. Благодаря этому мы можем сделать картинку легче и тем самым повысить скорость её загрузки. Если мы не собираемся использовать нашу картинку как фон во весь экран, а, например, хотим показать в блоке с размерами 400x121, то необходимо уменьшить её до размера (400x121)*2, чтобы картинка не стала выглядеть хуже на дисплеях с увеличенной плотностью пикселей, таких как Retina. Чтобы потом не возвращаться к этой главе, сразу предлагаю сделать картинки для обычных дисплеев размером 400x121. Будем ли мы их использовать, зависит от многих «но» и «если», но об этом позже. Ниже приведена таблица с разрешением и весом после изменения разрешения.
Формат | Разрешение | Вес |
png | 1600 x 484 | 1,1 Mб |
jpg | 1600 x 484 | 807 Kб |
png | 800 x 242 | 279 Kб |
jpg | 800 x 242 | 245 Kб |
png | 400 x 121 | 74 Kб |
jpg | 400 x 121 | 66 Kб |
Оптимизация изображений это всегда риск того, что что-то пойдёт не так. По этой причине я не использую всевозможные плагины для оптимизации изображений при сборке статики через Webpack. За много лет разработки неоднократно натыкался на испорченные изображения после выкатки в production. Используя всё тот же онлайн-инструмент, прогоняем все шесть картинок, получившихся в предыдущем пункте, и визуально осматриваем их на предмет отсутствия всевозможных артефактов и значительного ухудшения качества. Вес изображений после оптимизации привёл в табличке ниже.
Формат | Разрешение | Вес |
png | 1600 x 484 | 312 Kб |
jpg | 1600 x 484 | 177 Kб |
png | 800 x 242 | 82 Kб |
jpg | 800 x 242 | 52 Kб |
png | 400 x 121 | 22 Kб |
jpg | 400 x 121 | 15 Kб |
Что такое WebP, вы можете узнать из других статей, но если в двух словах: это уже устоявшийся формат изображений. Его преимущество в том, что весит он меньше, чем аналоги, сохраняет хорошее качество изображения и альфа-канал. Большинство браузеров уже давно поддерживают этот формат, но даже если вам нужно поддержать старый браузер, то всегда можно воспользоваться запасным вариантом, о котором я расскажу в следующей главе. Что ж, конвертировав исходные png-изображения в WebP можем получить такие результаты.
Разрешение | Вес |
1600 x 484 | 89 Kб |
800 x 242 | 23 Kб |
400 x 121 | 7 Kб |
Мы можем использовать picture для того, чтобы:
Добавить запасной вариант для браузеров, не понимающих формат WebP.
Дать браузеру возможность выбрать максимально подходящую картинку для отображения.
В пример ниже мы сообщаем браузеру о том, что ему следует использовать для дисплеев Retina изображения с разрешением 800x242, а для обычных — 400x121. Если браузер умеет работать с WebP, то использовать именно этот формат, иначе — png.
<picture>
<source srcset="800x242.webp 2x, 400x121.webp 1x" type="image/webp"/>
<img src="400x121.png" srcset="800x242.png 2x" alt="house"/>
</picture>
Но, как говорил Иван Васильевич: «есть нюанс». Об этом самом нюансе пойдёт речь в следующей главе.
Есть такая метрика веб-производительности, как Largest Contentful Paint (LCP). Она означает длительность загрузки самого большого визуального элемента в той области, которую видит пользователь при попадании на страницу. И, конечно же, при обнаружении изображения Lighthouse собирает с него метрики. Чтобы улучшить этот показатель, мы с вами уже изменили размер изображения, оптимизировали его и даже добавили WebP. Но есть ещё способ улучшить метрику, а именно: предварительно загрузить изображение. Делается это с помощью тега link в заголовке страницы:
<link rel="preload" as="image" type="image/webp" href="/800x242.webp"/>
Эта подсказка браузеру повышает приоритет загрузки изображения, а также начинает его загрузку ещё до обнаружения элемента img в DOM. И тут мы как раз подходим к нюансу. Если у нас были разные изображения для Retina и прочих дисплеев, то с предварительной загрузкой на текущий момент времени мы должны чётко знать, что и зачем загружаем. Проблема в ограниченной поддержке атрибута srcset для тега link. В данном случае лучше предварительно загрузить WebP с двойной плотностью пикселей и использовать эту картинку как для Retina, так и для обычных дисплеев. А код с запасным вариантом будет выглядеть немного проще.
<picture>
<source srcset="800x242.webp" type="image/webp"/>
<img src="800x242.png" alt="house"/>
</picture>
Теперь зайдём с другой стороны. Если картинка находится не в той области которую видит пользователь, заходя на страницу веб-приложения, то имеет смысл подумать о том, чтобы загрузить её попозже. Например, в тот момент, когда пользователь будет близко к изображению. На текущий день самые простые способы добиться такого поведения, это:
Как можно заметить, оба этих способа имеют довольно хорошую поддержку в браузерах. А вот что лучше использовать — решать вам. Могу лишь перечислить преимущества и недостатки. Например, первый вариант проще реализовать и он SEO-дружелюбный. Но вы не управляете тем, что и когда будет загружено, браузер сам задаёт пороговые значения расстояния от области просмотра. Иными словами, тяжёлая картинка за границами экрана может загрузиться сразу, что повлияет на скорость загрузки и отзывчивость сайта, особенно при медленном соединении. Пример с котиками тут.
Что касается intersectionObserver API, то процесс появления изображения лежит уже на наших плечах, и расстояние до контента задаём мы сами. А недостаток в том, что придётся написать пару строчек кода на JavaScript, к тому же этот вариант будет в любом случае подсовывать поисковикам либо неправильную ссылку на изображение, либо изображение будет отсутствовать в разметке вовсе. Но если вас не заботит SEO, то это не проблема.
Ещё одна из важнейших, по моему мнению, метрик из семейства web performance: это Cumulative Layout Shift (CLS). Она разделяет второе место по значимости вместе с Largest Contentful Paint(LCP) и имеет вес в 25 баллов в Lighthouse v10. Взглянем на пример ниже.
Я хотел купить кота, но невовремя загруженная картинка сдвинула из-под курсора кнопку. В лучшем случае это просто доставит мне неудобство, а в худшем — я нажму на загрузившийся рекламный баннер и случайно уйду на другой сайт, так и не выполнив целевое действие.
Так вот, очень важно указывать изображениям ширину (width
) и высоту (height
), это верный способ избежать смещения макета. А также поможет ленивой загрузке правильно высчитать расстояние до картинки. В результате после добавления картинке значений ширины и высоты сдвиг макета более не побеспокоит пользователей.
Довольно часто так случается, что контейнер, в котором необходимо показать изображение, по пропорциям не подходят друг другу, и картинке становится плохо.
Если вам нужно отображать картинки разной ширины и высоты, условно, в квадрате, то рекомендую использовать свойство object-fit
со значением cover
. Изображение без нарушения пропорций заполнит всю доступную область, с обрезкой всего, что не влезет.
Если важная часть изображения частично или полностью исчезла из области видимости, как на скриншоте выше, то мы можем позиционировать изображение при помощи свойства object-position
.
Очень здорово, когда дизайнеры продумывают всё до мелочей и приносят на блюдечке идеальные макеты с учётом всех граничных случаев, оптимизированные картинки и т. п. Но, по моему опыту, такое случается не слишком часто, поэтому всегда рекомендую своим ребятам самостоятельно за этим всем следить. Ведь кто, если не фронтенд?