Реалізація Lazy Loading компонентів на сайті
Lazy loading — це відкладена завантаження компонентів, зображень або модулів до моменту, коли вони реально потрібні користувачу. Зменшує розмір початкового бандла, прискорює Time to Interactive та знижує витрату трафіку на мобільних пристроях.
Два рівні lazy loading
Рівень модулів — JavaScript-код компонента завантажується тільки при першому рендері. Реалізується через динамічний import().
Рівень ресурсів — зображення, iframe, відео завантажуються тільки коли елемент входить у область видимості. Реалізується через атрибут loading="lazy" або IntersectionObserver.
React.lazy і Suspense
// Без lazy loading — весь код у main bundle
import HeavyChart from '@/components/HeavyChart'
import DataTable from '@/components/DataTable'
// З lazy loading — окремі чанки, завантажуються по вимозі
import { lazy, Suspense } from 'react'
const HeavyChart = lazy(() => import('@/components/HeavyChart'))
const DataTable = lazy(() => import('@/components/DataTable'))
function Dashboard() {
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable rows={rows} />
</Suspense>
</div>
)
}
Suspense перехоплює промис, який виконує lazy-компонент під час завантаження, і показує fallback. Як тільки чанк завантажено — рендерить компонент.
Іменовані експорти з lazy
React.lazy працює тільки з default export. Для іменованих потрібна обгортка:
// Якщо компонент експортується як named export
const BarChart = lazy(() =>
import('@/components/charts').then(module => ({
default: module.BarChart,
}))
)
Умовна завантаження: тільки при видимості
Завантажувати важкий компонент одразу при монтуванні сторінки — не завжди потрібно. Якщо компонент знаходиться нижче fold, стоїть відкласти завантаження до прокрутки до нього:
// hooks/useLazyComponent.ts
import { useState, useEffect, useRef } from 'react'
export function useLazyComponent(threshold = '200px') {
const ref = useRef<HTMLDivElement>(null)
const [shouldRender, setShouldRender] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setShouldRender(true)
observer.disconnect()
}
},
{ rootMargin: threshold }
)
observer.observe(el)
return () => observer.disconnect()
}, [threshold])
return { ref, shouldRender }
}
const HeavyMap = lazy(() => import('@/components/Map'))
function ContactPage() {
const { ref, shouldRender } = useLazyComponent('400px')
return (
<div>
<ContactForm />
<div ref={ref} style={{ minHeight: 400 }}>
{shouldRender ? (
<Suspense fallback={<MapSkeleton />}>
<HeavyMap lat={53.9} lng={27.5} />
</Suspense>
) : (
<MapSkeleton />
)}
</div>
</div>
)
}
Lazy loading зображень
// Нативний lazy loading — підтримується всіма сучасними браузерами
function ProductCard({ product }: { product: Product }) {
return (
<div>
<img
src={product.image}
alt={product.title}
loading="lazy"
decoding="async"
width={400}
height={300}
/>
</div>
)
}
Для більш тонкого контролю — кастомний хук з IntersectionObserver:
function useLazyImage(src: string) {
const imgRef = useRef<HTMLImageElement>(null)
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const img = imgRef.current
if (!img) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
img.src = src
img.onload = () => setLoaded(true)
observer.disconnect()
}
},
{ rootMargin: '100px' }
)
observer.observe(img)
return () => observer.disconnect()
}, [src])
return { imgRef, loaded }
}
Next.js: динамічний імпорт
import dynamic from 'next/dynamic'
// З користувацьким loading state
const RichTextEditor = dynamic(
() => import('@/components/RichTextEditor'),
{
loading: () => <EditorSkeleton />,
ssr: false, // не рендерити на сервері — актуально для window-dependent компонентів
}
)
// Тільки при умові
const AdminPanel = dynamic(() => import('@/components/AdminPanel'), { ssr: false })
function Page({ isAdmin }: { isAdmin: boolean }) {
return isAdmin ? <AdminPanel /> : <UserView />
}
Vite: аналіз бандла
Щоб зрозуміти, що варто виносити в lazy chunks:
# Встановити плагін
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [
visualizer({ open: true, gzipSize: true, filename: 'dist/stats.html' }),
],
}
Після npm run build відкриється інтерактивна карта бандла. Шукаємо крупні залежності: chart libraries, rich text editors, date pickers, map SDKs — перші кандидати на lazy loading.
Preload критичних чанків
Чанки, які майже гарантовано понадобляться, можна prefetch в idle time:
// Prefetch при hover на ссилку
function NavLink({ href, chunkImport, children }) {
const handleMouseEnter = () => {
chunkImport() // () => import('@/pages/About')
}
return <a href={href} onMouseEnter={handleMouseEnter}>{children}</a>
}
Строки виконання
Настройка React.lazy + Suspense для існуючих компонентів — 0.5 дня. Повний аудит бандла, приоритизація, реалізація intersection-based завантаження зі skeleton-заглушками — 2–3 дні залежно від кількості компонентів.







