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

Объём ресурсов вырос за десятилетие в несколько раз

Проблема имеет измеримые масштабы. По оценкам, в период с 2011 по 2019 год средний вес страницы на настольном компьютере вырос примерно со ста килобайт до четырёхсот. На мобильных устройствах рост ещё заметнее: с пятидесяти до трёхсот пятидесяти килобайт.

Отдельно по изображениям: на настольных системах средний вес вырос с двухсот пятидесяти килобайт до девятисот, на мобильных со ста до восьмисот пятидесяти.

Отсюда практический вывод. Медиа давно перестали быть второстепенной частью загрузки, они стали основной. Любая работа над скоростью начинается именно с них.

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

Браузер умеет откладывать загрузку без сценариев

Самый простой способ не требует ни строчки кода. Достаточно указать в разметке соответствующее значение атрибута.

<img
  src="/catalog/chair.jpg"
  width="800"
  height="600"
  loading="lazy"
  alt="Стул с деревянными подлокотниками"
/>

Браузер сам определяет, когда изображение приближается к области просмотра, и только тогда начинает загрузку. Расстояние запаса выбирается браузером.

Второе: указание размеров обязательно. Без них браузер не знает, сколько места зарезервировать, и разметка прыгает после загрузки изображения.

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

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

Пятое: у элемента видео есть собственный атрибут предзагрузки. Значение "нет" откладывает загрузку до нажатия пользователя.

<video controls preload="none" poster="/media/poster.jpg" width="640" height="360">
  <source src="/media/overview.mp4" type="video/mp4" />
</video>

Кадр-заставка показывается сразу, а сам файл подтягивается только после нажатия.

Сценарий применяется, когда нужен контроль

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

Для этого применяется средство наблюдения за пересечением. Оно асинхронно сообщает, когда элемент приближается к области просмотра или покидает её.

function lazyLoad(selector, callback) {
  const observer = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) {
          continue;
        }
        callback(entry.target);
        observer.unobserve(entry.target);
      }
    },
    { rootMargin: "200px 0px" },
  );

  document.querySelectorAll(selector).forEach((element) => observer.observe(element));
  return observer;
}

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

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

Второй приём: один наблюдатель на всю страницу вместо отдельного на каждый элемент. Создание сотни наблюдателей заметно на слабых устройствах.

Третий приём: загрузка через промежуточный атрибут. Разметка содержит адрес в отдельном поле, а сценарий переносит его в основной атрибут.

<img
  data-src="/catalog/table.jpg"
  src="/preview/table-small.jpg"
  width="800"
  height="600"
  loading="lazy"
  alt="Обеденный стол"
/>

Четвёртый приём: обработка ошибок. Если файл не загрузился, стоит показать запасное изображение, а не пустое место.

lazyLoad("[data-src]", async (element) => {
  const source = element.dataset.src;
  try {
    await new Promise((resolve, reject) => {
      const probe = new Image();
      probe.onload = resolve;
      probe.onerror = reject;
      probe.src = source;
    });
    element.src = source;
  } catch (error) {
    element.src = "/images/placeholder.svg";
  }
});

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

Порядок применения определяет результат

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

Первый уровень это изображение в первом экране. Загружается сразу, с приоритетом.

Второй уровень это стили и основной сценарий. Загружаются сразу, потому что без них страница не работает.

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

Четвёртый уровень это встроенные рамки и карты. Откладываются, часто с заменой на статичную картинку.

Пятый уровень это второстепенные сценарии: виджеты поддержки, системы аналитики, декоративные библиотеки. Загружаются после того, как страница стала работоспособной.

// отложенная загрузка виджета поддержки после простоя
function loadOnIdle(url) {
  const start = () => {
    const script = document.createElement("script");
    script.src = url;
    script.defer = true;
    document.head.appendChild(script);
  };

  if ("requestIdleCallback" in window) {
    requestIdleCallback(start, { timeout: 3000 });
    return;
  }

  window.addEventListener("load", start);
}

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

