К списку

Отзывчивые изображения

13 декабря 2019

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

Ситуация эта не новая и существует много вопросов, которые возникают при желании работать с этим. Все техники, как правило, называются одинаково. Это что-то отзывчивое. Например, отзывчивая верстка или отзывчивый дизайн, или всё то же самое, только адаптивное вместо отзывчивого. Один из вопросов всего этого набора решений и технологий — это отзывчивые изображения.

Проблема изображений на сайте — одна из первых, на которую стоит обратить внимание, когда вы задумываетесь о пользователях с мобильными устройствами. Исторически, очевидно, самое важное в изображениях — это их вес. Понятно, что не стоит заставлять пользователя, читающего ленту новостей в телефоне в метро загружать картинки размером 1920×1080, но очень хочется всё же какие-то картинки ему показать. Однако, есть и еще один важный момент: сегодня, как правило, дизайнер во время своей работы рассчитывает на возможность адаптации десктопного сайта, а хороший дизайнер, даже специально делает макеты для планшетов и телефонов. В таких случаях, бывает, что надо решать уже не проблему веса картинки, а даже иметь совершенно разные пропорции для различных устройств. В любом случае, решение проблемы размера изображения или проблемы фантазии дизайнера сводится к тому, что мы должны уметь показывать пользователю разные файлы в зависимости от размера окна его браузера. Я попробую рассказать обо всех техниках верстки, которые существуют для этого, примерно, в историческом порядке их появления.

Только, для начала, давайте определимся с задачей, на примере которой будем пробовать всё это сделать.

Пускай на странице есть статьи, которые состоят из картинки и текста. И предположим, что дизайнер нарисовал нам два макета этой страницы для десктопной и мобильной версии сайта. В каждом из них статьи одинаковые, но выглядят по-разному. Чтобы было веселее, представим, что одна из статей посвящена тренинг-центру. Ну, и, чтобы было понятно, чего нужно добиться, вот два варианта одной статьи:

Как видим, у статьи как-будто две разные картинки используются. На самом деле, дизайнер объяснил нам, что картинка к статье может быть любой, но для десктопной версии, нам нужно будет ее как-то обрезать, чтобы получился квадрат.

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

Надо еще определиться, какие экраны мы будем считать достаточно маленкими, чтобы считать их мобильными. Пусть это будет случайная цифра — 840 пикселей.

Очень хотелось бы, также, чтобы HTML код для этой статьи был примерно таким:

<article>
  <img src="image.jpg">
  <div>
    Lorem ipsum ...
  </div>
</article>

Осталось только суметь показать нужную картинку в нужный момент. Попробуем воплотить это в жизнь. И постараемся сделать это без использования javascript.

1. Самый простой способ

Самым простым и древним способом решения этого вопроса является наличие мобильной версии сайта. Прямо отдельной версии, с другим адресом, и ссылкой на главной странице на эту версию. В таком случае нам остается только сверстать две разные страницы — и дело в шляпе. Но это не очень удобно пользователю. Чтобы попасть на мобильную версию, нужно, либо помнить её адрес, либо все равно открывать основной сайт сначала, и там искать ссылку.

2. Медиазапросы.

2.1 Очевидный способ (смена видимой картинки)

Как всем нам известно, достаточно давно в CSS появилась возможность описывать верстку в зависимости от ширины окна браузера — медиазапросы. Это очень полезная технология, и можно попробовать её в нашем случае. Для начала сверстаем десктопную версию:

HTML:

<article class="article">
  <img class="article-img" src="img/square.jpg" alt="ISsoft Trainig center">
  <div>
    <p>
      Молодые специалисты с небольшим опытом и даже без него могут попасть в ISsoft, после успешного прохождения корпоративного обучения. Курс займет полтора – три месяца в зависимости от выбранной специальности. 
    </p>
    <p>  
      Мы обучаем разработчиков Java, .NET и C++, frontend и mobile, специалистов BI и DevOps, QA automation и manual, бизнес-аналитиков и мастеров UX
    </p>
    <p>
      Курсы ISsoft – не конкурс, где на работу берут пятерых из группы слушателей в сорок человек. Успешное окончание обучения – это 100% гарантия трудоустройства в ISsoft. 
    </p>
    <p>
      Более того, наши курсы не только бесплатные, но компания оплачивает слушателям время, затраченное на обучение в нашем тренинг-центре.
    </p>
  </div>
</article>

CSS:

