В наши дни главным камнем преткновения на пути к высокой скорости загрузки сайтов являются изображения. Это особенно характерно для проектов из сферы электронной коммерции. На них изображения, обычно довольно «тяжёлые», составляют основную часть содержимого страниц. Это, как правило, приводит к тому, что для того, чтобы показать пользователю страницу, его браузеру требуется загрузить несколько мегабайт графических данных. Как ускорить загрузку страниц в такой ситуации? Ответу на этот вопрос и посвящён материал, перевод которого мы сегодня публикуем.
Общие положения
Рассмотрим, для примера, стартовую страницу отдела Home на сайте Walmart.
Страница, на которой имеется множество изображений
Вот сведения о том, сколько изображений загружается для формирования этой страницы:
Изображения, загружаемые при формировании страницы
Как видите, тут 137 изображений! Это означает, что более 80% данных, необходимых для вывода страницы и передаваемых по сети, представлены в виде графических файлов.
Проанализируем теперь сетевые запросы, выполняемые при загрузке страницы:
Сетевые запросы, выполняемые при формировании страницы
В данном случае файлы, полученные в результате разделения кода проекта, загружаются позже, чем могли бы. Происходит это из-за того, что сначала нужно загрузить главный бандл cp_ny.bundle
. Этот бандл можно было бы загрузить куда быстрее, если бы ему не мешали 18 изображений, соревнующихся друг с другом за полосу пропускания.
Как это исправить? На самом деле, по-настоящему «исправить» это не получится, но можно сделать много всего для того, чтобы оптимизировать загрузку изображений. Существует немало подходов к оптимизации изображений, используемых на веб-страницах. Среди них — использование различных форматов графических файлов, сжатие данных, применение техники blur animation, использование CDN. Мне хотелось бы остановиться на так называемой «ленивой загрузке» изображений (lazy loading). В частности, речь пойдёт о том, как реализовать эту технику на React-сайтах, но, так как основана она на механизмах JavaScript, её можно интегрировать в любой веб-проект.
Экспериментальный проект
Начнём с такого вот предельно простого React-компонента Image
:
class Image extends PureComponent {
render() {
const { src } = this.props;
return <img align="center" src={src} />;
}
}
Он принимает, в качестве свойства, URL, и использует его для рендеринга HTML-элемента img
. Вот соответствующий код на JSFiddle. На следующем изображении показана страница, содержащая этот компонент. Обратите внимание на то, что для того, чтобы увидеть выводимое им изображение, нужно прокрутить содержимое страницы.
Страница с компонентом, выводящим изображение
Для того чтобы реализовать в этом компоненте методику ленивой загрузки изображений нужно выполнить следующие три шага:
- Не рендерить изображение сразу после загрузки.
- Настроить средства обнаружения появления изображения в области просмотра содержимого страницы.
- Вывести изображение после того, как будет обнаружено, что оно попало в область просмотра.
Разберём эти шаги.
Шаг 1
На этом шаге изображение, сразу после загрузки не выводится.
render() {
return <img />;
}
Шаг 2
Здесь мы настраиваем механизмы, которые позволяют обнаружить момент попадания изображения в область просмотра.
componentDidMount() {
this.observer = new IntersectionObserver(() => {
// тут будет код для реализации третьего шага
},
{
root: document.querySelector(".container")
});
this.observer.observe(this.element);
}
....
render() {
return <img ref={el => this.element = el} />;
}
Разберём этот код. Вот что здесь сделано:
- К элементу
img
добавлен атрибут ref. Это позволяет позже обновить ссылку на изображение вsrc
без необходимости проводить повторный рендеринг компонента. - Создан новый экземпляр
IntersectionObserver
(об этом мы поговорим ниже). - Объекту
IntersectionObserver
предложено наблюдать за изображением с использованием конструкцииobserve(this.element)
.
Что такое IntersectionObserver
? Учитывая то, что слово «intersection» переводится как «пересечение», а «observer» — это «наблюдатель», уже можно догадаться о роли этого объекта. Если же поискать сведения о нём на MDN, то можно узнать, что API Intersection Observer позволяет веб-приложениям асинхронно следить за изменением пересечения элемента с его родителем или областью видимости документа viewport.
На первый взгляд такая характеристика API может показаться не особенно понятной, но, на самом деле, устроено оно очень просто. Экземпляру IntersectionObserver
передаётся несколько параметров. В частности, мы использовали параметр root
, который позволяет задать корневой DOM-элемент, рассматриваемый нами в качестве контейнера, о пересечении элемента с границей которого нам нужно узнать. По умолчанию это область, в которой находится видимый фрагмент страницы (viewport), но я явным образом установил его на использование контейнера, находящегося в элементе iframe
JSFiddle. Сделано это для того, чтобы, позже, рассмотреть одну возможность, которая не рассчитана на использование элементов iframe
.
Причина, по которой использование IntersectionObserver
для определения момента того, когда элемент становится видимым, популярнее более традиционных методов, вроде совместного применения onScroll
и getBoundingClientRect()
заключается в том, что механизмы IntersectionObserver
выполняются за пределами главного потока. Однако, коллбэк, вызываемый после того, как IntersectionObserver
обнаружит пересечение элемента с контейнером, выполняется, естественно, в главном потоке, поэтому его код не должен быть слишком тяжёлым.
Шаг 3
Теперь нам надо настроить коллбэк, вызываемый при обнаружении пересечения элемента target
(this.element
в нашем случае) с контейнером root
(в нашем случае это div
-элемент .container
).
....
this.observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
const { isIntersecting } = entry;
if (isIntersecting) {
this.element.src = this.props.src;
this.observer = this.observer.disconnect();
}
});
},
{
root: document.querySelector(".container")
}
);
....
В коллбэк, при обнаружении пересечения, передаётся массив элементов entries
, который напоминает набор снимков состояния всех целевых элементов, для которых обнаружено пересечение заданной границы. Свойство isIntersecting
указывает на направление пересечения. Если элемент, за которым организовано наблюдение, попадает извне в корневой элемент, оно равно true
. Если элемент покидает корневой элемент, то оно равно false
.
Итак, когда выясняется, что элемент пересёкся с нижней границей контейнера, я вручную устанавливаю его свойство src
и отключаю наблюдение за ним, в котором больше нет необходимости.
Шаг 4 (секретный)
Теперь, на четвёртом, секретном шаге нашей работы, можно полюбоваться результатом и порадоваться успеху. Вот код, в котором собрано то, о чём мы только что говорили.
Результат применения методики ленивой загрузки изображений
Однако если поближе рассмотреть то, что у нас получилось, окажется, что тут можно найти кое-что не очень хорошее. Для того чтобы это разглядеть, я быстро прокрутил страницу, замедлив при этом скорость сетевого соединения.
Поведение страницы при её быстрой прокрутке и замедлении скорости сетевого соединения
Так как мы загружаем изображение только после того, как оно достигло области, в которой оно уже должно быть видимым, у пользователя нет возможности прокрутить страницу и увидеть область, занимаемую изображением, и, естественно, само изображение, до его загрузки. Когда сайты смотрят с обычных компьютеров, подключённых к быстрому интернету, это проблем не вызывает. Но многие современные пользователи посещают сайты с телефонов, иногда они пользуются 3G-сетями или, что ещё хуже, EDGE-подключениями.
С этой проблемой, правда, справиться не так уж и сложно. Сделать это можно благодаря тому, что API Intersection Observer предоставляет разработчику возможность расширять или сужать границы корневого элемента (в нашем случае это элемент .container
). Для того чтобы этой возможностью воспользоваться, достаточно добавить одну строчку кода туда, где осуществляется настройка корневого контейнера:
rootMargin: "0px 0px 200px 0px"
В свойство rootMargin
надо записать строку, структура которой соответствует правилам CSS, используемым для настройки параметров отступов элементов. В нашем случае мы сообщаем системе о том, что нижнюю границу, используемую для обнаружения пересечения элемента с контейнером, нужно увеличить на 200 пикселей. Это означает, что соответствующий коллбэк будет вызван, тогда, когда элемент попадёт в область, которая на 200 пикселей ниже нижней границы корневого элемента (по умолчанию здесь используется значение 0).
Вот код, в котором реализована эта методика.
Совершенствование методики ленивой загрузки изображений
В результате оказывается, что когда мы прокручиваем страницу лишь к 4-му элементу списка, изображение загружается в области, которая на 200 пикселей ниже видимой области страницы.
Теперь, казалось бы, сделано всё, что нужно. Но это не так.
Проблема высоты изображения
Если вы внимательно изучали приведённые выше GIF-иллюстрации, то вы могли заметить, что полоса прокрутки совершает «прыжок» после загрузки изображения. К счастью, с этой проблемой несложно справиться. Её причина заключается в том, что элемент, выводящий изображение, изначально имеет высоту 0, которая, после загрузки изображения, оказывается равной 300 пикселей. Поэтому для исправления проблемы достаточно задать элементу фиксированную высоту, добавив к изображению атрибут height={300}
.
О результатах оптимизации
Каких результатов мы, в Walmart, добились после применения ленивой загрузки изображений на этой странице? На самом деле, конкретные результаты очень сильно варьируются в зависимости от множества обстоятельств, среди которых можно отметить скорость подключения клиента к сети, доступность CDN, количество изображений на странице и применяемые к ним правила обнаружения пересечения с корневым элементом. Другими словами, вам, для того, чтобы оценить воздействие ленивой загрузки изображений на собственный проект, лучше всего самим это реализовать и проверить. Но если вам всё же интересно взглянуть на то, что ленивая загрузка изображений дала нам, вот пара отчётов Lighthouse. Первый сформирован до оптимизации, второй — после.
Отчёт Lighthouse, сформированный до оптимизации
Отчёт Lighthouse, сформированный после оптимизации
Итоги
Сегодня мы рассмотрели методику оптимизации веб-страниц с использованием ленивой загрузки изображений. Если страницы вашего сайта полны картинок, то, вполне возможно, эта методика вам пригодится.
Уважаемые читатели! Как вы оптимизируете изображения и их загрузку?
Автор: ru_vds