Есть много способов улучшить производительность вашего сайта. Обслуживание ваших статических ресурсов из CDN, использование сжатия GZip или Brotli и объединение ваших файлов CSS/JS должны быть в верхней части вашего списка. Одна вещь, которая может не быть на вашем радаре, — это ленивая загрузка ваших изображений.

Что такое ленивая загрузка?

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

Насколько полезна отложенная загрузка? Мы создали страницу «до» и «после», чтобы показать, насколько важна отложенная загрузка изображений. Наш пример — образец сообщения в блоге. Блоги — отличный пример, потому что в них часто много изображений, и большинство из них находятся ниже сгиба (за пределами окна просмотра).

Пример записи в блоге (до ленивой загрузки)

Пример записи в блоге (после ленивой загрузки)

Вы заметили разницу? Если вы не уверены, просто посмотрите, насколько сильно наш скрипт отложенной загрузки повлиял на производительность нашего сайта:

С помощью всего лишь одного крошечного скрипта мы поднялись с F на A в оценке производительности MachMetric. Мы сократили время загрузки страницы на более чем на 90 %. Размер нашей страницы уменьшился на более чем на 95%!

Готовы начать ленивую загрузку изображений прямо сейчас? Вот код, и он очень простой!

Ленивая загрузка — код

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

var images = document.querySelectorAll('.js-lazy-image');      
var config = {        
  rootMargin: '50px 0px',        
  threshold: 0.01      
};      
var imageCount = images.length;      
var observer;      
var fetchImage = function(url) {        
  return new Promise(function(resolve, reject) {          
    var image = new Image();          
    image.src = url;          
    image.onload = resolve;          
    image.onerror = reject;        
  });      
}      
var preloadImage = function(image) {        
  var src = image.dataset.src;        
  if (!src) {          
    return;        
  }        
  return fetchImage(src).then(function(){ 
    applyImage(image, src); 
  });      
}      
var loadImagesImmediately = function(images) {        
  for (image in images) {          
    preloadImage(image);        
  }      
}      
var onIntersection = function(entries) {        
  if (imageCount === 0) {          
    observer.disconnect();        
  }        
  for (entry in entries) {          
    if (entry.intersectionRatio > 0) {            
      imageCount--;            
      observer.unobserve(entry.target);              
      preloadImage(entry.target);          
    }        
  }      
}      
var applyImage = function(img, src) {        
  img.classList.add('js-lazy-image--handled');        
  img.src = src;      
}      
if (!('IntersectionObserver' in window)) {        
  loadImagesImmediately(images);      
} else {        
  observer = new IntersectionObserver(onIntersection, config);          
  for (image in images) {          
    var image = images[i];          
    if (image.classList.contains('js-lazy-image--handled')) {            
      continue;          
    }          
    observer.observe(image);        
  }      
}

Как это работает

Этот скрипт ищет изображения с классом js-lazy-image. Эти изображения должны иметь атрибут данных, называемый data-src, в котором указано местоположение изображения, точно так же, как был бы заполнен обычный атрибут src. Затем скрипт подсчитывает количество изображений с этим классом, и когда область просмотра пересекается с наблюдаемой областью, включающей изображение, источник изображения загружается из его атрибутов данных, и изображение отображается на экране! Вот пример того, как можно вызвать отложенную загрузку изображений:

<img alt="Lazy" class="js-lazy-image" data-src="/location/of/my/image.jpg" />

Как видите, это работает почти так же, как обычное изображение, но вам просто нужно добавить специальный класс JavaScript и добавить префикс data- перед вашим атрибутом src. И это все, что нужно, чтобы уйти от этого:

К этому:

Как работает скрипт — раздел за разделом

Все еще интересно, как работает скрипт? Разобьем по разделам:

var images = document.querySelectorAll('.js-lazy-image');      
var config = {        
  rootMargin: '50px 0px',        
  threshold: 0.01      
};      
var imageCount = images.length;      
var observer;

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

var fetchImage = function(url) {        
  return new Promise(function(resolve, reject) {          
    var image = new Image();          
    image.src = url;          
    image.onload = resolve;          
    image.onerror = reject;        
  });      
}

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

var preloadImage = function(image) {        
  var src = image.dataset.src;        
  if (!src) {          
    return;        
  }        
  return fetchImage(src).then(function(){ 
    applyImage(image, src); 
  });      
}