Ошибки сводят пользу к нулю и ухудшают картину

Разберём типичные провалы.

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

Вторая ошибка это отсутствие размеров. Разметка прыгает, и человек промахивается по ссылкам.

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

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

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

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

Числовой пример показывает эффект

Полезно оценить выигрыш в цифрах.

Страница каталога содержит двести изображений по двести килобайт каждое. Общий вес составляет сорок мегабайт.

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

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

Разница во времени до первой отрисовки измеряется секундами на медленном соединении. На быстром канале выигрыш меньше, но всё равно заметен.

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

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

Четвёртый расчёт это нагрузка на сервер. Меньше запросов к невидимым изображениям означает меньше трафика и меньше обращений к диску.

Проверка результата требует замеров

Пятая часть работы это измерение. Без цифр невозможно понять, помогло ли изменение.

Первое: замер времени до появления главного элемента. Сравнение до и после.

Второе: объём загруженных данных при первом открытии. Он должен сократиться.

Третье: поведение при прокрутке. Изображения должны появляться плавно, без рывков.

Четвёртое: проверка на медленном соединении. Именно там видны проблемы с заглушками и прыжками разметки.

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

Шестое: проверка отключённого сценария. Если при этом изображения не показываются, значит, встроенный механизм не используется, и это стоит исправить.

Отдельно про выбор формата и размера

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

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

Второе: правильные размеры. Отдавать изображение шириной в четыре тысячи пикселей для показа в шестьсот бессмысленно. Уменьшенная копия весит в разы меньше.

Третье: адаптивные источники. Атрибут набора источников позволяет браузеру выбрать подходящий файл под ширину экрана и плотность пикселей.

<img
  src="/catalog/chair-800.jpg" srcset="/catalog/chair-400.jpg 400w, /catalog/chair-800.jpg 800w, /catalog/chair-1600.jpg 1600w"
  sizes="(max-width: 640px) 90vw, 30vw"
  width="800"
  height="600"
  loading="lazy"
  alt="Стул с деревянными подлокотниками"
/>

Четвёртое: удаление метаданных. Снимки с камеры содержат сведения об устройстве и координаты съёмки. Для публичных страниц они не нужны и увеличивают вес.

Пятое: разумное качество. Разница между качеством девяносто пять и восемьдесят пять незаметна на экране, а вес отличается заметно.

Сопоставление с полной загрузкой проясняет границы

Полезно понять, когда приём уместен.

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

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

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

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

Короткая проверка перед выпуском

Первое: главное изображение не помечено как отложенное.

Второе: у всех изображений указаны размеры.

Третье: встроенный атрибут используется везде, где достаточно его возможностей.

Четвёртое: заглушки имеют малый вес и не выглядят пустыми прямоугольниками.

Пятое: проверено поведение при отключённом сценарии.

Шестое: замерено время до появления главного элемента до и после изменения.

Седьмое: проверено на медленном соединении и на мобильном устройстве.

Семь проверок занимают час. Пропуск первой из них делает всю работу бессмысленной: страница станет быстрее везде, но не там, где её оценивают.

Разбор случая из практики показывает типичный сценарий

Интернет-магазин мебели жаловался на медленное открытие категории. Страница содержала двести сорок карточек с фотографиями.

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

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

Время до первой отрисовки сократилось более чем вдвое. Объём данных при первом открытии уменьшился в несколько раз.

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

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

Что стоит помнить про доступность

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

Первое: описания обязательны. Изображение без описания не отличается от декоративного, и это влияет на восприятие.

Второе: порядок фокуса сохраняется. Отложенная загрузка не изменяет разметку, поэтому обход с клавиатуры работает как обычно.

Третье: движение не навязывается. Автоматически проигрывающиеся видео отключаются, если пользователь просил уменьшить анимацию.

Четвёртое: заглушка не содержит текста, который дублирует описание.

Пятое: при отказе загрузки показывается осмысленное сообщение, а не пустое место.

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