Frontender Magazine

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

Элемент <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.6 1 GET 1 GET 1 GET 2 GET 1 GET
Android 2.3 1 GET 1 GET 1 GET 2 GET 1 GET
Android 4.2 1 GET 1 GET 1 GET 2 GET 1 GET
Chrome 25 1 GET 1 GET 1 GET 2 GET 1 GET
Chromium 25 (RICG) 1 GET 1 GET 1 GET 2 GET 2 GET
Firefox 19 1 GET 1 GET 2 GET 2 GET 1 GET
IE 6 1 GET нет 1 GET 1 GET 1 GET
IE 7 1 GET нет 1 GET 1 GET 1 GET
IE 8 1 GET нет 1 GET 1 GET 1 GET
IE 9 1 HEAD, 1 GET 1 GET 1 HEAD, 1 GET 1 HEAD, 2 GET 1 GET
IE 10 1 HEAD, 1 GET 1 GET 1 HEAD, 1 GET 1 HEAD, 2 GET 1 GET
Opera 12.1 1 GET 1 GET 1 GET 2 GET 1 GET
Opera Mobile 12.1 1 GET 1 GET 1 GET 2 GET 1 GET
Safari 6 1 GET 1 GET 1 GET 2 GET 1 GET
Safari iOS 6 (iPad) 1 GET 1 GET 1 GET 2 GET 1 GET
Safari iOS 6 (iPhone) 1 GET 1 GET 1 GET 2 GET 1 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, за отзывы об этой статье.

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Dave Newton
Автор:
Dave Newton
Twitter:
@newtron
Сaйт:
http://davidnewton.ca/
Email:
david@davidnewton.ca
Наталья Фадеева
Переводчик:
Наталья Фадеева
вКонтакте:
natatik_l
Twitter:
@very_busy_girl
GitHub:
NatalieF

Комментарии (1 комментарий, если быть точным)

Автар пользователя
evgenyrodionov

HTML5 — это чудесно.