Перейти к содержимому

6. Lazy Loading

Lazy Loading — это паттерн отложенной загрузки ресурсов до момента, когда они действительно нужны.

<!-- Изображения -->
<img src="photo.jpg" loading="lazy" alt="Photo">
<img src="hero.jpg" loading="eager" alt="Hero (выше fold — eager!)">
<!-- iframes -->
<iframe src="video-embed.html" loading="lazy"></iframe>
<iframe src="map.html" loading="lazy"></iframe>

Браузер сам решает когда загружать — обычно 2000-3000px до viewport.

Для контроля над lazy loading используйте IntersectionObserver:

// Базовый lazy loader для изображений
const lazyImages = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img); // больше не следим
}
});
}, {
rootMargin: '200px', // начинаем загрузку за 200px до viewport
threshold: 0.01,
});
lazyImages.forEach(img => observer.observe(img));
<!-- HTML с data-src -->
<img
data-src="photo.jpg"
src="placeholder.jpg"
alt="Photo"
width="800"
height="600"
>
import { lazy, Suspense, useState } from 'react';
// Загружается только при первом рендере
const HeavyModal = lazy(() => import('./HeavyModal'));
const DataTable = lazy(() => import('./DataTable'));
const RichEditor = lazy(() => import('./RichEditor'));
function App() {
const [showModal, setShowModal] = useState(false);
const [showEditor, setShowEditor] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>Открыть Modal</button>
{showModal && (
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyModal onClose={() => setShowModal(false)} />
</Suspense>
)}
<button onClick={() => setShowEditor(true)}>Открыть Editor</button>
<Suspense fallback={<EditorSkeleton />}>
{showEditor && <RichEditor />}
</Suspense>
</>
);
}
import { useRef, useState, useEffect } from 'react';
// Хук для lazy rendering
function useLazyRender(options = {}) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
}, {
rootMargin: options.rootMargin || '300px',
threshold: options.threshold || 0,
});
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return { ref, isVisible };
}
// Использование
function LazySection({ children, fallback }) {
const { ref, isVisible } = useLazyRender();
return (
<div ref={ref}>
{isVisible ? children : (fallback || <div style={{ height: 400 }} />)}
</div>
);
}
// В компоненте
function Page() {
return (
<>
<HeroSection />
<LazySection fallback={<Skeleton />}>
<HeavyChartSection />
</LazySection>
<LazySection>
<CommentsSection />
</LazySection>
</>
);
}
// Pagination — загружаем по странице
async function loadPage(page, pageSize = 20) {
const response = await fetch(`/api/items?page=${page}&limit=${pageSize}`);
return response.json();
}
// Infinite Scroll с IntersectionObserver
function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loaderRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting && hasMore) {
const newItems = await loadPage(page);
if (newItems.length === 0) {
setHasMore(false);
} else {
setItems(prev => [...prev, ...newItems]);
setPage(p => p + 1);
}
}
});
if (loaderRef.current) observer.observe(loaderRef.current);
return () => observer.disconnect();
}, [page, hasMore]);
return (
<>
<ul>{items.map(item => <Item key={item.id} {...item} />)}</ul>
<div ref={loaderRef}>
{hasMore ? <Spinner /> : <p>Всё загружено!</p>}
</div>
</>
);
}

Для очень больших списков — рендерим только видимые элементы:

// react-window
import { FixedSizeList } from 'react-window';
function Row({ index, style }) {
return (
<div style={style}>
Строка {index}
</div>
);
}
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
width={800}
itemCount={items.length}
itemSize={50} // высота строки
>
{({ index, style }) => (
<div style={style}>{items[index].title}</div>
)}
</FixedSizeList>
);
}