Советы по созданию собственной ленивой загрузки картинок

Spread the love

Возможно, вы слышали, что «можно использовать ленивую загрузку!», когда искали способ увеличить скорость загрузки особенно тяжелой веб-страницы.

Ленивая загрузка – это популярный метод постепенного отображения изображений по мере их загрузки, а не сразу после того, как HTML страница был проанализирована. Этот метод может уменьшить начальный размер страницы и помочь нам сократить бюджет производительности, запрашивая изображения, только когда они необходимы.

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

Мы рассмотрим следующие темы:

Вот что мы будем строить:

Demo Site
GitHub Repo

Почему не использовать ленивую загрузку встроенную в браузеры?

В настоящее время не все браузеры поддерживают отложенную загрузку. Хотя в некоторых браузерах ситуация должна измениться в ближайшее время с выпуском Chrome 75, целью которого является включение поддержки отложенной загрузки изображений и фреймов. До тех пор отложенную загрузку придется реализовывать с использованием JavaScript. Существует множество библиотек и структур, которые могут нам помочь с этим.

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

lazy loading example

Типичная механика для ленивой загрузки

Большинство подходов следуют следующей схеме.

Сначала описывается HTML, чтобы определить наши лениво загруженные изображения.

<!-- 
Не включайте атрибут src в изображения, которые вы хотите загружать лениво. Вместо этого укажите их src безопасно в атрибуте данных
-->
<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" class="lazy" />

Когда должна происходить загрузка изображения?

Далее мы используем магию JavaScript, чтобы правильно установить атрибут src, что бы изображение отобразилось. Когда-то это была дорогостоящая операция JavaScript, включающая прослушивание событий прокрутки окна и изменения размера, но теперь мы можем использовать IntersectionObserver.

Создание intersection observer выглядит следующим образом:

// Установите intersection observer с опциями
var observer = new IntersectionObserver(lazyLoad, {
  // отступ от края области просмотра,
  rootMargin: "100px",
  // сколько элементов должно пересекаться
  // чтобы запустить нашу функцию загрузки
  threshold: 1.0
});

Мы указали нашему новому observer вызывать функцию с именем lazyLoad, когда указанные наблюдаемые условия будут выполнены. Элементы, которые удовлетворяют этим условиям, будут переданы в эту функцию, чтобы мы могли ими манипулировать … например, загрузить и отобразить их.

function lazyLoad(elements) {
  elements.forEach(image => {
    if (image.intersectionRatio > 0) {

      // установить атрибут src для запуска загрузки
      image.src = image.dataset.src;

      // хватит наблюдать за этим элементом. 
      // Наша работа здесь завершена!
      observer.unobserve(item.target);
    };
  });
};

Отлично. Нашим изображениям будет назначен правильный src, когда они появятся, что приведет к их загрузке. Но какие изображения? Нам нужно сообщить API Intersection Observer, какие элементы нам нужны. Для этой цели каждому из них был присвоен класс CSS .lazy.

// Скажите нашему observe наблюдать все элементы 
// img с помощью класса lazy
var lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
  observer.observe(img);
});

Хорошо. Но так ли это идеально?

Кажется, что это хорошо работает, но есть некоторые недостатки, которые следует учитывать:

  1. Пока JavaScript не появится и не будет успешно работать, у нас на странице будет куча элементов изображения, которые не будут отображены. Мы намеренно отменили их, удалив атрибут src. Это результат, который мы хотели получить, но теперь мы зависим от JavaScript для загрузки этих изображений. Хотя верно то, что JavaScript довольно широко распространен в наши дни в Интернете. Но JavaScript может стать дорогостоящим дополнением к нашим бюджетам на производительность, особенно если он участвует в доставке и рендеринге. содержания страницы. Как однажды заметил Джейк Арчибальд, все ваши пользователи не являются JS, пока они загружают ваш JS.
  2. Даже когда это хорошо работает, у нас на странице есть пустые элементы, которые могут вызвать некоторую визуальную проблему, когда они загружаются. Возможно, нам нужно сначала отобразить изображение по умолчанию. Мы вернемся к этому в ближайшее время.

Запланированная реализация Chrome с отложенной загрузкой должна помочь в решении нашей первой проблемы. Если элементу присвоен атрибут loading, Chrome может использовать атрибут src, указанный в нужное время, вместо того, чтобы запрашивать его в тот момент, когда он видит его в HTML.

Проект новой спецификации HTML включает в себя поддержку различных режимов загрузки::

  • <img loading="lazy" /> Браузер загрузит это изображение лениво.
  • <img loading="eager" /> Браузеру загрузить это изображение немедленно.
  • <img loading="auto" /> Браузеру сам сделает выбор как загрузить это изображение.

