Описательный синтаксис

В этом разделе вы узнаете, как дать браузеру возможность выбирать изображения, чтобы он мог принять оптимальное решение о том, что показывать. Атрибут srcset — это не метод замены источников изображений в конкретных точках останова и он не предназначен для замены одного изображения на другое. srcset позволяет браузеру решать довольно сложную проблему, не зависящую от нас: незаметно запрашивать и отображать источник изображения, адаптированный к контексту просмотра пользователя, включая размер области просмотра, плотность экрана, предпочтения пользователя, пропускную способность и множество других факторов.

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

Описание плотности с помощью x

<img> с фиксированной шириной будет занимать одинаковую часть области просмотра в любом контексте, независимо от плотности дисплея пользователя — количества физических пикселей, составляющих его экран. Например, изображение с собственной шириной 400 пикселей будет занимать почти всю область просмотра браузера как на исходном Google Pixel, так и на гораздо более новом Pixel 6 Pro — оба устройства имеют стандартную область просмотра шириной 412 пикселей.

Однако у Pixel 6 Pro гораздо более чёткий дисплей: у 6 Pro физическое разрешение составляет 1440×3120 пикселей, а у Pixel — 1080×1920 пикселей, т. е. количество аппаратных пикселей, составляющих сам экран.

Соотношение между логическими пикселями устройства и физическими пикселями — это соотношение пикселей устройства для этого дисплея (DPR). DPR рассчитывается путем деления фактического разрешения экрана устройства на количество CSS-пикселей области просмотра.

Так, DPR у оригинального Pixel составляет 2.6, а у Pixel 6 Pro — 3.5.

iPhone 4, первое устройство с DPR больше 1, сообщает о соотношении пикселей устройства равное 2 — это значит, что физическое разрешение экрана в два раза больше логического. У всех устройств до iPhone 4 DPR был равен 1: один логический пиксель на один физический пиксель.

Если вы видите изображение шириной 400px на дисплее с DPR равным 2, каждый логический пиксель отображается на четырёх физических пикселях дисплея: двух горизонтальных и двух вертикальных. Изображение не выигрывает от высокой плотности отображения — оно будет выглядеть так же, как и на дисплее с DPR равным 1. Конечно, всё, что «рисуется» механизмом отображения браузера — текст, CSS-фигуры или SVG, например, — будет рисоваться с учётом более высокой плотности дисплея. Но, как вы узнали ранее, растровые изображения представляют собой фиксированные сетки пикселей. Хотя это не всегда очевидно, растровое изображение, увеличенное для соответствия дисплею с высокой плотностью, будет выглядеть с низким разрешением по сравнению с окружающей страницей.

Чтобы предотвратить такое масштабирование, визуальное изображение должно иметь собственную ширину не менее 800 пикселей. При уменьшении масштаба для размещения в макете шириной 400 логических пикселей 800-пиксельное изображение имеет вдвое большую плотность пикселей, и на дисплее с DPR со значением 2 оно будет выглядеть красивым и чётким.

<style> img { width: 100%; max-width: 400px; } p { font-size: 1.4em; } body { display: flex; } div { width: 50%; } </style> <div> <h1>Изображение с низкой плотностью</h1> <img src="image/aquilegia-hybrida_400.jpg"> <p>Это изображение имеет собственную ширину <code>400px</code> и отображается с шириной <code>400px</code> пикселей.</p> </div> <div> <h1>Изображение с высокой плотностью</h1> <img src="image/aquilegia-hybrida_800.jpg"> <p>Это изображение имеет собственную ширину <code>800px</code> и отображается с шириной <code>400px</code> пикселей.</p> </div>

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

Как вы узнали в предыдущем разделе, пользователю с дисплеем низкой плотности, просматривающему источник изображения, масштабированный до 400px, нужен источник шириной 400px. В то время как изображение гораздо большего размера будет визуально работать для всех пользователей, огромный источник изображения с высоким разрешением, отображаемый на маленьком дисплее с низкой плотностью, будет выглядеть как любое другое маленькое изображение с низкой плотностью, но работать будет гораздо медленнее.

