Страница каталога содержит двести карточек товара с фотографиями. При открытии браузер начинает тянуть всё сразу, и первая отрисовка ждёт, пока загрузятся десятки мегабайт. Пользователь видит белый экран несколько секунд и уходит. Задача решается отложенной загрузкой, но у неё есть границы, за которыми она начинает вредить.
Объём ресурсов вырос за десятилетие в несколько раз
Проблема имеет измеримые масштабы: за 2011-2019 средний вес десктопной страницы вырос с примерно семисот пятидесяти килобайт до почти двух мегабайт, мобильные страницы догнали десктопные. Изображения составляют крупнейшую долю веса: порядка девятисот килобайт на десктопной странице к концу периода. Медиа стали основной частью загрузки. Любая работа над скоростью начинается с них.
Приём отложенной загрузки известен давно: смысл - отложить инициализацию ресурса до момента, когда он понадобится.
Браузер умеет откладывать загрузку без сценариев
Самый простой способ не требует ни строчки кода. Достаточно указать в разметке соответствующее значение атрибута.
<img
src="/catalog/chair.jpg"
width="800"
height="600"
loading="lazy"
alt="Стул с деревянными подлокотниками"
/>
Браузер сам определяет, когда изображение приближается к экрану, и начинает загрузку. Расстояние запаса выбирается браузером. Указание размеров обязательно - без них разметка прыгает после загрузки. Ленивая загрузка не применяется к изображениям в первом экране - иначе падает скорость отрисовки, там указывают приоритетную загрузку. Значение работает и для iframe - для них приём полезен, так как каждая рамка - отдельный документ. У video есть атрибут preload="none", откладывающий загрузку до нажатия.
<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;
}
Запас в двести пикселей означает, что загрузка начинается чуть раньше, чем элемент появится на экране. Это скрывает задержку от пользователя.
Первый приём: после срабатывания наблюдение прекращается - лишние вызовы нагружают браузер. Второй: один наблюдатель на всю страницу вместо сотни отдельных. Третий: загрузка через data-атрибут - адрес в отдельном поле, сценарий переносит в src.
<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";
}
});
Проверка через отдельный Image позволяет загрузить файл в кэш до подмены src. Пользователь не увидит момент загрузки.
Порядок применения определяет результат
Третья часть работы - последовательность. Откладывать нужно не всё подряд, а по приоритетам.
Первый уровень - изображение в первом экране. Загружается сразу, с приоритетом.
Второй уровень - стили и основной сценарий. Загружаются сразу, потому что без них страница не работает.
Третий уровень - изображения ниже первого экрана. Откладываются.
Четвёртый уровень - встроенные рамки и карты. Откладываются, часто с заменой на статичную картинку.
Пятый уровень - второстепенные сценарии: виджеты поддержки, системы аналитики, декоративные библиотеки. Загружаются после того, как страница стала работоспособной.
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. Если сценарий не выполнился, изображения не появятся вовсе. Встроенный атрибут в этом смысле надёжнее, потому что работает без сценария.
Шестая ошибка - использование приёма там, где он не нужен. Для страницы с двумя изображениями выигрыш нулевой, а сложность растёт.
Числовой пример показывает эффект
Полезно оценить выигрыш в цифрах.
Страница каталога содержит двести изображений по двести килобайт каждое. Общий вес составляет сорок мегабайт.
При обычной загрузке браузер тянет всё одновременно, канал ограничен, и время зависит от пропускной способности и очереди. При отложенной загрузке в первом экране видно шесть изображений, то есть 1,2 МБ вместо сорока, остальное подтягивается по прокрутке. Разница во времени до первой отрисовки измеряется секундами на медленном канале. На мобильных пользователь часто уходит, не долистав до конца, и загружать всё заранее значит тратить трафик зря. Память: сотня полноразмерных фото занимает десятки мегабайт, на слабом телефоне вкладку перезагружает система. Серверу достаётся меньше запросов к невидимым изображениям, меньше трафика и дисковых операций.
Проверка результата требует замеров
Пятая часть работы - измерение. Без цифр невозможно понять, помогло ли изменение.
Первое: замер времени до появления главного элемента. Сравнение до и после.
Второе: объём загруженных данных при первом открытии. Он должен сократиться.
Третье: поведение при прокрутке. Изображения должны появляться плавно, без рывков.
Четвёртое: проверка на медленном соединении - там видны проблемы с заглушками и прыжками. Пятое: проверка на мобильном - эмуляция не воспроизводит слабый процессор и сенсорный ввод. Шестое: проверка отключённого JS - если изображения не показываются, встроенный механизм не используется, исправьте.
Отдельно про выбор формата и размера
Отложенная загрузка работает вместе с оптимизацией самих файлов, и без неё выигрыш меньше.
Первое: правильный формат. Современные форматы сжатия дают выигрыш в полтора-два раза по сравнению с устаревшими при том же визуальном качестве.
Второе: правильные размеры - отдавать 4000px для показа 600px бессмысленно, уменьшенная копия весит в разы меньше. Третье: адаптивные источники - srcset позволяет браузеру выбрать файл под экран и плотность.
<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="Стул с деревянными подлокотниками"
/>
Четвёртое: удаление метаданных. Снимки с камеры содержат сведения об устройстве и координаты съёмки. Для публичных страниц они не нужны и увеличивают вес.
Пятое: разумное качество - разница между 95 и 85 незаметна, а вес отличается заметно.
Сопоставление с полной загрузкой проясняет границы
Полезно понять, когда приём уместен.
Полная загрузка подходит для страницы с единственным изображением или для раздела, где пользователь почти наверняка доходит до низа.
Отложенная загрузка подходит для длинных списков, галерей, лент, страниц с iframe. Смешанный подход чаще: первый экран сразу, остальное откладывается. Граница - по вероятности просмотра: видят почти все - не откладываем, доходит меньшинство - откладываем.
Короткая проверка перед выпуском
Первое: главное изображение не помечено как отложенное.
Второе: у всех изображений указаны размеры.
Третье: встроенный атрибут используется везде, где достаточно его возможностей.
Четвёртое: заглушки имеют малый вес и не выглядят пустыми прямоугольниками.
Пятое: проверено поведение при отключённом сценарии.
Шестое: замерено время до появления главного элемента до и после изменения.
Седьмое: проверено на медленном соединении и на мобильном устройстве.
Семь проверок занимают час. Пропуск первой делает всю работу бессмысленной: страница станет быстрее везде, но не там, где её оценивают.
Разбор случая из практики показывает типичный сценарий
Интернет-магазин мебели жаловался на медленное открытие категории. Страница содержала двести сорок карточек с фотографиями.
Замер показал, что до первой отрисовки проходило около четырёх секунд на среднем мобильном соединении. Основную часть времени занимала загрузка изображений, которые браузер начинал тянуть сразу.
Изменения по шагам: всем фото ниже экрана - lazy loading и размеры, формат на современный, качество до 85, первые 6 карточек - приоритетная загрузка. Время до первой отрисовки сократилось более чем вдвое, объём данных - в несколько раз. При быстрой прокрутке фото не успевали - запас наблюдения увеличили до 300px, проблема исчезла. Вывод: lazy loading даёт выигрыш в сочетании с оптимизацией файлов, по отдельности помогает меньше.
Что стоит помнить про доступность
Отложенная загрузка не должна ухудшать работу вспомогательных технологий.
Первое: описания обязательны. Изображение без описания не отличается от декоративного, и это влияет на восприятие.
Второе: порядок фокуса сохраняется. Отложенная загрузка не изменяет разметку, поэтому обход с клавиатуры работает как обычно.
Третье: движение не навязывается - автовоспроизведение отключается, если пользователь просил уменьшить анимацию. Четвёртое: заглушка не дублирует описание. Пятое: при отказе загрузки - осмысленное сообщение, а не пустое место.
Отложенная загрузка - приём с простым ядром и многими деталями. Указать атрибут может каждый, не испортить первый экран, размеры и прокрутку - сложнее. Там, где приоритеты верны, страница открывается быстро и плавна до низа. Там, где применён ко всему подряд - прыгающая разметка и серые прямоугольники.
Дополнительно стоит учесть влияние на Core Web Vitals. LCP (Largest Contentful Paint) улучшается, когда главное изображение загружается с приоритетом high, а остальные не блокируют поток. CLS (Cumulative Layout Shift) снижается, если для всех lazy-изображений заданы width и height - браузер резервирует место до загрузки. INP (Interaction to Next Paint) может улучшиться за счёт освобождения главного потока от лишней работы по декодированию невидимых изображений. Замеры в полевых условиях показывают, что грамотная отложенная загрузка вместе с оптимизацией форматов даёт прирост LCP на 20-40% на медиа-насыщенных страницах.
Отдельно стоит упомянуть атрибут fetchpriority для критически важных ресурсов. Значение high заставляет браузер загрузить ресурс с наивысшим приоритетом, а low - снизить приоритет для фоновых элементов. Комбинация loading="lazy" для контента ниже экрана и fetchpriority="high" для главного изображения даёт максимальный выигрыш в LCP. На практике это выглядит так: главной фотографии товара задают fetchpriority="high" и loading="eager" (или опускают loading), а остальным карточкам - loading="lazy". Такой подход работает в современных браузерах без сценариев и не требует IntersectionObserver.