Как избежать лишних загрузок для отзывчивых изображений

Элемент <picture> — это одно из нововведений HTML5, разработанное общественной группой W3C по адаптивным изображениям (Responsive Images Community Group — RICG). Оно должно стать семантичным, основанным на разметке, инструментом для реализации отзывчивых изображений без использования JavaScript или сложных проверок на стороне сервера.

Элемент <picture> поддерживает возможность добавления различных типов резервного контента, но пока в практической реализации этой поддержки есть ряд проблем. В этой статье мы разберемся как работает резервный контент, в каких случаях он перестает работать и как это можно исправить.

Элемент <picture> и резервное содержимое

Как и для <video> и <audio>, в <picture> могут быть помещены элементы <source> для указания набора изображений, из которых браузер может выбрать подходящее. Элементы <source> могут при необходимости принимать атрибуты type и media, которые предоставляют браузеру сведения о типе файла и медиатипе подключенного контента соответственно. На основе информации, помещенной в атрибуты, браузер должен загружать первый <source> с типом файла, который поддерживается, и с подходящим медиазапросом. Например:

<picture>
    <source src="landscape.webp" type="image/webp" media="screen and (min-width: 20em) and (orientation: landscape)" />
    <source src="landscape.jpg" type="image/jpg" media="screen and (min-width: 20em) and (orientation: landscape)" />
    <source src="portrait.webp" type="image/webp" media="screen and (max-width: 20em) and (orientation: portrait)" />
    <source src="portrait.jpg" type="image/jpg" media="screen and (max-width: 20em) and (orientation: portrait)" />
</picture>

Для подстраховки от ситуаций когда браузер не поддерживает <picture> (или <video>, или <audio>) или не может отобразить содержимое ни одного из элементов <source>, разработчик может добавить резервный контент. Этим резервным контентом обычно является изображение или текстовое описание; если резервный контент помещён в тег <img>, обычно добавляют дополнительный резервный контент в атрибуте alt (или longdesc).

<picture>
    <source type="image/webp" src="image.webp" />
    <source type="image/vnd.ms-photo" src="image.jxr" />
    <img src="fallback.jpg" alt="модные штаны">
</picture>

Элемент <picture> отличается от <video> и <audio> тем, что может принимать атрибут srcset. С его помощью разработчик может указать разные изображения для устройств с разной пиксельной плотностью. При создании отзывчивого изображения используя <picture> и srcset, мы получим что-то вроде следующего:

<picture>
    <source srcset="big.jpg 1x, big-2x.jpg 2x, big-3x.jpg 3x" type="image/jpeg" media="(min-width: 40em)" />
    <source srcset="med.jpg 1x, med-2x.jpg 2x, med-3x.jpg 3x" type="image/jpeg" />
    <img src="fallback.jpg" alt="модные штаны" />
</picture>

Главная идея такого кода с использованием <picture> состоит в том, чтобы загружалось только одно изображение в зависимости от контекста пользователя:

Для пользователя преимущество в том, что скачивается только один файл с изображением, вне зависимости от возможностей браузера, параметров экрана устройства или его пиксельной плотности.

Элемент <picture> также позволяет использовать не только изображения в качестве резервного контента, что положительно влияет на доступность: можно добавить <p>, <span>, <table> или любой другой элемент в качестве резервного контента на тот случай если ни одно из изображений не может быть отображено или если пользователю нужно описание изображения. Такой резервный контент более надёжен и логичен чем простое описание в атрибуте alt.

Проблема резервного содержимого

На данный момент ни один браузер не поддерживает элемент <picture>. Разработчики, которые хотя использовать <picture>, могут воспользовать полифилом Picturefill, который предложил Скотт Джэлл (Scott Jehl). Кроме того, Йоав Вайс (Yoav Weiss) разработал прототип базовой реализации на основе Chromium, которая частично поддерживает <picture>. Его сборка Chromium не только доказывает что поддержка <picture> возможна в техническом плане, а также даёт нам возможность сравнить нашими ожиданиями с реальными функциональностью и поведением этого элемента.

Тестируя код, похожий на приведенный в качестве примера выше, в своей сборке Chromium, Йоав обнаружил проблему: хотя <picture> поддерживается и один из двух первых элементов <source> загружается, резервное изображение <img> также загружается. Скачиваются два изображения, хотя используется только одно из них.

Скриншот1

Крупный план.

Это происходит потому что браузеры «забегают наперёд» когда грузится HTML и немедленно начинают загрузку изображений. Вот как Йоав это объясняет:

«Когда парсер обнаруживает тэг img, он создаёт узел HTMLImageElement и присваивает ему его атрибуты. Когда атрибуты добавлены, узел не обращает внимание на родительские элементы и после добавления атрибута src немедленно запускается скачивание изображений»