Как вы уже догадались, на мобильных устройствах DPR, равный 1, встречается крайне редко, хотя в контексте «настольных» браузеров он все ещё распространён. Согласно данным, предоставленным Мэттом Хоббсом, примерно 18% сеансов просмотра сайта GOV.UK с ноября 2022 года имеют DPR, равный 1. Хотя изображения высокой плотности будут выглядеть так, как ожидают эти пользователи, они будут требовать гораздо большей пропускной способности и затрат на обработку, что особенно важно для пользователей старых и менее мощных устройств, которые по-прежнему имеют дисплеи низкой плотности.

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

Атрибут srcset определяет одного или нескольких кандидатов для отображения изображения, разделяя их запятыми. Каждый кандидат состоит из двух частей: адреса, как и в атрибуте src, и синтаксиса, описывающего источник изображения. Каждый кандидат в srcset описывается присущей ему шириной («синтаксис w») или предполагаемой плотностью («синтаксис x»).

Синтаксис x — это сокращение, означающее «данный источник подходит для дисплея с такой плотностью». К примеру, кандидат, за которым следует 2x, подходит для дисплея с DPR, равным 2.

<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">

Браузерам, поддерживающим атрибут srcset, будут представлены два кандидата: файл double-density.jpg, который, по описанию 2x, подходит для дисплеев с DPR равным 2, и файл low-density.jpg в атрибуте src — этот кандидат выбирается, если в srcset не найдено ничего более подходящего. В браузерах, не поддерживающих srcset, этот атрибут и его значение будут проигнорированы и как обычно будет запрошено содержимое src.

Значения, указанные в атрибуте srcset, легко принять за инструкции. Значение 2x сообщает браузеру, что связанный с ним исходный файл подходит для использования на дисплее с DPR, равным 2 — это информация о самом источнике. Он не говорит браузеру, как использовать этот источник, а просто информирует его о том, как этот источник может быть использован. Это тонкое, но важное различие: перед нами изображение двойной плотности, а не изображение для использования на дисплее двойной плотности.

Разница между синтаксисом, который говорит «этот источник подходит для дисплеев 2x», и синтаксисом, который говорит «используйте этот источник на дисплеях 2x», незначительна, но плотность дисплея — это только один из огромного количества взаимосвязанных факторов, которые браузер использует для принятия решения о выборе кандидата для показа изображения, и только некоторые из них вы можете знать. Например: по отдельности вы можете определить, что пользователь включил в браузере предпочтение экономии полосы пропускания через медиа-запрос prefers-reduced-data, и использовать его для того, чтобы всегда показывать пользователям изображения с низкой плотностью, независимо от плотности отображения, но если так не будет реализовано последовательно каждым разработчиком на каждом сайте это не принесет особой пользы пользователю. На одном сайте их предпочтения могут быть учтены, а на другом они столкнутся со стеной изображений, замедляющих пропускную способность.

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

Описание ширины с помощью w

Атрибут srcset принимает второй тип дескрипторов для кандидатов в источники изображений. Это гораздо более мощный метод и для наших целей проще для понимания. Вместо того чтобы отмечать, что кандидат имеет размеры, соответствующие заданной плотности отображения, синтаксис w описывает присущую каждому кандидату ширину источника. Опять же, каждый кандидат идентичен, за исключением размеров — то же содержимое, то же кадрирование и то же соотношение сторон. Но в данном случае вы хотите, чтобы браузер пользователя выбирал между двумя кандидатами: small.jpg — источник с собственной шириной 600px, и large.jpg — источник с собственной шириной 1200px.

srcset="small.jpg 600w, large.jpg 1200w"

Это не говорит браузеру, что делать с этой информацией, а просто предоставляет ему список кандидатов для показа изображения. Прежде чем браузер примет решение о том, какой источник отобразить, необходимо предоставить ему немного больше информации: описание того, как изображение будет отображаться на странице. Для этого используйте атрибут sizes.

Описание использования с помощью sizes