Эта функция предварительной загрузки изображения получает источник изображения из атрибута данных, который мы прикрепили к нашему тегу изображения. Если он не находит исходный объект, не проблема, мы просто останавливаемся. В противном случае мы получаем наше изображение, и если все идет хорошо, мы применяем изображение к DOM следующим образом:

var applyImage = function(img, src) {        
  img.classList.add('js-lazy-image--handled');        
  img.src = src;      
}

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

if (!('IntersectionObserver' in window)) {        
  loadImagesImmediately(images);      
} else {        
  observer = new IntersectionObserver(onIntersection, config);        
  for (image in images) {          
    var image = images[i];          
    if (image.classList.contains('js-lazy-image--handled')) {            
      continue;          
    }          
    observer.observe(image);        
  }      
}

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

Традиционно вам пришлось бы делать что-то подобное с Событием прокрутки, которое одновременно и медленное, и вызывается постоянно. Чтобы бороться с этим сбоем производительности, вы можете использовать устранение дребезга или регулирование, чтобы ограничить количество запросов событий прокрутки. Но с Intersection Observers все это делается за вас.

Помните переменную конфигурации в верхней части нашего скрипта? Это конфигурация нашего Intersection Observer. Он говорит нам, что когда мы находимся в пределах 50 пикселей (по вертикали) от нашего изображения, именно тогда мы хотим активировать наш обратный вызов наблюдения. Порог, как вы можете догадаться, представляет собой допустимое значение того, какой процент объекта должен соблюдаться, чтобы обратный вызов был вызван после достижения поля. В нашем случае мы выбрали 1%, то есть сразу после отображения тега изображения.

Итак, теперь, когда у нас есть эта предыстория, мы можем увидеть, как работает это выражение if. Если мы видим, что Intersection Observers являются частью объекта окна, мы знаем, что находимся в браузере, поддерживающем эту функциональность. На данный момент Intersection Observers доступны во всех основных браузерах, кроме IE и Safari. Поэтому, если вы используете IE или Safari, вы сразу же загрузите изображения. Если нет, мы создаем новый объект Intersection Observer с конфигурацией, которую мы предоставили в начале, а также функцию обратного вызова, которая срабатывает при достижении нашего целевого наблюдения.

Наконец, мы должны сообщить наблюдателю, что именно он должен наблюдать для инициализации обратного вызова. В этом случае мы наблюдаем за всеми изображениями, которые еще не были обработаны, то есть изображениями, которые еще не были применены к DOM (с помощью функции applyImages, которую мы видели ранее).

Так как же выглядит загрузка изображений и обратный вызов наблюдения?

var loadImagesImmediately = function(images) {        
  for (image in images) {          
    preloadImage(image);        
  }      
}

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

var onIntersection = function(entries) {        
  if (imageCount === 0) {          
    observer.disconnect();        
  }        
  for (entry in entries) {          
    if (entry.intersectionRatio > 0) {            
      imageCount--;            
      observer.unobserve(entry.target);            
      preloadImage(entry.target);          
    }        
  }      
}

Наш обратный вызов пересечения немного сложнее. Если мы загрузили все наши изображения, которые имеют наш класс CSS отложенной загрузки, мы закончили, и мы можем отключиться от нашего объекта Observer. В противном случае для каждой записи наблюдателя в нашем IntersectionObserver мы хотим активировать наши изображения. Мы делаем это, гарантируя, что достигли нашего порога. intersectionRatio — это свойство, которое нам нужно, чтобы увидеть, виден ли наш целевой элемент изображения в пределах порога, который мы определили в нашей конфигурации. Если это так, свойство возвращает 1, в противном случае оно возвращает 0. Таким образом, если мы достигли необходимого соотношения, у нас есть еще одно изображение для добавления в DOM, что означает, что мы можем удалить 1 из нашего количества изображений, оставшихся до нагрузка. Поэтому мы можем перестать наблюдать за этим изображением, потому что оно будет загружено на страницу. Наконец, мы используем наш ранее определенный preloadImage для выполнения нашего уже знакомого процесса добавления URL-адреса изображения в тег изображения и загрузки изображения в DOM.

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

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

Какие еще быстрые победы у вас есть для ускорения вашего сайта? Оставьте ответ в комментариях ниже!

Первоначально опубликовано на www.machmetrics.com 24 мая 2018 г.