Такой поспешный парсинг очень удобен в большинстве случаев, так как браузер может начать загрузку изображений даже до окончания загрузки разметки HTML. Но в ситуациях когда элемент img является дочерним элементом <picture> (или <video>, или <audio>), браузер не обращает внимание на родительский элемент: он видит img и начинает загрузку. Проблема также возникает если мы забудем о родительском элементе и рассмотрим <img> с атрибутами src и srcset: парсер скачивает изображения src до того как выберет подходящее для отображения из srcset.

<picture>
    <source srcset="big.jpg 1x, big-2x.jpg 2x, big-3x.jpg 3x" media="(min-width: 40em)" />
    <source srcset="med.jpg 1x, med-2x.jpg 2x, med-3x.jpg 3x" />
    <img src="fallback.jpg" alt="модные штаны" />
    <!-- резервное изображение fallback.jpg скачивается *всегда* -->
</picture>

<img src="fallback.jpg" srcset="med.jpg 1x, med-2x.jpg 2x, med-3x.jpg 3x" alt="модные штаны" />
<!-- резервное изображение fallback.jpg скачивается *всегда* -->

<video>
    <source src="video.mp4" type="video/mp4" />
    <source src="video.webm" type="video/webm" />
    <source src="video.ogv" type="video/ogg" />
    <img src="fallback.jpg" alt="модные штаны" />
    <!-- резервное изображение fallback.jpg скачивается *всегда* -->
</video>

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

Возможное решение

Для этой проблемы нужно найти краткосрочное и долгосрочное решение.

С точки зрения долгосрочной перспективы, мы должны убедиться что при реализации <picture> (а также <video> и <audio>) в браузерах этот глюк будет устранён. Например, Робин Бэрджон (Robin Berjon) предложил сделать содержимое <picture> пассивным, как содержимое <template>, и использовать Shadow DOM (о нём можно почитать, например, в статье «Template, новый тэг HTML5: стандартизация шаблонизации на стороне клиента»). Йоав предложил использовать для <img> атрибут, который прикажет браузеру подождать перед загрузкой содержимого из src.

Хотя технически изменение работы парсера возможно, это усложнит реализацию. Изменения парсера могут повлиять на код и библиотеки JavaScript, построенные на предположении что загрузка начинается как только к <img> добавляется атрибут src. Эти долгосрочные изменения требуют сотрудничества разработчиков браузеров, создателей библиотек JavaScript и веб-разработчиков.

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

Так что же нам поможет найти кратковременное решение? Наши старые друзья <object> и <embed>, которые могут быть использованы для добавления изображений. Изображение, добавленное с помощью этих тэгов, будет должным образом отображаться в ситуации когда нужно активировать резервный контент и не будет загружаться пока такая ситуация не наступила.

Браузеры ведут себя по-разному в зависимости от того какой тэг используется: <object>, <embed>или оба. Чтобы определить какое решение является лучшим, я провел тестирование (используя немного измененную версию этого кода) в:

Я провёл пять тестов:

  1. для <picture> в качестве резервного контента используется <object>.
  2. Для <picture> в качестве резервного контента используется <embed>.
  3. Для <picture> в качестве резервного контента используется <object>, и для него в свою очередь <embed>.
  4. Для <picture> в качестве резервного контента используется <object>, и для него в свою очередь <img>.
  5. Для <picture> в качестве резервного контента используется <img>.

Результаты оказались следующими:

Что видит пользователь
Тест 1Тест 2Тест 3Тест 4Тест 5
Android 1.6резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Android 2.3резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Android 4.2резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Chrome 25резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Chromium 25 (RICG)исходное изображениеисходное изображениеисходное изображениеисходное изображениеисходное изображение
Firefox 19резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
IE 6изображение отсутствуетизображение отсутствуетизображение отсутствуетизображение отсутствуетрезервное изображение
IE 7изображение отсутствуетизображение отсутствуетизображение отсутствуетизображение отсутствуетрезервное изображение
IE 8резервное изображениеизображение отсутствуетрезервное изображениерезервное изображениерезервное изображение
IE 9резервное изображениерезервное изображение (обрезанное, с полосками прокрутки)резервное изображениерезервное изображениерезервное изображение
IE 10резервное изображениерезервное изображение (обрезанное, с полосками прокрутки)резервное изображениерезервное изображениерезервное изображение
Opera 12.1резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Opera Mobile 12.1резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Safari 6резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Safari iOS 6 (iPad)резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
Safari iOS 6 (iPhone)резервное изображениерезервное изображениерезервное изображениерезервное изображениерезервное изображение
HTTP-запросы
Тест 1Тест 2Тест 3Тест 4Тест 5
Android 1.61 GET1 GET1 GET2 GET1 GET
Android 2.31 GET1 GET1 GET2 GET1 GET
Android 4.21 GET1 GET1 GET2 GET1 GET
Chrome 251 GET1 GET1 GET2 GET1 GET
Chromium 25 (RICG)1 GET1 GET1 GET2 GET2 GET
Firefox 191 GET1 GET2 GET2 GET1 GET
IE 61 GETнет1 GET1 GET1 GET
IE 71 GETнет1 GET1 GET1 GET
IE 81 GETнет1 GET1 GET1 GET
IE 91 HEAD, 1 GET1 GET1 HEAD, 1 GET1 HEAD, 2 GET1 GET
IE 101 HEAD, 1 GET1 GET1 HEAD, 1 GET1 HEAD, 2 GET1 GET
Opera 12.11 GET1 GET1 GET2 GET1 GET
Opera Mobile 12.11 GET1 GET1 GET2 GET1 GET
Safari 61 GET1 GET1 GET2 GET1 GET
Safari iOS 6 (iPad)1 GET1 GET1 GET2 GET1 GET
Safari iOS 6 (iPhone)1 GET1 GET1 GET2 GET1 GET
Контекстное меню для изображения
Тест 1Тест 2Тест 3Тест 4Тест 5
Android 1.6естьестьестьестьесть
Android 2.3естьестьестьестьесть
Android 4.2естьестьестьестьесть
Chrome 25нетнетнетнетесть
Chromium 25 (RICG)нетнетнетнетнет
Firefox 19естьестьестьестьесть
IE 6нетнетнетнетесть
IE 7нетнетнетнетесть
IE 8естьнетестьестьесть
IE 9естьестьестьестьесть
IE 10естьестьестьестьесть
Opera 12.1естьестьестьестьесть
Opera Mobile 12.1естьнетестьестьесть
Safari 6нетнетнетнетесть
Safari iOS 6 (iPad)нетнетнетнетесть
Safari iOS 6 (iPhone)нетнетнетнетесть

Обеспечиваем доступность контента

Хотя насчёт самого лучшего способа добавления резервного контента для <picture> мнения пока расходятся (также взгляните на это обсуждение), мне захотелось проверить как программа VoiceOver от Apple будет работать с разными элементами. В процессе этого эксперимента я проверил как VoiceOver понимает атрибут alt в разных контекстах и резервные элементы <span>. К сожалению у меня не было возможности провести проверку других скринридеров и вспомогательных технологий, буду рад если вы поделитесь своим опытом.

Может быть прочитано VoiceOver:
`alt` для `picture``alt` для `source` (`picture → source`)`alt` для `object` (`picture → object`)`alt` для `embed` (`picture → embed`)`alt` для `embed` (`picture → object → embed`)
Chrome 25нетнетдаданет
Chromium 25 (RICG)данетнетнетнет
Firefox 19нетнетдаданет
Opera 12.1нетнетнетнетнет
Safari 6нетнетдаданет
Safari iOS 6 (iPad)нетнетдаданет
Safari iOS 6 (iPhone)нетнетдаданет
Может быть прочитано VoiceOver:
`alt` для `img` (`picture → object → img`)`alt` для `img` (`picture → img`)`span` (`picture → span`)`span` (`picture → object → span`)
Chrome 25нетдаданет
Chromium 25 (RICG)нетнетнетнет
Firefox 19нетдаданет
Opera 12.1нетнетданет
Safari 6нетдаданет
Safari iOS 6 (iPad)нетдаданет
Safari iOS 6 (iPhone)нетдаданет

Ошибкоустойчивый синтаксис

Опираясь на эти данные я пришёл к следующему ошибкоустойчивому решению:

<picture alt="модные штаны">
    <!-- загружается в браузерах, которые поддерживают тэг picture и один из элементов source -->
    <source srcset="big.jpg 1x, big-2x.jpg 2x, big-3x.jpg" type="image/jpeg" media="(min-width: 40em)" />
    <source srcset="med.jpg 1x, med-2x.jpg 2x, big-3x.jpg" type="image/jpeg" />

    <!-- загружается в браузерах IE 8+, браузерах других производителей, которые не поддерживают picture, а также браузерах, которые поддерживают picture, но не могут принять ни один из элементов source -->
    <![if gte IE 8]>
    <object data="fallback.jpg" type="image/jpeg"></object>
    <span class="fake-alt">модные штаны</span>
    <![endif]>

    <!-- загружается в IE 6 и 7 -->
    <!--[if lt IE 8]>
    <img src="fallback.jpg" alt="модные штаны" />
    <![endif]-->
</picture>

.fake-alt {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
}