Браузеры невероятно производительны, когда речь идёт о передаче изображений. Запросы к изображениям инициируются задолго до запросов к таблицам стилей или JavaScript — зачастую даже до того, как разметка будет полностью анализирована. Когда браузер выполняет эти запросы, у него нет никакой информации о самой странице, кроме разметки — возможно, он ещё даже не инициировал запросы на внешние таблицы стилей, не говоря уже об их применении. В тот момент, когда браузер анализирует вашу разметку и начинает выполнять внешние запросы, у него есть только информация на уровне браузера: размер области просмотра пользователя, плотность пикселей на его дисплее, предпочтения пользователя и т. д.

Это ничего не говорит нам о том, как изображение должно быть отображено в макете страницы — браузер даже не может использовать область просмотра для определения верхней границы размера <img>, поскольку изображение может занимать горизонтально прокручиваемый контейнер. Поэтому нам нужно предоставить браузеру эту информацию и сделать это с помощью разметки. Это всё, что мы сможем использовать для этих запросов.

Как и srcset, атрибут sizes предназначен для того, чтобы сделать информацию об изображении доступной сразу после анализа разметки. Так же как атрибут srcset является сокращением для «вот исходные файлы и присущие им размеры», атрибут sizes является сокращением для «вот размер изображения в макете». То, как вы описываете изображение, относится к области просмотра — опять же, размер области просмотра является единственной информацией о макете, которой располагает браузер при запросе изображения.

В печатном виде это может звучать несколько запутанно, но на практике понять это гораздо проще:

<img
 sizes="80vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">

Здесь значение sizes сообщает браузеру, что пространство в нашем макете, которое занимает элемент <img>, имеет ширину 80vw — 80% от области просмотра. Помните, что это не инструкция, а описание размера изображения в макете страницы. Здесь не говорится «сделайте так, чтобы это изображение занимало 80% области просмотра», а говорится «это изображение будет занимать 80% области просмотра после отображения страницы».

<style> body { margin: 0; } div { width:80vw; } img { max-width: 100%; } </style> <div> <img sizes="80vw" srcset="image/pontederia-crassipes_600.jpg 600w, image/pontederia-crassipes_1200.jpg 1200w, image/pontederia-crassipes_2000.jpg 2000w" src="fallback.jpg" alt="..."> </div>

Здесь ваша работа как разработчика закончена. Вы точно описали список возможных источников в srcset и ширину вашего изображения в sizes, так что, как и в случае с синтаксисом x в srcset, остальное зависит от браузера.

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

Вы сообщили браузеру, что это изображение будет занимать 80% от доступной области просмотра — таким образом, если мы отобразим этот <img> на устройстве с шириной области шириной 1000 пикселей, это изображение будет занимать 800 пикселей. Затем браузер возьмёт это значение и разделит на него ширину каждого из возможного источника, которые мы указали в srcset. Самый маленький источник имеет собственный размер 600 пикселей, поэтому: 600÷800=0.75. Наше среднее изображение имеет ширину 1200 пикселей: 1200÷800=1.5. Самое большое изображение имеет ширину 2000 пикселей: 2000÷800=2.5.

Результаты этих расчетов (0.75, 1.5 и 2.5) — это, по сути, варианты DPR, специально адаптированные к размеру области просмотра пользователя. Поскольку браузер также располагает информацией о плотности изображения дисплея пользователя, он принимает ряд решений.

При таком размере области просмотра кандидат small.jpg отбрасывается независимо от плотности отображения пользователя — при расчётном DPR меньше 1 этот источник потребует увеличения для любого пользователя, поэтому он не подходит. На устройстве с DPR, равным 1, наиболее подходящим является medium.jpg — этот источник подходит для отображения при DPR равном 1.5, поэтому он немного больше, чем нужно, но помните, что уменьшение масштаба — это визуально плавный процесс. На устройстве с DPR равном 2 файл large.jpg является наиболее близким по размеру, поэтому будет выбран он.