.article {
  width: 800px;
  margin: 40px auto;
  padding: 10px;
  display: flex;
  border-radius: 5px;
  box-shadow: 1px 1px 10px rgba(0,0,0,.5);
}

.article-img {
  width: 100%;
  height: auto;
}

Теперь настало время медиазапроса. В нашем случае, для всех экранов меньше 840 пикселей, нам больше не нужен флекс для статьи и задавать ей фиксированную ширину тоже не нужно:

@media only screen and (max-width: 840px) {
  .article {
    display: block;
    width: auto;
    padding: 50px;
  }
}

Почти получилось. Это пример того, как писать медиазапрос. Но проблема показа различных картинок все же осталась. Можно попробовать добавить в HTML вторую картинку тоже, и с помощью медиазапросов управлять тем, какая из них видна. Для этого придется добавить картинкам свои классы.

<article class="article">
  <img class="img-desktop" src="img/square.jpg" alt="ISsoft Trainig center">
  <img class="img-mobile" src="img/landscape.jpg" alt="ISsoft Trainig center">
  <div>
    ....
  </div>
</article> 

А в CSS определим какой класс когда будет виден:

.article {
  width: 800px;
  margin: 40px auto;
  padding: 10px;
  display: flex;
  border-radius: 5px;
  box-shadow: 1px 1px 10px rgba(0,0,0,.5);
}

.article-mobile {
  width: auto;
  padding: 50px;
}

.img-desktop {
  display: block;
  height: 100%;
  margin-right: 20px;
}
.img-mobile {
  display: none;
  width: 100%;
  height: auto;
  margin-bottom: 20px;
}

@media only screen and (max-width: 840px) {
  .article {
    display: block;
    width: auto;
    padding: 50px;
  }

  .img-desktop {
    display: none;
  }
  .img-mobile {
    display: block;
  }
}

Простое элегантное решение. Оно великолепно решает задачу, поставленную перед нами дизайнером. Но, если вы попробуете повторить у себя этот код, то включите вкладку Network в консоли браузера и посмотрите, какие картинки и при каких размерах окна он загружает. У меня так:

Визуальная часть — это хорошо, но мы видим, что браузер в любом случае загружает обе картинки. Это не удивительно, ведь браузер применяет css правила чуть позже, чем справляется с разбором загруженного в него HTML. Получается, что браузер видит две картинки, загружает обе, а уже потом скрывает нужную. Мы жертвуем трафиком пользователя, для того, чтобы ему было удобно и красиво пользоваться сайтом с телефона. Это совсем нелогично и совсем неудобно (браузер загружает обе картинки) и, как мне кажется, это неуважение к пользователю. Можно ли как-то справиться с этим с помощью медиазапросов?

2.2 Неочевидный способ (вся магия только в CSS)

Медиазапросы могут управлять любыми CSS-свойствами. Воспользуемся этим. Есть же возможность отобразить картинку на странице не с помощью тэка img, а как background-image какого-то элемента. Поскольку background-image — это всего лишь свойство, то его легко менять с помощью медиазапросов и такой вариант выглядит отличным выходом из сложившейся ситуации. Правда, придется немного поменять верстку. Теперь нам не нужен элемент img, можно попробовать поставить вместо него, например, div, фоном которого мы и будем управлять. HTML изменится совсем немного:

<article class="article">
  <div class="article-img"></div>  
  <div>
    ....
  </div>
</article> 

Ну и как и в предыдущий раз добавим стили для десктопной версии:

.article {
  width: 800px;
  margin: 40px auto;
  padding: 10px;
  display: flex;
  border-radius: 5px;
  box-shadow: 1px 1px 10px rgba(0,0,0,.5);
  
}

.article-img {
  min-width: 300px;
  height: 300px;
  background: url('img/square.jpg');
  margin-right: 20px;
}

Почти ничего не изменилось). Однако, теперь нужно задавать четкие размеры элементу, который отвечает за отображения картинки. Тэг img автоматически мог подстроить высоту самого себя в зависимости от ширины. Теперь такой возможности нет. Но, если нам нужно всегда показывать картинку фиксированного размера, то это и не проблема. Сейчас попробуем подключить медиазапрос, который сменит фон нашего элемента-картинки:

@media only screen and (max-width: 840px) {
  .article {
    display: block;
    width: auto;
    padding: 50px;
  }
  .article-img {
    background: url('img/landscape.jpg');
    margin-right: 0;
    margin-bottom: 20px;
  }
}

