Олег Марков
Ленивая загрузка lazy loading - подходы и практические примеры
Введение
Ленивая загрузка (lazy-loading) — это подход, при котором ресурс загружается не сразу, а только тогда, когда он действительно нужен. Например, вы прокручиваете страницу вниз, и изображения подгружаются по мере прокрутки, а не все сразу при первом открытии страницы.
С точки зрения производительности это один из самых эффективных и относительно простых приемов. Вы снижаете:
- время первого отображения страницы;
- объем загружаемых данных;
- нагрузку на сервер и браузер.
При этом вы получаете более отзывчивый интерфейс, особенно на мобильных устройствах и при медленном интернете.
В этой статье мы разберем разные варианты lazy-loading:
- изображения и iframe в браузере;
- компоненты и модули в фронтенд‑фреймворках;
- данные на стороне сервера;
- ленивую инициализацию объектов и ресурсов.
Покажу вам реальные примеры кода и объясню, почему та или иная техника работает именно так.
Что такое ленивая загрузка и зачем она нужна
Основная идея lazy-loading
Идея ленивой загрузки проста:
- Не загружать ресурс заранее.
- Отложить загрузку до момента, когда:
- ресурс попадает в зону видимости (например, в viewport);
- пользователь выполняет какое‑то действие (клик, наведение курсора, переход по маршруту);
- код действительно обращается к данным или объекту.
Смотрите, можно сравнить это с книгой. Вы покупаете книгу целиком, но читаете ее по главам. Вы не держите все главы открытыми на столе, а открываете только ту, которая нужна прямо сейчас. Lazy-loading делает то же самое с ресурсами приложения.
Где ленивую загрузку применяют чаще всего
Чтобы вам было проще ориентироваться, перечислю типичные случаи:
- изображения и видео на длинных страницах (ленты, каталоги, блоги);
- iframe с картами, внешними виджетами и плеерами;
- тяжелые JavaScript-модули и компоненты SPA;
- данные из базы или стороннего API (подгрузка по скроллу, пагинация);
- инициализация сложных объектов (например, дорогостоящие вычисления, подключения к внешним системам).
Теперь давайте перейдем к конкретным примерам в браузере.
Ленивая загрузка изображений и iframe в браузере
Самый простой способ — атрибут loading
Современные браузеры поддерживают атрибут loading у тегов img и iframe. Он встроен прямо в HTML, без JavaScript.
Пример:
<!-- Изображение будет загружено только при приближении к области видимости -->
<img
src="photo-large.jpg"
alt="Горы на рассвете"
loading="lazy"
/>
<!-- Встроенный iframe с картой подгружается лениво -->
<iframe
src="https://maps.example.com/embed/123"
loading="lazy"
/>
Комментарии по примеру:
loading="lazy"говорит браузеру отложить загрузку до тех пор, пока элемент не окажется рядом с viewport (обычно чуть заранее, чтобы не было задержки отображения).- Если атрибут не указан, используется поведение по умолчанию браузера (часто
eager— загружать сразу).
Есть еще два значения:
eager— загружать сразу;auto— дать браузеру решить самостоятельно.
Ограничения атрибута loading
Обратите внимание на несколько важных нюансов:
- Поддержка в современных браузерах хорошая, но в очень старых версий может не быть.
- Атрибут работает только для
imgиiframe. Для других типов ресурсов понадобятся другие техники. - Вы меньше контролируете «точку» загрузки: браузер сам определяет, когда начать подгружать ресурс.
Если вам нужен более точный контроль, подключается JavaScript и IntersectionObserver.
Ленивая загрузка с помощью IntersectionObserver
Как работает IntersectionObserver
IntersectionObserver — это API браузера, которое позволяет отслеживать, когда элемент пересекается с областью видимости (viewport) или с другим контейнером.
Логика простая:
- На страницы изначально создаются «заглушки» вместо реальных изображений.
- JavaScript начинает наблюдать за этими элементами.
- Когда элемент попадает в viewport, скрипт меняет атрибуты, и браузер загружает реальный ресурс.
Подготовка HTML для ленивой загрузки
Смотрите, сначала делаем разметку:
<!-- Используем data-src вместо src -->
<img
data-src="photo-large-1.jpg"
alt="Чайное поле"
class="lazy"
/>
<img
data-src="photo-large-2.jpg"
alt="Город ночью"
class="lazy"
/>
Комментарии:
data-srcхранит реальный URL изображения.- Атрибут
srcмы пока не задаем, чтобы браузер не начал загрузку сразу. - Класс
lazyнужен, чтобы мы легко нашли все такие элементы в скрипте.
Реализация ленивой загрузки на JavaScript через IntersectionObserver
Теперь вы увидите, как это выглядит в коде:
// Находим все элементы, которые нужно загружать лениво
const lazyImages = document.querySelectorAll('img.lazy');
// Проверяем, поддерживает ли браузер IntersectionObserver
if ('IntersectionObserver' in window) {
// Создаем наблюдатель
const observer = new IntersectionObserver((entries, obs) => {
// entries - список наблюдаемых элементов и их состояний
entries.forEach(entry => {
// Если элемент пересекся с зоной видимости
if (entry.isIntersecting) {
const img = entry.target;
// Берем реальный URL изображения из data-src
const realSrc = img.getAttribute('data-src');
if (realSrc) {
img.src = realSrc; // Запускаем загрузку изображения
img.removeAttribute('data-src'); // Очищаем data-src, он уже не нужен
}
// Перестаем наблюдать за этим элементом
obs.unobserve(img);
}
});
}, {
root: null, // Следим за пересечением с viewport
rootMargin: '200px', // Начинаем загрузку чуть заранее (200px до входа в экран)
threshold: 0.01 // Достаточно пересечения хотя бы 1% элемента
});
// Регистрируем все изображения в наблюдателе
lazyImages.forEach(img => observer.observe(img));
} else {
// Фолбэк для старых браузеров - загружаем сразу
lazyImages.forEach(img => {
const realSrc = img.getAttribute('data-src');
if (realSrc) {
img.src = realSrc;
img.removeAttribute('data-src');
}
});
}
На что стоит обратить внимание:
rootMargin: '200px'— это «запас» до края экрана. Изображение начнет загружаться за 200 пикселей до появления в viewport, чтобы к моменту прокрутки оно уже было готово.- В колбэке мы всегда снимаем наблюдение
obs.unobserve(img)— так вы не тратите ресурсы на уже загруженные элементы. - В ветке else скрипт ведет себя как обычная загрузка, без ленивости, но сайт останется рабочим даже в старых браузерах.
Ленивая загрузка компонентов и модулей во фронтенде
Ленивая загрузка касается не только картинок. Очень часто основной «тяжелый» вес — это JavaScript‑код: библиотеки, компоненты, роуты. Здесь вы можете выигрывать десятки и сотни килобайт при первом открытии страницы.
Ниже посмотрим на разные варианты: нативный JavaScript, React и пример с роутингом.
Динамический импорт модулей в JavaScript
Современный JavaScript поддерживает динамический импорт через функцию import().
Давайте разберемся на примере:
// Допустим, у нас есть тяжелый модуль charts.js
// В нем — код для построения сложных графиков
// Мы не хотим грузить этот модуль, пока пользователь не нажмет кнопку "Показать график"
const button = document.getElementById('show-chart');
button.addEventListener('click', async () => {
// Динамически импортируем модуль по клику
const module = await import('./charts.js');
// Предположим, в модуле экспортируется функция renderChart
module.renderChart('#chart-container'); // Рисуем график только по запросу пользователя
});
Комментарии:
import('./charts.js')возвращает промис. Код из файла будет загружен только при первом вызовеimport.- Бандлеры (Webpack, Vite, Rollup) создают отдельный чанк для этого модуля. Браузер скачивает его по мере необходимости.
- Это классический пример lazy-loading логики, завязанной на действия пользователя.
Ленивая загрузка компонентов в React (React.lazy)
В React есть отдельный механизм для ленивой загрузки компонентов — React.lazy и Suspense.
Посмотрите на пример:
// App.jsx
import React, { Suspense } from 'react';
// Ленивая загрузка компонента
// Компонент HeavyComponent не войдет в основной бандл
const HeavyComponent = React.lazy(() => import('./HeavyComponent.jsx'));
function App() {
return (
<div>
<h1>Главная страница</h1>
{/* Suspense показывает запасной контент, пока компонент загружается */}
<Suspense fallback={<div>Загрузка компонента...</div>}>
<HeavyComponent /> {/* Этот компонент загрузится лениво */}
</Suspense>
</div>
);
}
export default App;
Ключевые моменты:
React.lazy(() => import('./HeavyComponent.jsx'))— здесь React подключает ленивую загрузку. Компонент окажется в отдельном чанке.- Блок
Suspenseпоказывает «заглушку», пока идет загрузка. Как только загрузка завершена, рендерится реальный компонент. - Вы можете использовать React.lazy и на уровне роутов, и внутри сложных страниц, чтобы не грузить редкие сценарии раньше времени.
Ленивая загрузка роутов в SPA
В одностраничных приложениях (SPA) имеет смысл подгружать страницы (роуты) только тогда, когда пользователь на них переходит.
Условный пример на React Router v6:
// routes.jsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Ленивая загрузка страниц
const HomePage = React.lazy(() => import('./pages/HomePage.jsx'));
const AdminPage = React.lazy(() => import('./pages/AdminPage.jsx'));
function AppRoutes() {
return (
<BrowserRouter>
<Suspense fallback={<div>Загрузка страницы...</div>}>
<Routes>
{/* Главная страница */}
<Route path="/" element={<HomePage />} />
{/* Админка грузится только при переходе по /admin */}
<Route path="/admin" element={<AdminPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default AppRoutes;
Как видите, идея та же:
- вы не включаете все страницы в стартовый бандл;
- каждая страница подгружается при первом переходе на нее;
- это особенно полезно для «редких» разделов типа статистики, настроек, админки.
Ленивая загрузка данных с сервера
Ленивая загрузка — это не только про файлы, но и про данные. Очень часто интерфейс устроен так, что пользователю не нужны все данные сразу.
Типичные примеры:
- бесконечная лента (infinite scroll);
- постраничная навигация (пагинация);
- вкладки, которые подгружают данные только при активации.
Ленивая подгрузка по скроллу (infinite scroll)
Представьте ленту новостей. Вместо того, чтобы отдавать сразу 500 записей, сервер возвращает первые 20. Остальные подгружаются при прокрутке.
Покажу вам упрощенный пример фронтенда:
let page = 1; // Текущая страница данных
let isLoading = false; // Флаг, чтобы не запускать несколько загрузок одновременно
const container = document.getElementById('posts');
// Функция для загрузки данных с сервера
async function loadMore() {
if (isLoading) return; // Если уже идет загрузка - выходим
isLoading = true;
try {
// Запрашиваем данные следующей страницы
const response = await fetch(`/api/posts?page=${page}`);
const data = await response.json();
// Рендерим посты в контейнер
data.posts.forEach(post => {
const div = document.createElement('div');
div.className = 'post';
div.textContent = post.title; // В реальном коде вы бы собрали шаблон красивее
container.appendChild(div);
});
// Увеличиваем номер страницы, если есть еще данные
if (data.hasMore) {
page += 1;
} else {
// Если данных больше нет - отписываемся от события
window.removeEventListener('scroll', onScroll);
}
} finally {
isLoading = false;
}
}
// Обработчик скролла
function onScroll() {
const scrollPosition = window.innerHeight + window.scrollY;
const threshold = document.body.offsetHeight - 500; // Запас 500px до конца страницы
// Когда пользователь приблизился к низу страницы - подгружаем данные
if (scrollPosition >= threshold) {
loadMore();
}
}
// Подписываемся на скролл
window.addEventListener('scroll', onScroll);
// Загружаем первую порцию данных при старте
loadMore();
Комментарии по коду:
isLoadingзащищает от одновременных запросов, когда пользователь быстро прокручивает страницу.- Сервер должен возвращать флаг
hasMore, чтобы клиент знал, есть ли еще данные. - Такой подход снижает нагрузку на сервер и ускоряет первый рендер.
Ленивая загрузка по вкладкам (tabs)
Еще один распространенный вариант — вы не загружаете данные для всех вкладок сразу.
Пример:
const tabs = document.querySelectorAll('[data-tab]');
const content = document.getElementById('tab-content');
let loadedTabs = new Set(); // Здесь храним вкладки, которые уже загрузили
tabs.forEach(tab => {
tab.addEventListener('click', async () => {
const tabName = tab.getAttribute('data-tab');
// Если данные для этой вкладки уже загружены - просто показываем
if (loadedTabs.has(tabName)) {
showTab(tabName);
return;
}
// Иначе запрашиваем данные у сервера
const response = await fetch(`/api/tab/${tabName}`);
const html = await response.text();
// Вставляем загруженный HTML в контейнер
const tabDiv = document.createElement('div');
tabDiv.id = `tab-${tabName}`;
tabDiv.innerHTML = html;
content.appendChild(tabDiv);
loadedTabs.add(tabName); // Помечаем вкладку как загруженную
showTab(tabName); // Показываем вкладку пользователю
});
});
function showTab(name) {
// Скрываем все вкладки
content.querySelectorAll('div[id^="tab-"]').forEach(div => {
div.style.display = 'none';
});
// Показываем нужную
const active = document.getElementById(`tab-${name}`);
if (active) {
active.style.display = 'block';
}
}
Как видите, принцип все тот же: «загрузить только тогда, когда вкладка стала актуальной для пользователя».
Ленивая инициализация объектов и ресурсов
Ленивая загрузка — это частный случай более общей идеи: ленивой инициализации. Речь о том, что вы не создаете объект или не выполняете тяжелую операцию, пока она действительно не понадобится.
Ленивое создание объекта
Пример на JavaScript:
// Допустим, нам нужна дорогая инициализация - например, подключение к внешней библиотеке
let analyticsClient = null; // Здесь будет лежать экземпляр клиента
function getAnalyticsClient() {
// Если клиент уже создан - возвращаем его
if (analyticsClient) {
return analyticsClient;
}
// Иначе создаем новый экземпляр
// Представим, что это дорогая операция
analyticsClient = {
sendEvent(eventName, data) {
// Здесь могла бы быть реальная логика отправки события
console.log('Отправка события', eventName, data);
}
};
return analyticsClient;
}
// В коде вы просто вызываете getAnalyticsClient тогда, когда нужно
function onUserAction() {
const client = getAnalyticsClient(); // Ленивая инициализация
client.sendEvent('user_action', { time: Date.now() });
}
Комментарии:
- Такой подход полезен, когда вы не уверены, что функциональность вообще будет использована в текущей сессии.
- Вы распределяете нагрузку: вместо одного тяжелого старта приложения у вас несколько небольших операций по мере работы.
Важные нюансы и подводные камни lazy-loading
Кумулятивный сдвиг верстки (CLS)
Если вы лениво загружаете изображения, легко столкнуться с проблемой: страница «прыгает», когда картинки загружаются. Это ухудшает пользовательский опыт и показатели Core Web Vitals (метрика CLS).
Чтобы этого избежать, нужно резервировать место под изображение заранее.
Пример с указанием размеров:
<!-- Мы указываем ширину и высоту, чтобы браузер мог зарезервировать место -->
<img
src="placeholder.jpg" <!-- Легкий placeholder низкого качества -->
data-src="photo-large.jpg"
alt="Пример"
width="800" <!-- Исходная ширина -->
height="600" <!-- Исходная высота -->
class="lazy"
/>
И стили:
img.lazy {
display: block;
width: 100%; /* Масштабируем по ширине контейнера */
height: auto; /* Высота посчитается по соотношению сторон */
}
Комментарии:
- Атрибуты
widthиheightпозволяют браузеру вычислить пропорции и зарезервировать нужное пространство до загрузки реального изображения. - Можно использовать CSS‑контейнер с фиксированным аспект‑рейшо (через
aspect-ratio), если это удобнее.
SEO и индексирование контента
Поисковые системы в целом научились работать с ленивой загрузкой, но есть моменты:
- Важно, чтобы основной контент был доступен без сложных действий (например, без авторизации или нестандартных JS‑трюков).
- Для SEO‑критичных изображений (например, главная картинка статьи) ленивую загрузку иногда отключают, чтобы гарантировать их индексирование и быстрое появление в поиске.
Пример: вы можете использовать loading="eager" или вообще не ставить атрибут loading для ключевых изображений.
Баланс между lazy и eager
Полностью «переложить» все на ленивую загрузку не всегда хорошая идея. Важно найти баланс:
- Ключевой контент страницы (first screen, заголовки, важные кнопки) лучше загружать без задержки.
- Все второстепенное (ниже первого экрана, редкие страницы, дополнительные виджеты) — идеальные кандидаты для lazy-loading.
Практические рекомендации по внедрению lazy-loading
1. Начинайте с самых «тяжелых» элементов
Обычно это:
- большие изображения (галереи, карточки товаров);
- сторонние виджеты и iframe (карты, видео, комментарии);
- браузерные JS‑чанки (админка, сложные отчеты, аналитика).
Смотрите, вы можете постепенно включать ленивую загрузку, не пытаясь сразу переделать все приложение.
2. Комбинируйте встроенные возможности и JavaScript
Хороший практический подход:
- Сначала используйте
loading="lazy"дляimgиiframeтам, где это подходит. - Для сложных сценариев (тонкий контроль, менее стандартные элементы) применяйте
IntersectionObserver.
Так вы получите хороший результат с меньшими усилиями.
3. Обязательно продумывайте фолбэки
Некоторые пользователи могут работать:
- в старых браузерах;
- с отключенным JavaScript.
Для них:
- лениво загружаемый контент должен оставаться доступным хотя бы в простой форме;
- не стоит полагаться только на JavaScript, если речь идет о критичном контенте.
4. Тестируйте производительность и UX
После внедрения lazy-loading полезно проверить:
- время до первого рендера страницы;
- объем загруженных данных;
- отсутствие визуальных «прыжков»;
- корректность подгрузки при быстром/медленном скролле.
Здесь помогут:
- инструменты разработчика в браузере (вкладка Network, Performance);
- Lighthouse и WebPageTest;
- DevTools в мобильно‑эмуляторном режиме (задержка сети, CPU throttling).
Заключение
Ленивая загрузка — это простой с концептуальной точки зрения, но очень мощный инструмент оптимизации. Вместо того чтобы загружать все ресурсы и данные сразу, вы загружаете только то, что нужно прямо сейчас, а остальное откладываете.
Вы увидели, как реализовать lazy-loading:
- для изображений и iframe с помощью
loading="lazy"иIntersectionObserver; - для модулей и компонентов в JavaScript и React через динамический импорт и
React.lazy; - для данных с сервера через пагинацию, infinite scroll и подгрузку по вкладкам;
- для инициализации объектов, когда создание переносится до первого реального использования.
Главная идея — не усложнять архитектуру без необходимости и всегда держать баланс между удобством пользователя, скоростью разработки и производительностью. Там, где можно использовать встроенные механизмы, лучше опираться на них. Там, где нужна тонкая настройка, у вас есть JavaScript‑инструменты и API браузера.
Частозадаваемые технические вопросы по теме статьи
Как лениво загружать фоновые изображения в CSS
Фоновые изображения нельзя просто пометить loading="lazy", потому что это не HTML‑тег. Один из подходов — использовать data-атрибуты и менять стиль через JavaScript, когда элемент попадает в viewport.
Пример:
<div
class="banner lazy-bg"
data-bg="banner-large.jpg"
></div>
const lazyBlocks = document.querySelectorAll('.lazy-bg');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
const url = el.getAttribute('data-bg');
if (url) {
// Устанавливаем фон, запускаем загрузку
el.style.backgroundImage = `url("${url}")`;
el.removeAttribute('data-bg');
}
observer.unobserve(el);
}
});
});
lazyBlocks.forEach(el => observer.observe(el));
Как избежать двойной загрузки изображений при сочетании loading="lazy" и JavaScript
Если вы добавите и loading="lazy", и скрипт, который сам управляет src, браузер может загрузить изображение дважды. Чтобы этого избежать:
- Либо используйте только один подход (предпочтительно
loading="lazy"для простых случаев). - Либо уберите
srcи работайте только черезdata-srcи JavaScript, как в примерах выше. - Не сочетайте автоматический lazy у браузера и собственную логику без необходимости.
Как делать ленивую загрузку модулей в TypeScript с типами
В TypeScript динамический импорт работает так же, как в JS, но вы можете дополнительно типизировать загружаемый модуль:
// Описываем тип модуля
type ChartsModule = {
renderChart: (selector: string) => void;
};
async function loadChartsModule(): Promise<ChartsModule> {
const module = await import('./charts'); // charts.ts или charts.tsx
return module as ChartsModule; // Явное приведение типов
}
async function onClick() {
const charts = await loadChartsModule();
charts.renderChart('#chart');
}
Как лениво грузить большие JSON данные без блокировки UI
Если JSON очень большой, то даже его парсинг может подвесить интерфейс. Для таких случаев:
- Используйте потоковый парсинг на сервере и отдачу данных порциями (chunked transfer).
- На клиенте обрабатывайте данные частями с помощью
requestIdleCallbackилиsetTimeout, разбивая тяжелую обработку на шаги. - Если возможно, отдавайте не один огромный JSON, а API с пагинацией или курсором, чтобы загружать данные лениво по потребности.
Как тестировать корректность ленивой загрузки в автотестах
Чтобы убедиться в работе lazy-loading в тестах (например, Cypress или Playwright):
- Отключите JavaScript‑скролл и явно прокручивайте страницу в тесте до нужных координат.
- После скролла проверяйте:
- что
srcу изображения изменился сdata-srcна реальный URL; - что соответствующие сети‑запросы появились (через перехват запросов в тестовом фреймворке).
- что
- Для модулей/компонентов проверяйте, что соответствующий бандл не загружается до тех пор, пока вы не выполните действие (клик/переход по маршруту), запускающее lazy-loading.