Если то же самое изображение будет отображаться на экране просмотра шириной 600 пикселей, результат всей этой математики будет совершенно другим: 80vw теперь равно 480px. Разделив ширину наших источников на это значение, мы получим 1.25, 2.5 и 4.1666666667. При таком размере области просмотра файл small.jpg будет выбран на устройствах 1х, а medium.jpg — на устройствах 2х.

Это изображение будет выглядеть одинаково во всех контекстах просмотра: все наши исходные файлы абсолютно одинаковы, за исключением их размеров, и каждый из них отображается настолько чётко, насколько позволяет плотность дисплея пользователя. Однако вместо того, чтобы предлагать каждому пользователю файл large.jpg, чтобы удовлетворить потребности широких областей просмотра и дисплеев с высокой плотностью, пользователям всегда будет предлагаться подходящий кандидат наименьшего размера. Используя описательный, а не предписывающий синтаксис, вам не нужно вручную устанавливать точки останова и учитывать области просмотра и DPR — вы просто даёте браузеру информацию и позволяете ему решать за вас.

Поскольку значение sizes привязано к области просмотра и совершенно не зависит от макета страницы, это добавляет дополнительный уровень сложности. Редко когда изображение занимает лишь часть области просмотра, без каких-либо фиксированных по ширине полей, отступов или влияния других элементов на странице. Часто требуется выразить ширину изображения, используя комбинацию единиц измерения: проценты, em, px и др.

К счастью, здесь можно использовать calc() — любой браузер с поддержкой адаптивных изображений также поддерживает calc(), что позволяет нам смешивать и сочетать единицы CSS — например, изображение, занимающее всю ширину области просмотра пользователя, за вычетом margin в 1em с каждой стороны:

<img
 sizes="calc(100vw-2em)"
 srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
 src="fallback.jpg"
 alt="...">

Описание точек останова

Если вы проводили много времени, работая с адаптивными макетами, то наверняка заметили, что в этих примерах чего-то не хватает: пространство, занимаемое изображением в макете, скорее всего, будет меняться в зависимости от точек останова макета. В этом случае необходимо передать браузеру несколько больше деталей: sizes принимает набор условий для размера изображения, разделённых запятыми, точно так же, как srcset принимает разделённые запятыми условия для источников изображения. Эти условия используют знакомый синтаксис медиа-запросов. Данный синтаксис работает до первого соответствия: как только медиа-условие совпадает, браузер прекращает анализ атрибута sizes и применяет указанное значение.

Допустим, у вас есть изображение, которое должно занимать 80%, за вычетом padding в 1em с каждой стороны, на областях просмотра более 1200px, а на меньших экранах оно должно занимать всю ширину области просмотра.

<img
 sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">
<style> body { margin: 0; } img { max-width: 100%; } div { margin: 0 auto; width: 100%; } @media( min-width: 1200px ) { div { width: 80%; padding: 1em; } } </style> <div> <img sizes="(min-width: 1200px) calc(80vw - 2em), 100vw" srcset="image/pontederia-crassipes_600.jpg 600w, image/pontederia-crassipes_1200.jpg 1200w, image/pontederia-crassipes_2000.jpg 2000w" src="https://assets.codepen.io/11355/pontederia-crassipes_2000.jpg" alt="..."> </div>

Если область просмотра пользователя больше 1200px, calc(80vw - 2em) описывает ширину изображения в нашем макете. Если условие (min-width: 1200px) не совпадает, браузер переходит к следующему значению. Поскольку к этому значению не привязано конкретное медиа-условие, по умолчанию используется 100vw. Если написать атрибут sizes с использованием медиа-запросов max-width:

<img
 sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">
<style> body { margin: 0; } img { max-width: 100%; } div { margin: 0 auto; width: 80%; padding: 1em; } @media( max-width: 1200px ) { div { width: 100%; padding: 0; } } </style> <div> <img sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)" srcset="image/pontederia-crassipes_600.jpg 600w, image/pontederia-crassipes_1200.jpg 1200w, image/pontederia-crassipes_2000.jpg 2000w" src="https://assets.codepen.io/11355/pontederia-crassipes_2000.jpg" alt="..."> </div>