Очевидно, что этого недостаточно. Наша картинка всё ещё имеет размеры 300Х300 пикселей. Попробуем решить эту проблему. Для начала ширина. Ширину можно установить в 100% ширины родителя и сбросить правило про минимальную ширину. Вот так будет выглядеть правило в медиазапросе теперь:

.article-img {
  background: url('img/landscape.jpg');
  width: 100%;
  min-width: 0;
  margin-right: 20px;
}

Можно заметить, что когда сам элемент с классом article-img имеет размер меньший, чем картинка, то картинка обрезается. Но и это можно побороть. Скажем с помощью CSS, чтобы фон элемента всегда располагался в его центре, не повторялся и менял свой масштаб так, чтобы всегда полностью быть видимым:

.article-img {
  background: url('img/landscape.jpg');
  width: 100%;
  min-width: 0;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  margin-right: 0;
  margin-bottom: 20px;
}

Работает! Причем, действительно, при загрузке страницы, браузер скачивает именно ту картинку, которая нужна. Но, можно заметить, что на совсем маленьких экранах это выглядит плохо. Мы никак не можем менять высоту элемента относительно ширины. Она все ещё 300 пикселей. Поэтому, когда ширина нашего элемента становится меньше 300 пикселей, картинка становится совсем маленькой, чтобы влезть по ширине, и остаются большие ничем не занятые области. Это явно не то, что дизайнер нарисовал. Правда, и с этим можно справиться. Используем небольшой хак. Известно, что, если в CSS задавать вертикальные отступы в процентах (не важно margin это или padding), то размеры этих отступов рассчитываются от высоты. Например, если указать padding-top: 50%, это будет означать, что вертикальный отступ элемента — половина его ширины. Воспользуемся этим знанием и тем фактом, что картинка, над которой мы мучаемся имеет соотношение сторон 650 X 431. Это значит, что высота картинки составляет примерно 66 процента от ее ширины. Скинем высоту до 0 и установим отступ сверху для элемента:

.article-img {
  background: url('img/landscape.jpg');
  width: 100%;
  min-width: 0;
  height: 0;
  padding-top: 66%;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  margin-right: 0;
  margin-bottom: 20px;
}

Круто. Но тоже неидеально. Во-первых, этот код может быть не совсем понятен новичку. Откуда 66 процентов, почему высота нулевая? А во-вторых, больше на странице нет элемента img. Это значит, что поисковые роботы воспринимают наш сайт, как сайт без картинок. Если нам очень важно SEO и у нас на сайте уникальные картинки, которые очень хочется, чтобы были проиндексирован, то такой способ совсем не для нас. Ещё один аспект, который мы теряем — семантика. Мы используем тэг div совсем не для того, для чего он предназначен, и при этом не используем специально существующий для нашей задачи тэг img.

3. Более-менее современный способ (тэг img с новыми атрибутами)

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

<img src="image-full.png" 
     srcset="image-small.png 100w, 
             image-medium.png 500w, 
             image-full.png 1000w">

Тут мы сообщаем, что у нас на сервере есть 3 картинки image-small.png, image-medium.png и image-full.png, а ширины этих картинок 100, 500 и 1000 пикселей соответственно. Обратите внимание, при указании ширины надо добавлять букву w, а не px. Атрибут src мы оставляем для совместимости со старыми браузерами. Чтобы всё заработало придется добавить ещё один атрибут к тэгу — sizes. Этот атрибут указывает, какие размеры изображений мы бы хотели видеть в зависимости от медиазапросов. Тут всё очень просто — похоже на медиазапросы в CSS:

<img src="image-full.png" 
     srcset="image-small.png 100w, 
             image-medium.png 500w, 
             image-full.png 1000w"
     sizes="(max-width: 300px) 100px,
            (max-width: 600px) 450px,
            (max-width: 1000px) 1000px">

Теперь всё будет происходить следующим образом:

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

Например, если так получается, что размер окна браузера будет равен 500 пикселей, то, судя по атрибутам, тэг img должен иметь размер 450 пикселей, и браузер должен скачать и нарисовать для нас файл image-medium.png потому, что ширина 500 пикселей наиболее близка к значению 450.

Попробуем применить этот подход для нашей задачи. У нас всего два файла картинок и одно значение ширины экрана в медиазапросе.

<img class="article-img" 
     src="img/square.jpg" 
     srcset="img/landscape.jpg 800w, 
             img/square.jpg 300w"
     sizes="(max-width: 840px) 100%,
            (min-width: 841px) 300px"
     alt="ISsoft Trainig center">

