Олег Марков
Ленивая загрузка lazy loading - полное практическое руководство
Введение
Ленивая загрузка (lazy-loading) — это подход, при котором ресурсы или данные загружаются не сразу, а только в момент, когда они действительно нужны. Вы не тратите время и сеть на то, что пользователь может так и не увидеть или не использовать.
В веб‑разработке это особенно важно. Страница может содержать десятки картинок, виджеты, большие JavaScript‑бандлы, данные для разных блоков интерфейса. Если загрузить все сразу, первая отрисовка страницы замедлится, время до взаимодействия (TTI) вырастет, а пользовательский опыт станет хуже.
Здесь вы разберетесь:
- какие виды ленивой загрузки бывают;
- как лениво загружать изображения, видео, компоненты и данные;
- какие есть стандартные средства браузера и когда нужен JavaScript;
- как реализовать lazy-loading в SPA (React, Vue, Angular);
- какие подводные камни есть у этого подхода и как их обойти.
Я буду показывать примеры и обязательно сопровождать их комментариями в коде, чтобы вы могли шаг за шагом повторить все в своем проекте.
Что такое ленивая загрузка и зачем она нужна
Основной принцип ленивой загрузки
Идея проста: не грузить то, что не нужно прямо сейчас.
Смотрите, общий жизненный цикл ресурса при ленивой загрузке выглядит так:
- Браузер или приложение видит, что есть некий ресурс (картинка, модуль, данные).
- Вместо немедленной загрузки ресурс помечается как «отложенный».
- При наступлении определенного события (пользователь проскроллил, открыл вкладку, нажал кнопку, достиг определенного маршрута) выполняется реальная загрузка.
- После загрузки ресурс отображается или используется в коде.
Условно можно выделить два ключевых триггера:
- ленивость по видимости — когда элемент появляется в видимой области (viewport);
- ленивость по сценарию — когда код или данные нужны только для конкретного действия пользователя.
Преимущества lazy-loading
Почему стоит тратить время на внедрение ленивой загрузки:
- Ускорение первоначальной загрузки страницы.
- Снижение потребления трафика (особенно важно для мобильных).
- Снижение нагрузки на сервер в пиковые моменты.
- Лучшая оценка производительности в инструментах вроде Lighthouse, WebPageTest.
- Гибкость: вы сами решаете, какие части приложения критичны, а какие можно отложить.
Есть и оборотная сторона:
- усложнение кода и логики загрузки;
- риск «позднего появления» контента (всплывающие подгрузки) и сдвига верстки;
- возможные ошибки с SEO и индексированием, если сделать все неаккуратно.
Далее я покажу на конкретных примерах, как реализовать ленивую загрузку так, чтобы плюсов было больше, чем минусов.
Виды ленивой загрузки в вебе
Ленивая загрузка ресурсов интерфейса
К этой группе обычно относят:
- изображения и фоновые картинки;
- видео и аудио;
- iframe (карты, виджеты, YouTube);
- CSS и JavaScript‑бандлы.
Примеры задач
- Большой лендинг с галереей — не загружать картинки, пока пользователь не прокрутит до них.
- Страница с несколькими вкладками (табы) — загружать содержимое вкладок только при открытии.
- SPA‑приложение с маршрутизацией — подгружать код страниц по мере навигации.
Ленивая загрузка данных
Здесь речь о данных, а не о самих файлах:
- догрузка элементов списка при прокрутке (infinite scroll);
- получение подробностей по клику «Показать детали»;
- подгрузка графиков и статистики в скрытых блоках.
Логика та же: вы откладываете запросы к API и обработку ответа до момента, когда блок точно понадобится пользователю.
Ленивая загрузка на стороне сервера
Этот подход встречается реже в вебе с точки зрения «видимого» эффекта, но он тоже важен:
- ленивые связи в ORM (например, загрузка связанных сущностей только при обращении);
- отложенные вычисления (кеширование тяжелых расчетов по запросу);
- ленивое чтение файлов и потоков.
В этой статье мы сосредоточимся на фронтенд‑части, но иногда полезно помнить, что lazy‑loading может начинаться еще на сервере.
Ленивая загрузка изображений
Начнем с самого частого кейса — картинок. Именно они чаще всего составляют большую часть веса страницы.
Вариант 1 — нативный атрибут loading
Смотрите, современный способ реализовать ленивую загрузку изображений — использовать атрибут loading:
<!-- Картинка, которая загружается лениво -->
<img
src="photo-large.jpg"
alt="Пейзаж в горах"
loading="lazy"
/>
Комментарии к примеру:
- Атрибут loading="lazy" говорит браузеру отложить загрузку, пока изображение не приблизится к видимой области.
- Поведение контролируется самим браузером — это простой и безопасный способ.
- Для критических изображений (например, логотип в шапке) лучше явно ставить loading="eager" или не указывать атрибут.
Преимущество: вам не нужен JavaScript, поведение стабильно и поддерживается большинством современных браузеров.
Минусы:
- нет гибкой настройки порога срабатывания (на сколько пикселей заранее грузить);
- поведение немного отличается между браузерами;
- старые браузеры этот атрибут игнорируют (там потребуется fallback).
Вариант 2 — Intersection Observer
Если вам нужно больше контроля или вы хотите совместить ленивую загрузку с анимациями и сложной логикой, используйте Intersection Observer.
Пример: ленивое изменение атрибута data-src на src.
<!-- Здесь мы используем data-src для отложенной загрузки -->
<img
data-src="photo-large.jpg"
alt="Пейзаж в горах"
class="lazy-image"
/>
<script>
// Находим все картинки, которые должны загружаться лениво
const lazyImages = document.querySelectorAll('.lazy-image')
// Создаем наблюдатель за пересечением с viewport
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
// Проверяем - элемент действительно виден
if (entry.isIntersecting) {
const img = entry.target
// Берем реальный адрес картинки из data-src
const realSrc = img.getAttribute('data-src')
if (realSrc) {
img.src = realSrc // Запускаем реальную загрузку изображения
img.removeAttribute('data-src') // Очищаем data-src, чтобы не путать
}
// Перестаем наблюдать за картинкой - она уже загружена
obs.unobserve(img)
}
})
}, {
root: null, // Наблюдаем за видимостью относительно окна браузера
rootMargin: '200px 0px', // Начинаем загрузку немного заранее
threshold: 0.1 // Достаточно чтобы 10 процентов картинки попало в viewport
})
// Подключаем наблюдатель ко всем выбранным картинкам
lazyImages.forEach(img => observer.observe(img))
</script>
Обратите внимание на параметры:
- root: null — значит, наблюдаем относительно окна браузера;
- rootMargin: '200px 0px' — загружаем картинку заранее, когда до нее осталось 200 пикселей;
- threshold: 0.1 — достаточно появления 10% площади картинки.
Плюсы:
- точный контроль момента загрузки;
- возможность комбинировать с анимациями или эффектами;
- можно использовать с любыми элементами, а не только с img.
Минусы:
- немного больше кода;
- нужен fallback для очень старых браузеров (если это важно для проекта).
Ленивая загрузка фоновых изображений
Фоны через CSS нельзя просто так пометить атрибутом loading, но вы можете использовать тот же подход с Intersection Observer и data‑атрибутом.
<!-- Блок с отложенной фоновой картинкой -->
<div
class="card lazy-bg"
data-bg="background-large.jpg"
>
Контент карточки
</div>
<script>
// Ищем элементы с ленивыми фонами
const lazyBackgrounds = document.querySelectorAll('.lazy-bg')
const bgObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target
const url = el.getAttribute('data-bg')
if (url) {
// Устанавливаем фоновое изображение через inline-стиль
el.style.backgroundImage = `url("${url}")`
el.removeAttribute('data-bg')
}
obs.unobserve(el)
}
})
})
// Подключаем наблюдатель ко всем элементам с ленивым фоном
lazyBackgrounds.forEach(el => bgObserver.observe(el))
</script>
Такой подход удобен для карточек, баннеров и блоков с большими бэкграундами.
Ленивая загрузка видео и iframe
Видео и iframe часто загружают тяжелые скрипты и дополнительные запросы, поэтому их тоже лучше загружать лениво.
HTML атрибут loading для iframe
Сейчас iframe также поддерживает атрибут loading:
<!-- Встраиваем Google Maps лениво -->
<iframe
src="https://www.google.com/maps/embed?pb=..."
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
style="border:0;"
allowfullscreen
></iframe>
Комментарии:
- loading="lazy" отложит загрузку карты до приближения к видимой области.
- Это простой способ ускорить страницы с картами, видео‑плеерами и внешними виджетами.
Отложенная инициализация видео YouTube
С YouTube есть часто используемый трюк: сначала выводим превью‑картинку и кнопку Play, а iframe подставляем только по клику.
<!-- Контейнер для видео -->
<div class="video-wrapper" data-video-id="dQw4w9WgXcQ">
<!-- Здесь мы показываем превью из YouTube по ID видео -->
<div
class="video-preview"
style="background-image: url('https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg');"
>
<button class="video-play-button">▶</button>
</div>
</div>
<script>
// Ищем все контейнеры с видео
const videoWrappers = document.querySelectorAll('.video-wrapper')
videoWrappers.forEach(wrapper => {
const button = wrapper.querySelector('.video-play-button')
// Навешиваем обработчик клика по кнопке Play
button.addEventListener('click', () => {
const videoId = wrapper.getAttribute('data-video-id')
// Создаем iframe только в момент клика
const iframe = document.createElement('iframe')
iframe.width = '560'
iframe.height = '315'
// Добавляем параметр autoplay чтобы видео сразу запустилось
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`
iframe.title = 'YouTube video player'
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
iframe.allowFullscreen = true
// Очищаем превью и вставляем iframe
wrapper.innerHTML = ''
wrapper.appendChild(iframe)
})
})
</script>
Так вы избегаете ранней загрузки тяжелых скриптов YouTube, пока пользователь даже не дошел до блока с видео.
Ленивая загрузка компонентов и модулей JavaScript
Теперь давайте посмотрим на lazy‑loading кода. Это особенно актуально для SPA и крупных фронтенд‑проектов.
Динамический импорт модулей
Современный JavaScript поддерживает динамический импорт — функцию import(), которая возвращает промис и загружает модуль по требованию.
// Здесь мы лениво подключаем модуль только при необходимости
async function loadChartModule() {
// import() загрузит модуль только в момент вызова функции
const module = await import('./chart.js')
// Предполагаем что модуль экспортирует функцию initChart
module.initChart('#chart-container')
}
// Обработчик клика по кнопке "Показать график"
document.getElementById('show-chart').addEventListener('click', () => {
// При первом клике загрузится модуль и отрисуется график
loadChartModule()
})
Комментарии:
- В отличие от статического import в начале файла, import() позволяет разбить код на чанки.
- Бандлер (Webpack, Vite, Rollup) автоматически создаст отдельный файл для этого модуля и подключит его только по запросу.
Преимущество: вы уменьшаете размер первоначального бандла и загружаете тяжелый функционал (например, графики, редакторы) только при необходимости.
Code splitting и lazy-loading в React
Смотрите, как это выглядит в React с использованием React.lazy и Suspense.
// Chart.jsx - тяжелый компонент для отображения графика
export default function Chart() {
// Здесь может быть сложная логика и тяжелые библиотеки
return <div>Здесь будет график</div>
}
// App.jsx - главный компонент
import React, { Suspense, useState } from 'react'
// React.lazy объявляет ленивый компонент
const LazyChart = React.lazy(() => import('./Chart'))
export default function App() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>
Показать график
</button>
{showChart && (
// Suspense показывает fallback пока LazyChart загружается
<Suspense fallback={<div>Загрузка графика...</div>}>
<LazyChart />
</Suspense>
)}
</div>
)
}
Как видите, этот код выполняет:
- определение ленивого компонента через React.lazy;
- показ заглушки во время загрузки;
- подгрузку компонента только после клика на кнопку.
Ленивая загрузка маршрутов в React Router
Частый сценарий — подгружать коды страниц при переходе по маршруту.
// routes.jsx - конфигурация маршрутов
import React, { Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// Ленивая загрузка страниц
const HomePage = React.lazy(() => import('./pages/HomePage'))
const ProfilePage = React.lazy(() => import('./pages/ProfilePage'))
const SettingsPage = React.lazy(() => import('./pages/SettingsPage'))
export function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<div>Загрузка страницы...</div>}>
<Routes>
{/* Главная страница */}
<Route path="/" element={<HomePage />} />
{/* Профиль пользователя */}
<Route path="/profile" element={<ProfilePage />} />
{/* Настройки */}
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
Теперь вы увидите, как это выглядит в коде: каждую страницу можно загрузить лениво, а общий fallback отображается, пока соответствующий бандл подгружается.
Ленивая загрузка компонентов в Vue
В Vue 3 ленивые компоненты можно объявлять с помощью defineAsyncComponent или динамических импортов в маршрутах.
// router.js - пример ленивой загрузки страниц во Vue Router
import { createRouter, createWebHistory } from 'vue-router'
// Каждая страница подключается через динамический import
const Home = () => import('./pages/Home.vue')
const Profile = () => import('./pages/Profile.vue')
const Settings = () => import('./pages/Settings.vue')
const routes = [
{ path: '/', component: Home },
{ path: '/profile', component: Profile },
{ path: '/settings', component: Settings }
]
export const router = createRouter({
history: createWebHistory(),
routes
})
Такой подход позволяет автоматически разбить бандл приложения на части и загружать их только при переходе на соответствующий маршрут.
Ленивая загрузка данных
Теперь давайте разберемся на примере с данными. Представим, что у вас есть длинный список элементов, и вы не хотите загружать все сразу.
Пагинация и «Показать еще»
Один из самых простых вариантов — догружать данные по нажатию кнопки.
// Здесь мы храним текущую страницу данных
let currentPage = 1
const pageSize = 20 // Количество элементов на страницу
async function loadItems(page) {
// Отправляем запрос на сервер с указанием страницы
const response = await fetch(`/api/items?page=${page}&limit=${pageSize}`)
const data = await response.json()
const listEl = document.getElementById('items-list')
data.items.forEach(item => {
const li = document.createElement('li')
// Добавляем текст из данных в список
li.textContent = item.title
listEl.appendChild(li)
})
// Если больше нет элементов - скрываем кнопку
if (!data.hasMore) {
document.getElementById('load-more').style.display = 'none'
}
}
// Обработчик нажатия на кнопку "Показать еще"
document.getElementById('load-more').addEventListener('click', () => {
currentPage += 1
loadItems(currentPage)
})
// Начальная загрузка первой страницы
loadItems(currentPage)
Плюсы:
- простая реализация;
- контроль над тем, когда именно делать сетевые запросы;
- пользователю понятен паттерн «Показать еще».
Бесконечный скролл с Intersection Observer
Если вы хотите автоматическую подгрузку при прокрутке, можно использовать Intersection Observer с «сентинелом» — специальным элементом внизу списка.
<ul id="items-list">
<!-- Здесь будут добавляться элементы -->
</ul>
<!-- Сентинел - элемент для наблюдения внизу списка -->
<div id="scroll-sentinel"></div>
<script>
let currentPage = 1
const pageSize = 20
let loading = false // Флаг чтобы не делать несколько запросов одновременно
let hasMore = true // Флаг что данные еще есть
async function loadMoreItems() {
if (loading || !hasMore) {
return // Выходим если уже идет загрузка или данных больше нет
}
loading = true
const response = await fetch(`/api/items?page=${currentPage}&limit=${pageSize}`)
const data = await response.json()
const listEl = document.getElementById('items-list')
data.items.forEach(item => {
const li = document.createElement('li')
li.textContent = item.title
listEl.appendChild(li)
})
hasMore = data.hasMore
loading = false
currentPage += 1
}
// Создаем наблюдатель за появлением сентинела в viewport
const sentinel = document.getElementById('scroll-sentinel')
const listObserver = new IntersectionObserver(async entries => {
for (const entry of entries) {
// Если сентинел стал видимым и данные еще есть - подгружаем
if (entry.isIntersecting && hasMore) {
await loadMoreItems()
}
}
}, {
root: null, // Наблюдаем относительно окна
rootMargin: '0px 0px 200px 0px', // Заранее начинаем загрузку
threshold: 0
})
// Запускаем наблюдение
listObserver.observe(sentinel)
// Загрузка первой страницы
loadMoreItems()
</script>
Обратите внимание:
- Сентинел всегда остается внизу списка, и когда список почти доскролливают, он попадает вviewport.
- Это триггерит загрузку новой порции данных.
- Важно следить за флагами loading и hasMore, чтобы не запускать лишние запросы.
Ленивая загрузка и SEO
Многие переживают, не повредит ли lazy‑loading поисковой оптимизации. Здесь есть несколько моментов.
Когда ленивая загрузка безопасна для SEO
Если вы используете:
- нативный атрибут loading="lazy" для изображений и iframe;
- Intersection Observer с корректными src и текстовым контентом, доступным без JS;
то современные поисковые боты в большинстве случаев справляются.
Однако есть важные нюансы:
- Критический текстовый контент лучше не скрывать и не подгружать через сложный JavaScript.
- Важно, чтобы img в DOM имели реальные src или хотя бы корректно подставляли атрибуты при отработке скриптов.
Практические рекомендации
- Не применяйте ленивую загрузку к логотипам, основным иллюстрациям над первым экраном и картинкам, важным для поисковых сниппетов.
- Старайтесь не лениво загружать основной контент страниц, по которым вы хотите ранжироваться.
- Если используете свои скрипты для ленивой загрузки, убедитесь, что без JS страница остается хотя бы частично полезной (принцип progressive enhancement).
Паттерны и практические советы по внедрению
Что стоит загружать лениво в первую очередь
Обычно при аудите производительности стоит пройтись по таким категориям:
- большие изображения ниже первого экрана;
- внешние виджеты (чаты, аналитика, карты, плееры);
- тяжелые JS‑библиотеки, которые не нужны сразу (редакторы, графики, WYSIWYG);
- необязательные данные (статистика, дополнительные списки, рекомендательные блоки).
Давайте посмотрим, как это можно оформлять на практике.
Пример: отложенная загрузка аналитики
Нередко скрипты аналитики и трекинга значимо замедляют загрузку. Вы можете отложить их до первого взаимодействия пользователя.
// Флаг - загружена ли уже аналитика
let analyticsLoaded = false
function loadAnalytics() {
if (analyticsLoaded) return
analyticsLoaded = true
// Создаем тег script вручную
const script = document.createElement('script')
script.src = 'https://example.com/analytics.js'
script.async = true // Не блокируем парсинг HTML
document.head.appendChild(script)
}
// Обработчик первого взаимодействия
function onFirstInteraction() {
loadAnalytics()
// После первого срабатывания - убираем обработчики
window.removeEventListener('scroll', onFirstInteraction)
window.removeEventListener('click', onFirstInteraction)
window.removeEventListener('keydown', onFirstInteraction)
}
// Подключаемся к нескольким возможным событиям первого действия
window.addEventListener('scroll', onFirstInteraction, { once: true })
window.addEventListener('click', onFirstInteraction, { once: true })
window.addEventListener('keydown', onFirstInteraction, { once: true })
Здесь я размещаю пример, чтобы вам было проще понять идею: вы отложили загрузку аналитики до того момента, когда пользователь проявил интерес к странице хотя бы одним действием.
Паттерн: «ленивая вкладка»
Частая ситуация — у вас есть несколько табов с содержимым, но пользователь видит только один.
<div class="tabs">
<button data-tab="overview">Обзор</button>
<button data-tab="stats">Статистика</button>
<button data-tab="logs">Логи</button>
</div>
<div id="tab-overview">Контент обзора (загружен сразу)</div>
<div id="tab-stats" data-lazy-url="/api/stats" hidden>Статистика будет здесь</div>
<div id="tab-logs" data-lazy-url="/api/logs" hidden>Логи будут здесь</div>
<script>
// Запоминаем какая вкладка активна
let activeTab = 'overview'
async function activateTab(tabName) {
if (tabName === activeTab) return
// Скрываем текущую вкладку
document.getElementById(`tab-${activeTab}`).hidden = true
const newTabEl = document.getElementById(`tab-${tabName}`)
const url = newTabEl.getAttribute('data-lazy-url')
// Если у вкладки есть URL и она еще не загружена
if (url && !newTabEl.dataset.loaded) {
// Делаем запрос к API
const response = await fetch(url)
const data = await response.text()
// Вставляем полученный HTML или текст
newTabEl.innerHTML = data
// Помечаем вкладку как загруженную
newTabEl.dataset.loaded = 'true'
}
// Показываем новую вкладку
newTabEl.hidden = false
activeTab = tabName
}
// Обработчик кликов по кнопкам табов
document.querySelectorAll('.tabs button').forEach(button => {
button.addEventListener('click', () => {
const tab = button.getAttribute('data-tab')
activateTab(tab)
})
})
</script>
Как видите, этот фрагмент кода решает задачу: вы грузите данные для вкладки только один раз — при первом открытии. При повторных переключениях данные уже не запрашиваются.
Возможные проблемы и как их решать
Всплывающее появление элементов и сдвиг верстки (Cumulative Layout Shift)
Когда вы лениво подгружаете изображения, может возникнуть ситуация: сначала картинка отсутствует, а потом резко занимает место, сдвигая контент. Это негативно влияет на метрику CLS.
Чтобы этого избежать:
- всегда задавайте ширину и высоту изображений;
- либо используйте aspect-ratio в CSS.
Пример:
<img
src="photo-large.jpg"
alt="Пейзаж в горах"
loading="lazy"
width="800"
height="600"
/>
Или через CSS:
.lazy-image-placeholder {
/* Соотношение сторон 4 к 3 */
aspect-ratio: 4 / 3;
background-color: #f0f0f0; /* Серый фон пока картинка не загрузилась */
}
<!-- Блок c зафиксированной высотой за счет aspect-ratio -->
<div class="lazy-image-placeholder">
<img
src="photo-large.jpg"
alt="Пейзаж в горах"
loading="lazy"
/>
</div>
Так место под картинку будет зарезервировано заранее, и лейаут не дернется.
Слишком агрессивная ленивая загрузка
Если вы сделаете threshold слишком маленьким или укажете большой отступ rootMargin, ресурсы могут начинать грузиться слишком поздно, и пользователь увидит «подпрыгивающий» контент или задержки.
Рекомендация:
- Подбирайте rootMargin в зависимости от скорости сети целевой аудитории.
- Тестируйте на медленным соединениям в DevTools (throttling).
Например, для картинок:
- на десктопах с хорошей связью можно использовать rootMargin: '200px';
- для медленных сетей — увеличить до '500px', чтобы дать больше времени на загрузку.
Память и утечки
При использовании Intersection Observer не забывайте отписываться от элементов, которые уже обработаны или удалены.
В приведенных примерах мы вызывали obs.unobserve(element). Это важно, чтобы:
- не держать лишние ссылки в памяти;
- не вызывать колбэк для уже несуществующих или неинтересных элементов.
Ленивая загрузка и доступность (accessibility)
Некоторые техники ленивой загрузки могут ухудшать доступность:
- если вы удаляете контент из DOM, скринридеры его не увидят;
- при динамической подгрузке лучше явно управлять фокусом и использовать aria‑атрибуты.
Рекомендации:
- оставляйте в DOM содержимое, если оно важно для понимания страницы;
- для загружаемых блоков можно использовать живые регионы (aria-live), если вы хотите сообщить пользователю о появлении нового контента.
Заключение
Ленивая загрузка — один из ключевых инструментов оптимизации производительности в веб‑разработке. С ее помощью вы можете:
- ускорить первую отрисовку страниц;
- уменьшить объем загружаемых данных;
- гибко контролировать, что и когда загружается.
Мы рассмотрели:
- нативную ленивую загрузку изображений и iframe через атрибут loading;
- кастомные решения на базе Intersection Observer для картинок, фонов, списков и бесконечного скролла;
- ленивую загрузку модулей и компонентов в JavaScript, включая React и Vue;
- паттерны для ленивых вкладок, аналитики и подгрузки данных;
- частые проблемы — сдвиг верстки, задержки, утечки — и способы их решения.
Когда вы строите архитектуру приложения, полезно мысленно разделять код и ресурсы на критичные (которые действительно должны загрузиться сразу) и опциональные (которые можно отложить). Lazy‑loading как раз дает вам механизмы, чтобы реализация этого разделения была четкой и контролируемой.
Главное — не превращать ленивую загрузку в самоцель. Важно, чтобы пользователь получал быстрый и предсказуемый опыт, а контент оставался доступным и для людей, и для поисковых систем.
Частозадаваемые технические вопросы по теме и ответы
Как сделать fallback для ленивой загрузки изображений в старых браузерах
Старые браузеры не поддерживают loading и Intersection Observer. Используйте простую проверку:
if ('IntersectionObserver' in window) {
// Здесь включаем вашу логику ленивой загрузки через Intersection Observer
} else {
// Старый браузер - просто проставляем всем data-src в src
document.querySelectorAll('img[data-src]').forEach(img => {
const realSrc = img.getAttribute('data-src')
if (realSrc) {
img.src = realSrc
}
})
}
Так пользователи старых браузеров просто загрузят все сразу, но останутся с рабочим интерфейсом.
Как дебажить момент срабатывания ленивой загрузки
В DevTools включите имитацию медленной сети и логируйте события:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
console.log('Элемент', entry.target, 'видим', entry.isIntersecting)
})
}, {
rootMargin: '200px',
threshold: 0.1
})
Прокрутите страницу и смотрите в консоль, когда элементы отмечаются как видимые, и при необходимости подкорректируйте rootMargin и threshold.
Как комбинировать ленивую загрузку и prefetch
Вы можете заранее подсказывать браузеру, что некоторый ресурс скоро понадобится, но все равно управлять моментом использования:
<!-- Заранее подсказываем браузеру что этот скрипт будет нужен -->
<link rel="prefetch" href="/static/chart-module.js" as="script">
А в коде по‑прежнему делаете динамический import. Браузер может заранее скачать файл в фоне, но выполнить его только когда вы вызовете import().
Как отменять ленивую загрузку при изменении условий
Если вы перестали нуждаться в наблюдении за элементом (например, вкладка скрыта навсегда), вызовите unobserve:
// Предположим мы храним observer снаружи
observer.unobserve(element) // Прекращаем следить за элементом
Для ленивых запросов к API можно использовать AbortController:
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
// Позже если данные уже не нужны
controller.abort()
Можно ли делать ленивую загрузку CSS
Чисто лениво подключать критичный CSS не стоит. Но неважные стили (например, для виджета внизу страницы) можно грузить позже:
function loadExtraStyles() {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/css/extra.css'
document.head.appendChild(link)
}
// Например после полной загрузки страницы
window.addEventListener('load', loadExtraStyles)
Так основной CSS загрузится быстро, а дополнительный подтянется без влияния на время первого рендера.