React.lazy Та suspense — ледачі компоненти в React
React.lazy - функція, яка завантажує React-компонент динамічно: не в початковому бандлі, а тоді, коли компонент вперше рендериться. Разом із Suspense вона показує заглушку під час завантаження і робить розділення коду (code splitting) майже без зусиль.
Теорія
TL;DR
React.lazy()обгортає динамічнийimport()і повертає компонент, який React рендерить звичноSuspenseперехоплює стан завантаження і показує fallback UI (спінер, скелет, заглушка)- Webpack і Vite автоматично створюють окремі чанки для lazy-модулів
- Працює лише з default-експортами - для named-експортів потрібен один додатковий крок
- Найкраще підходить для сторінок за роутом, важких модалок і всього, що користувач бачить рідко
Швидкий приклад
import React, { lazy, Suspense } from 'react';
// Vite/Webpack автоматично виносить це в окремий чанк
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dashboard />
</Suspense>
);
}Коли Dashboard рендериться вперше, React призупиняється, завантажує чанк, потім продовжує рендеринг. Prop fallback - це те, що бачить користувач в цей момент.
Як відбувається розділення
Бандлер бачить import('./pages/Dashboard') і створює окремий .js файл для цього модуля. React.lazy обгортає результат у Promise. При першому рендері компонента React "кидає" цей Promise всередині, Suspense його перехоплює, рендерить fallback і повторно рендерить компонент після того, як Promise виконається.
Ти не торкаєшся цієї механіки. API - це lazy + Suspense, решта автоматично.
Коли використовувати
- Розділення на рівні роутів: кожна сторінка за роутом - хороший кандидат. Користувачі на
/homeне потребують бандла/settingsодразу при завантаженні. - Важкі UI-бібліотеки: текстові редактори, чарти, PDF-в'юери - завантажуй їх лінево.
- Модалки і дровери: компоненти, що відкриваються по кліку, завантажуються лише тоді, коли потрібні.
- Адмін-розділи: частини додатку, куди більшість користувачів ніколи не заходить.
Не треба лінево завантажувати дрібні компоненти або ті, що відображаються відразу при кожному рендері. Async-оверхед є, навіть якщо він малий.
Named exports і lazy
React.lazy очікує, що динамічний імпорт поверне модуль з default-експортом. Якщо компонент використовує named-експорт, обгорни так:
// UserCard використовує named-експорт
const UserCard = lazy(() =>
import('./UserCard').then((mod) => ({ default: mod.UserCard }))
);Це не баг - так спроектовано API. Я бачив, як ця деталь ставить в тупик цілі команди, які вважали, що lazy працює з будь-яким імпортом.
Типові помилки
Забули Suspense:
// Впаде в рантаймі
const Chart = lazy(() => import('./Chart'));
function Dashboard() {
return <Chart />; // Вище немає Suspense - React видасть помилку
}Lazy-компоненти потребують Suspense-границі десь вище в дереві. Одна границя покриває кілька lazy-компонентів одночасно.
Оголошення lazy всередині компонента:
// Погано: при кожному рендері створюється новий reference
function Page() {
const Chart = lazy(() => import('./Chart')); // неправильно
return <Chart />;
}Оголошуй lazy-компоненти на рівні модуля, поза будь-якою функцією.
Нема ErrorBoundary в продакшені:
Мережа падає. Якщо завантаження чанку зафейлиться, React зруйнує весь дерево без ErrorBoundary. В продакшені завжди додавай обидва:
<ErrorBoundary fallback={<p>Не вдалося завантажити.</p>}>
<Suspense fallback={<Spinner />}>
<LazyPage />
</Suspense>
</ErrorBoundary>Де зустрічається
- React Router-додатки: lazy для кожного роуту, один
Suspenseна рівні роутера - Next.js: використовує
next/dynamic, який обгортає той самий патерн з додатковими опціями якssr: false - Бібліотеки компонентів: важкі компоненти (DataGrid, RichEditor) шиплять окремими точками входу, щоб споживачі могли їх завантажувати лінево
Follow-up питання
Q: Чи може один Suspense обгортати кілька lazy-компонентів?
A: Так. Suspense показує fallback, якщо хоча б один з його lazy-дітей ще завантажується. Після того як всі завантажились, рендеряться всі одразу. Зручно для розділення на рівні роуту.
Q: Що станеться, якщо чанк не завантажиться?
A: React кидає помилку вгору по дереву. Без ErrorBoundary увесь додаток розмонтується. З нею - тільки підтерево границі покаже fallback помилки.
Q: Чи працює React.lazy з Server Components у React 18+?
A: Не безпосередньо. На сервері динамічні імпорти працюють інакше. Next.js обробляє це через next/dynamic з опцією { ssr: false }. Чистий React.lazy - тільки клієнт.
Q: Яка різниця між React.lazy і next/dynamic?
A: next/dynamic обгортає React.lazy і додає опції: ssr: false для пропуску серверного рендерингу та вбудований prop loading замість Suspense. Та сама концепція, інший шар API.
Приклади
Базовий lazy-компонент
import React, { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function ReportPage() {
return (
<div>
<h1>Місячний звіт</h1>
<Suspense fallback={<div>Завантаження графіка...</div>}>
<HeavyChart data={reportData} />
</Suspense>
</div>
);
}HeavyChart і його залежності завантажуються окремим файлом. Користувачі, які ніколи не відкривають ReportPage, не скачують цей код.
Розділення на рівні роутів з React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Завантаження сторінки...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Один Suspense на рівні роутера покриває всі сторінки. Кожна завантажується лише при переході. Адмін-бандл не потрапить до звичайного користувача, поки він не зайде на /admin.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.