Браузеры без этой поддержки будут игнорировать новые атрибуты HTML и загрузят изображение в обычном режиме.

Но … эта функция еще не реализована в Chrome, и существует также неопределенность относительно того, когда и когда другие браузеры смогут ее реализовать. Мы можем использовать feature detection, чтобы решить, какой метод мы используем, но это по-прежнему не дает надежного подхода для прогрессивного улучшения, когда изображения не зависят от JavaScript.

<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" loading="lazy" class="lazy" />
// Если браузер поддерживает отложенную загрузку,
// мы можем смело назначать аттрибут src
// не вызывая мгновенную загрузку изображения.
if ("loading" in HTMLImageElement.prototype) {
  const lazyImages = document.querySelectorAll("img.lazy");
  lazyImages.forEach(img => {
    img.src = img.dataset.src;
  });
}
else {
  // используйте свою собственную ленивую загрузку с помощью Intersection Observers и всего этого джаза
}

Как компаньон для адаптивных изображений

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

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

HTML дает нам несколько способов реализации адаптивных изображений, которые связывают разные источники изображений с различными условиями области просмотра. Мне нравится использовать picture :

<picture>
  <source srcset="massive-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="medium-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="small-lighthouse.jpg" media="(min-width: 300px)">
  <img src="regular-lighthouse.jpg" alt="snazzy lighthouse" />
</picture>

Вы заметите, что у каждого source есть атрибут srcset, который указывает URL-адрес изображения, и атрибут media, который определяет условия, при которых этот источник должен использоваться. Браузер выбирает наиболее подходящий источник из списка в соответствии с условиями media со стандартным элементом img, действующим как значение по default/fallback.

Можем ли мы объединить эти два подхода для создания лениво загруженных адаптивных изображений?

Конечно, мы можем! Давай сделаем это.

Вместо того, чтобы иметь пустое изображение, пока мы не выполним нашу ленивую загрузку, мне нравится загружать изображение-заполнитель, который имеет крошечный размер. Это приводит к дополнительным расходам на создание большего количества HTTP-запросов, но также дает хороший эффект намека на изображение до его появления. Возможно, вы видели этот эффект на Medium или на сайте, использующем ленивую загрузочную механику Gatsby.

Мы можем добиться этого, первоначально определив источники изображения в нашем элементе изображения как крошечные версии того же ресурса, а затем используя CSS, чтобы масштабировать их до того же размера, с более высоким разрешением. Затем, через нашего IntersectionObserver, мы можем обновить каждый из указанных source, чтобы они указывали на правильные источники изображений.

Итак, изначально наш элемент изображения может выглядеть так:

<picture class="lazy">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 300px)">
  <img src="tiny-lighthouse.jpg" alt="snazzy cake" />
</picture>

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

Предварительный просмотр изображения из стилей

Браузер может увеличить крошечное предварительное изображение для нас с помощью CSS, чтобы оно соответствовало всему элементу изображения, а не была бы просто 20 пикселей. Объекты станут немного … пикселированными, как вы можете себе представить, когда изображение с низким разрешением раздувается до больших размеров.

picture {
  width: 100%; /* stretch to fit its containing element */
  overflow: hidden;
}

picture img {
  width: 100%; /* stretch to fill the picture element */
}
blurred image

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

picture.lazy img {
  filter: blur(20px);
}
more fully blurred image

Переключение source с помощью JavaScript

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

function lazyLoad(elements) {

  elements.forEach(picture => {
    if (picture.intersectionRatio > 0) {

      // gather all the image and source elements in this picture
      var sources = picture.children;

      for (var s = 0; s < sources.length; s++) {
        var source = sources[s];

        // установить новый srcset для source элементов
        if (sources.hasAttribute("srcset")) {
          source.setAttribute("srcset", ONE_OF_OUR_BIGGER_IMAGES);
        }
        // или новый src на элементе img
        else {
          source.setAttribute("src", ONE_OF_OUR_BIGGER_IMAGES);
        }
      }

      // стоп наблюдать за этим элементом. Наша работа завершена!
      observer.unobserve(item.target);
    };
  });

};

Последний шаг для завершения эффекта: удаление эффекта размытия изображения после загрузки нового source. Мы сделаем это через event listener события load на каждом новом ресурсе изображения.

// remove the lazy class when the full image is loaded to unblur
source.addEventListener('load', image => {
  image.target.closest("picture").classList.remove("lazy")
}, false);

Так же мы можем сделать хороший переход, который устраняет размытие с помощью CSS.

picture img {
  ...
  transition: filter 0.5s,
}

Маленький помощник от наших друзей

Отлично. С помощью всего лишь небольшого кода JavaScript, нескольких строк CSS и очень удобного кода HTML мы создали метод отложенной загрузки, который также обслуживает адаптивные изображения. Итак, почему мы не счастливы?