Говоря простым языком: «выполняется ли условие (max-width: 1200px)»? Если нет, переходим дальше. Следующее значение calc(80vw - 2em) не имеет никаких условий, поэтому выбирается именно оно.

Теперь, когда вы предоставили браузеру всю эту информацию о вашем элементе <img> — потенциальные источники, присущую ему ширину и то, как вы собираетесь представить изображение пользователю, — браузер использует нечёткий набор правил для определения того, что делать с этой информацией. Если это звучит расплывчато, так оно по замыслу и есть. Алгоритм выбора источника, заложенный в спецификации HTML, очень размыто определяет, как следует выбирать источник. После анализа источников, их описаний и того, как будет отображаться изображение, браузер волен делать всё, что ему заблагорассудится — вы не можете знать наверняка, какой источник выберет браузер.

Синтаксис, гласящий «используйте этот источник на мониторе с высоким разрешением», был бы предсказуем, но не решал бы основную проблему с изображениями в адаптивной вёрстке: экономию пропускной способности канала. Плотность пикселей экрана имеет лишь косвенное отношение к скорости интернет-соединения, если вообще имеет. Если вы пользуетесь первоклассным ноутбуком, но выходите в Интернет дозированно, с телефона или через зыбкий Wi-Fi в самолёте, вам лучше отказаться от источников изображений высокого разрешения, независимо от качества дисплея.

Оставляя последнее слово за браузером, можно добиться гораздо большего повышения производительности, чем при использовании строго предписанного синтаксиса. Например: в большинстве браузеров <img>, использующее синтаксис srcset или sizes, никогда не будет запрашивать источник с меньшими размерами, чем тот, который уже есть в кэше браузера. Какой смысл делать новый запрос к источнику, который будет выглядеть идентично, когда браузер может без проблем уменьшить размер уже имеющегося у него изображения? Но если пользователь увеличит область просмотра до такой степени, что потребуется новое изображение, чтобы избежать увеличения, этот запрос всё равно будет сделан, так что всё будет выглядеть так, как вы ожидаете.

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

Использование sizes и srcset

Получается очень много информации — как для вас, читателя, так и для браузера. У атрибутов srcset и sizes насыщенный синтаксис, описывающий шокирующее обилие информации в относительно небольшом количестве символов. Это, к счастью или к несчастью, так и задумано: если сделать эти синтаксисы менее лаконичными и более удобными для разбора человеком, тогда уже браузеру будет сложнее их разобрать. Чем больше сложностей добавляется в строку, тем больше вероятность ошибок или непреднамеренных различий в поведении браузера. Однако в этом есть и положительная сторона: синтаксис, который легче читается машинами, легче ими и пишется.

Атрибут srcset — это наглядный пример автоматизации. Вряд ли вы будете вручную создавать несколько версий изображений для рабочего сайта, вместо этого вы автоматизируете данный процесс с помощью менеджера задач вроде Gulp, сборщика вроде Webpack, сторонней CDN вроде Cloudinary или функциональности, уже заложенной в выбранную вами CMS. Имея достаточную информацию для генерации наших исходников, система обладает достаточной информацией, чтобы записать её в эффективный атрибут srcset.

Автоматизация атрибута sizes несколько сложнее. Как вы знаете, единственный способ, которым система может вычислить размер изображения в отрисованном макете, — это отрисовать сам макет. К счастью, появилось множество инструментов для разработчиков, позволяющих абстрагироваться от процесса ручного написания атрибутов sizes, причём с эффективностью, которую никогда не сравнить с ручным трудом. respImageLint, например, представляет собой фрагмент кода, предназначенный для проверки точности атрибутов sizes и рекомендаций по их улучшению. Проект Lazysizes жертвует скоростью ради эффективности, откладывая запросы изображений до момента создания макета, позволяя JavaScript генерировать значения размеров за вас. Если вы используете фреймворк для отображения на стороне клиента, например React или Vue, существует ряд решений для создания или генерации атрибутов srcset и sizes, которые мы рассмотрим в следующем разделе.

Автор: Мэт Маркиз
Последнее изменение: 20.05.2024