У нас есть элемент <picture>, два элемента source на выбор для браузеров, поддерживающих <picture>, резервный контент в <object> и <span> для большинства браузеров (смотрите примечание сразу под этим абзацом) и отдельное резервное изображение <img> для IE 7 и старше. Пустой alt не даёт скринридерам озвучить информацию о настоящем изображении, <span> спрятан с помощью CSS (используемый класс идентичен .visuallyhidden, который применяется в HTML5 Boilerplate), но может быть прочитан скринридерами. В элементе <embed> нет необходимости.

( Примечание: Мы вынуждены использовать <span> как бутафорный alt чтобы VoiceOver мог прочитать текст в браузере Opera. Хотя процент пользователей Opera относительно небольшой и она находится в процессе перехода на движок WebKit, я всё же считаю что её стоит принимать во внимание. Однако, если для вас не важна поддержка этого конкретного браузера, вы можете избавиться от <span> и вместо него добавить alt для <object>(хотя это и не приветствуется спецификацией). Это в случае если в <span> и alt помещён один и тот же контент. Если у вас более сложный резервный элемент, например <table>, вероятно предпочтительней использовать и <span>, и alt с текстовым описанием)

Похожее решение должно работать и для <audio>, хотя элементы <img> довольно редко используются для него в качестве резервного контента. При работе с <video>, проблема легко решается если резервное изображение совпадает с превью-картинкой. Если они могут быть одинаковыми, надёжный синтаксис для <video> будет таким:

<video poster="fallback.jpg">
    <!-- загружается в браузерах, которые поддерживают тэг video и один из элементов source -->
    <source src="video.mp4" type="video/mp4" />
    <source src="video.webm" type="video/webm" />
    <source src="video.ogv" type="video/ogg" />

    <!-- загружается в браузерах, которые не поддерживают video, и браузерах, которые поддерживают video, но не могут принять ни один из элементов source -->
    <img src="fallback.jpg" alt="fancy pants" />
</video>

Однако если для вашего <video> резервное изображение и превью-картинка должны быть разными, возможно вам стоит использовать такой же код как приведён для <picture> выше.

Обратите внимание что <video> и <audio> не принимают атрибут alt, даже если вы его добавите, VoiceOver его проигнорирует. Если вас интересует оптимизация доступности видео, вам будет интересно ознакомиться с работой которая ведется для формата Web Video Text Tracks (WebVTT).

К сожалению, подробное тестирование работы элементов <video> и <audio> выходит за рамки этой статьи, если у вас есть интересная информация на эту тему, делитесь ею в комментариях.

Насколько хорошим (или плохим) является это решение?

Давайте сначала рассмотрим его недостатки. Это решение является хаком. Его определённо нельзя рассматривать как настоящее, долгосрочное решение. Оно безумно объемное; никто будучи в здравом уме не захочет писать столько кода только чтобы просто добавить изображение на страницу.

Кроме того, с точки зрения семантики для добавления изображения нам следует использовать элемент <img>, а не <object>. Именно для этого был придуман <img>.

Также есть несколько практических моментов:

Принимая всё это во внимание, такое решение можно считать неплохим в краткосрочной перспективе. Оно даёт нам следующие преимущества:

Скриншот2

Крупный план.

Семантика этого решения хоть и не идеальна, но и не ужасна: спецификация HTML5 говорит что элемент <object> «может представлять внешний ресурс, который, в зависимости от его типа, будет рассматриваться как изображение, встроенный контекст просмотра или внешний ресурс, который должен быть обработан с помощью плагина» (выделение в текст добавил я).

И хотя <span> не так же хорош как настоящий атрибут alt, использование визуально невидимого элемента в целях повышения доступности является довольно распространённой практикой. Вспомните о ссылках «перейти к содержимому», спрятанных для глаза, но видимых для скринридеров.

Следующие шаги

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

Решить этот вопрос можно только посредством обсуждения и активного участия разработчиков браузеров и веб-разработчиков. Поддержка со стороны создателей браузеров крайне важна; ведь можно написать спецификацию для чего-бы то ни было, но она останется только на бумаге пока не будет реализована в браузерах. Поддержка со стороны веб-разработчиков также важна, они могут помочь убедиться что то или иное решение достаточно хорошо продумано чтобы использоваться на практике. Именно такой подход, основанный на всеобщем соглашении, недавно был использован при добавлении элемента <main> в спецификацию; Стив Фолкнер (Steve Faulkner) описывает этот процесс в замечательном интервью с доктором HTML5.

Если вы хотите помочь найти решение для этой проблемы, присоединяйтесь к её обсуждению:

Следующим шагом навстречу долгосрочному решению должно стать достижения консенсуса между веб-разработчиками и создателями браузеров. Не упустите возможность в этом поучаствовать.

Хочу поблагодарить Йоава Вайса, Маркоса Касереса (Marcos Cáceres) и Мэта Маркуиса (Mat Marquis), членов RICG, за отзывы об этой статье.