Skip to main content

React.lazy Та suspense — ледачі компоненти в React

React.lazy - функція, яка завантажує React-компонент динамічно: не в початковому бандлі, а тоді, коли компонент вперше рендериться. Разом із Suspense вона показує заглушку під час завантаження і робить розділення коду (code splitting) майже без зусиль.

Теорія

TL;DR

  • React.lazy() обгортає динамічний import() і повертає компонент, який React рендерить звично
  • Suspense перехоплює стан завантаження і показує fallback UI (спінер, скелет, заглушка)
  • Webpack і Vite автоматично створюють окремі чанки для lazy-модулів
  • Працює лише з default-експортами - для named-експортів потрібен один додатковий крок
  • Найкраще підходить для сторінок за роутом, важких модалок і всього, що користувач бачить рідко

Швидкий приклад

jsx
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-експорт, обгорни так:

jsx
// UserCard використовує named-експорт const UserCard = lazy(() => import('./UserCard').then((mod) => ({ default: mod.UserCard })) );

Це не баг - так спроектовано API. Я бачив, як ця деталь ставить в тупик цілі команди, які вважали, що lazy працює з будь-яким імпортом.

Типові помилки

Забули Suspense:

jsx
// Впаде в рантаймі const Chart = lazy(() => import('./Chart')); function Dashboard() { return <Chart />; // Вище немає Suspense - React видасть помилку }

Lazy-компоненти потребують Suspense-границі десь вище в дереві. Одна границя покриває кілька lazy-компонентів одночасно.

Оголошення lazy всередині компонента:

jsx
// Погано: при кожному рендері створюється новий reference function Page() { const Chart = lazy(() => import('./Chart')); // неправильно return <Chart />; }

Оголошуй lazy-компоненти на рівні модуля, поза будь-якою функцією.

Нема ErrorBoundary в продакшені:

Мережа падає. Якщо завантаження чанку зафейлиться, React зруйнує весь дерево без ErrorBoundary. В продакшені завжди додавай обидва:

jsx
<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-компонент

jsx
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

jsx
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?