Понадобится ещё немного CSS, чтобы управлять размерами элемента

.article-img {
  display: block;
  min-width: 300px;
  width: 300px;
  height: 100%;
  margin-right: 20px;
}

@media only screen and (max-width: 840px) {
  .article-img {
    width: 100%;
    margin-right: 0;
    margin-bottom: 20px;
  }
}

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

Однако есть в нашем случае одно интересное поведение. Попробуйте открыть страницу сразу в окне браузера маленького размера, а затем увеличить окно. Получается вот так:


Как видно, картинка не поменялась на квадратную. Это совсем не то поведение, которое нам бы хотелось видеть. Дело в том, что атрибуты srcset и sizes придуманы прежде всего не для визуальных эффектов, а для сокращения трафика. Браузер интерпретирует их содержание не совсем буквально, а так, чтобы размер загруженной картинки был минимальным. Причем, чаще всего, нам требуется показывать не разные картинки в зависимости от ширины экрана, а одну и ту же, просто уменьшенную и увеличенную. Получается так, что мы указали значение ширины каждого файла в srcset, и благодаря этому, браузер понимает, что картинка landscape.jpg больше, чем square.jpg. Поэтому, когда мы смотрим сайт в мобильной версии, приходится загружать широкую картинку, но, когда мы увеличиваем ширину экрана, несмотря на то, что в атрибутах картинки явно указан новый файл, браузер даже не собирается его загружать. Зачем тратить трафик на загрузку меньшей картинки, если большая уже скачана — думает он. Более того, если словить этот баг и перезагрузить страницу, то мы все равно увидим не то изображение, которое хотим. Широкая картинка попала в кэш браузера, и самый лучший способ сократить трафик — достать ее из кэша.

Такое поведение вполне оправдано, если вы хотите действительно для разных экранов загружать одни и те же картинки разного разрешения, но совершенно не подходит для нас.

Вот почти получилось, но опять неидеально… Есть ли способ сделать то, что нужно, но без всяких оговорок? Есть!

4. Последний и самый лучший вариант (элемент picture)

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

<picture>
  <source srcset="image-full.png" media="(max-width: 100px)">
  <source srcset="image-medium.png" media="(max-width: 600px)">
  <source srcset="image-small.png" media="(max-width: 300px)">
  <img src="image-full.png" alt="ISsoft Trainig center">
</picture>

Тэг picture не простой, а составной. Внутри него находится обычный img, отображением которого мы собираемся управлять, и несколько элементов source с помощью которых, можно указать какая картинка какому медиазапросу соответствует. Тут всё очень просто, браузер не пробует ничего додумать сам, с помощью тэга picture мы задаем абсолютно четкие инструкции. Попробуем применить это для нашей задачи:

<picture>
  <source srcset="img/landscape.jpg" media="(max-width: 840px)">
  <img class="article-img" src="img/square.jpg" alt="ISsoft Trainig center">
</picture>

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

Но, к сожалению, не бывает решений без оговорок. И для элемента picture есть один нюанс, который стоит учитывать — это отсутствие поддержки во всех версиях IE. Но, на самом деле, эта особенность не такая уж и страшная. Внутри тэга picture мы используем img, который IE обязательно увидит и нарисует, просто не будет работать фишка со сменой картинок для разных размеров окна. В принципе, можно добавить туда в src специальную картинку, которая будет видна во всех старых браузерах, а в элементах source определить нужное нам поведения для поддерживающих picture браузеров, например так:

<picture>
  <source srcset="img/square.jpg" media="(min-width: 841px)">
  <source srcset="img/landscape.jpg" media="(max-width: 840px)">
  <img class="article-img" src="img/image-for-IE.jpg" alt="ISsoft Trainig center">
</picture>

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

Подведем итоги

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

  1. Наиболее простой и подходящий способ показывать разные картинки для различных размеров браузера — это элемент picture
  2. Если в зависимости от ширины экрана вам нужно показывать одну и ту же картинку, но разного разрешения, то можно использовать тэг img c атрибутами srcset и sizes
  3. Если нужна поддержка старых браузеров, то придется пожертвовать семантикой и размеров CSS кода и использовать способ с background-image

Полный код всех примеров можно посмотреть в репозитории.

Автор материала – Игорь Коршук, преподаватель Тренинг центра ISsoft.

Образование: окончил физический факультет Белорусского Государственного Университета.
Опыт работы: front-end разработчик с 2010 года.