Ну, мы создали две проблемы:

  1. Наша разметка для добавления изображений стала сложнее, чем раньше. Раньше жизнь была простой, когда все, что нам было нужно, это один тег img со старым добрым атрибутом src.
  2. Нам также потребуется создавать несколько версий каждой картинки, чтобы получить каждый размер и плюс предварительно загруженное состояние. Это потребует значительное время на создание всего того что будет нужно.

Но не бойтесь. Мы можем упростить обе эти проблемы.

Генерация элементов HTML

Давайте сначала посмотрим, как генерировать новый HTML, а не каждый раз создавать его вручную.

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

  • Jekyll позволяет создавать собственные плагины
  • Hugo дает вам специальные шорткоды
  • У Eleventy есть шорткоды для всех шаблонных движков, которые он поддерживает
  • и т. д…

В качестве примера я сделал шорткод lazypicture в своем примере проекта, созданного с помощью Eleventy. Шорткод используется так:

{% lazypicture lighthouse.jpg "A snazzy lighthouse" %}

Чтобы сгенерировать HTML, который нам нужен во время сборки:

<picture class="lazy">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 300px)">
  <img src="/images/tiny/lighthouse.jpg" alt="A snazzy lighthouse" />
</picture>

Генерация изображений

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

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

Вариант 1. Создание изображений во время сборки

Для этого существуют популярные утилиты. Независимо от того, запускаете ли вы свои сборки с помощью Grunt, Gulp, webpack, Make или чего-то еще, скорее всего, для вас есть уже созданные плагины.

Пример ниже использует gulp-image-resize в задаче Gulp как часть процесса сборки. Он может разбирать каталог с изображениями и генерировать нужные вам варианты. У него есть куча опций, которыми вы можете управлять, и вы можете комбинировать их с другими утилитами Gulp, чтобы делать такие вещи, как называть разные варианты в соответствии с выбранными вами соглашениями.

var gulp = require('gulp');
var imageResize = require('gulp-image-resize');

gulp.task('default', function () {
  gulp.src('src/**/*.{jpg,png}')
    .pipe(imageResize({
      width: 100,
      height: 100
    }))
    .pipe(gulp.dest('dist'));
});

Сайт CSS-Tricks использует аналогичный подход (благодаря функции нестандартных размеров в WordPress) для автоматической генерации всех его различных размеров изображения. (О да! CSS-Tricks – в тренде!) ResponsiveBreakpoints.com предоставляет веб-интерфейс для экспериментов с различными настройками и опциями для создания наборов изображений и даже генерирует код для вас.

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

Альтернативой является преобразование этих ресурсов во время запроса, а не на этапе сборки. Это второй вариант.

Вариант 2: преобразования изображений по требованию

Я сторонник предварительного рендеринга контента. Я рассказывал об этом подходе (часто называемом JAMstack) в течение достаточно долгого времени, и я считаю, что он имеет многочисленные преимущества в производительности, безопасности и простоте. (Крис подытожил это в посте о статическом хостинге и JAMstack.)

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

Комбинируя преобразования изображений с мощными возможностями CDN и кэширования активов такими компаниями, как Netlify, Fastly и Cloudinary, можно быстро создавать изображения с размерами, которые вы передаете им через URL-адрес. Каждый сервис обладает значительной вычислительной мощностью, чтобы выполнять эти преобразования на лету, а затем кэшировать сгенерированные изображения для будущего использования. Это обеспечивает бесперебойную визуализацию для последующих запросов.

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

Сервис преобразования изображений Netlify построен на основе чего-то, называемого Netlify Large Media. Это функция, созданная для управления большими ресурсами в вашем VCS. Git не очень хорош в этом по умолчанию, но  Git Large File Storage  может расширить Git, чтобы позволить включать большие ресурсы в ваши репозитории, не засоряя их и не делая их неуправляемыми.

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

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

Netlify ищет параметры строки запроса querystring при преобразовании изображений. Вы можете указать высоту, ширину и тип обрезки, которую хотите выполнить. Например:

  • Необработанное изображение без преобразований:
    /images/apple3.jpg
  • Изображение размером 300 пикселей в ширину:
    /images/apple3.jpg?nf_resize=fit&w=300
  • Изображение обрезается до 500 на 500 пикселей с автоматическим определением фокуса:
    /images/apple3.jpg?nf_resize= smartcrop&w=500&h=500

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

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

Завершение

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

Этот демонстрационный сайт объединяет ряд этих концепций и использует сервис преобразования изображений Netlify.

GitHub Repo

Оригинальная статья: Phil Hawksworth Tips for rolling your own lazy loading


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.