Пошук уроків, статей та іншого контенту
Прискорите старт застосунку, завантажуючи компоненти маршрутів ліниво через React.lazy, Suspense і динамічний імпорт.
За стандартної збірки весь JavaScript застосунку може потрапити в один великий файл. Користувач завантажує його ще до того, як відкриє конкретний маршрут:
головна сторінка завантажує код сторінки профілю;
авторизований користувач завантажує код публічної частини;
рідко використовувані сторінки збільшують час старту застосунку.
Code splitting розділяє код на окремі частини — chunks. Браузер завантажує chunk лише тоді, коли він потрібен.
Для маршрутизації це особливо корисно: кожна сторінка може бути окремим chunk, який завантажується під час переходу на відповідний маршрут.
Статичний імпорт виконується під час завантаження модуля:
import DashboardPage from "./pages/DashboardPage";Динамічний імпорт повертає Promise:
const pageModule = import("./pages/DashboardPage");Бандлер розпізнає динамічний import() і зазвичай створює для такого модуля окремий файл.
React надає компонент lazy, який перетворює динамічний імпорт на компонент:
import { lazy } from "react";
const DashboardPage = lazy(() => import("./pages/DashboardPage"));Компонент DashboardPage не завантажується одразу. Його модуль буде завантажено під час першого рендерингу цього компонента.
Поки динамічний імпорт виконується, lazy-компонент призупиняє рендеринг. Найближчий компонент Suspense показує fallback:
import { lazy, Suspense } from "react";
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
function App() {
return (
<Suspense fallback={<p>Завантаження сторінки...</p>}>
<DashboardPage />
</Suspense>
);
}fallback має бути коротким і зрозумілим. Це може бути:
індикатор завантаження;
skeleton сторінки;
повідомлення для користувача.
Без Suspense React не зможе коректно обробити призупинення lazy-компонента.
Розглянемо застосунок із трьома маршрутами:
/ — головна сторінка;
/dashboard — панель керування;
/settings — налаштування.
Кожен lazy-модуль має експортувати компонент за замовчуванням:
// src/pages/HomePage.jsx
export default function HomePage() {
return (
<main>
<h1>Головна сторінка</h1>
<p>Ця сторінка завантажується разом із базовим кодом застосунку.</p>
</main>
);
}// src/pages/DashboardPage.jsx
export default function DashboardPage() {
return (
<main>
<h1>Панель керування</h1>
<p>Код панелі завантажується лише під час переходу на цей маршрут.</p>
</main>
);
}// src/pages/SettingsPage.jsx
export default function SettingsPage() {
return (
<main>
<h1>Налаштування</h1>
<p>Код налаштувань завантажується окремим chunk.</p>
</main>
);
}// src/App.jsx
import { lazy, Suspense } from "react";
import {
BrowserRouter,
Link,
Route,
Routes,
} from "react-router-dom";
import HomePage from "./pages/HomePage";
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
function PageLoader() {
return (
<div role="status" aria-live="polite">
Завантаження сторінки...
</div>
);
}
export default function App() {
return (
<BrowserRouter>
<header>
<nav>
<Link to="/">Головна</Link>{" "}
<Link to="/dashboard">Панель керування</Link>{" "}
<Link to="/settings">Налаштування</Link>
</nav>
</header>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}У цьому прикладі:
HomePage імпортується статично.
DashboardPage і SettingsPage імпортуються динамічно.
Бандлер створює окремі chunks для lazy-маршрутів.
Suspense показує PageLoader, поки chunk маршруту завантажується.
Навігація та header залишаються на екрані, бо Suspense обгортає лише Routes.
Точка виклику lazy має бути на рівні модуля, а не всередині компонента:
// Правильно
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
function App() {
return <DashboardPage />;
}// Неправильно
function App() {
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
return <DashboardPage />;
}У другому варіанті новий lazy-компонент створюється під час кожного рендерингу App. Це може призводити до скидання стану компонента та повторного завантаження.
Один Suspense навколо всіх маршрутів є простим варіантом, але під час навігації він замінює весь вміст маршруту одним fallback.
За потреби можна створити окремі межі завантаження:
// src/App.jsx
import { lazy, Suspense } from "react";
import {
BrowserRouter,
Link,
Route,
Routes,
} from "react-router-dom";
import HomePage from "./pages/HomePage";
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
function RouteFallback() {
return <p>Завантаження...</p>;
}
function LazyRoute({ children }) {
return (
<Suspense fallback={<RouteFallback />}>
{children}
</Suspense>
);
}
export default function App() {
return (
<BrowserRouter>
<header>
<nav>
<Link to="/">Головна</Link>{" "}
<Link to="/dashboard">Панель керування</Link>{" "}
<Link to="/settings">Налаштування</Link>
</nav>
</header>
<Routes>
<Route path="/" element={<HomePage />} />
<Route
path="/dashboard"
element={
<LazyRoute>
<DashboardPage />
</LazyRoute>
}
/>
<Route
path="/settings"
element={
<LazyRoute>
<SettingsPage />
</LazyRoute>
}
/>
</Routes>
</BrowserRouter>
);
}Такий підхід дозволяє:
мати різні fallback для різних сторінок;
ізолювати завантаження одного маршруту від іншого;
залишити постійний layout поза межами Suspense.
React.lazy очікує, що динамічний імпорт поверне модуль із default export:
// Підтримується React.lazy
export default function ReportsPage() {
return <h1>Звіти</h1>;
}Якщо модуль має лише іменований export:
// src/pages/ReportsPage.jsx
export function ReportsPage() {
return <h1>Звіти</h1>;
}його можна перетворити на модуль із default:
import { lazy } from "react";
const ReportsPage = lazy(() =>
import("./pages/ReportsPage").then((module) => ({
default: module.ReportsPage,
})),
);Це працює, тому що callback для lazy повертає Promise з об'єктом такого вигляду:
{
default: Component
}Якщо компонент можна експортувати за замовчуванням, це зазвичай простіший і зрозуміліший варіант.
Lazy chunk завантажується не під час оголошення:
const SettingsPage = lazy(() => import("./pages/SettingsPage"));Він завантажується, коли React вперше намагається відрендерити SettingsPage.
Типовий сценарій:
користувач відкриває /;
завантажуються базові chunks і код HomePage;
користувач переходить на /settings;
React починає рендерити SettingsPage;
браузер завантажує chunk налаштувань;
поки chunk не завантажено, відображається Suspense fallback;
після завантаження React показує сторінку.
Під час наступного переходу цей chunk зазвичай уже є в кеші браузера, тому повторне завантаження не потрібне.
Suspense обробляє стан очікування, але не помилки завантаження. Наприклад, chunk може бути недоступним через:
проблеми з мережею;
видалення старих файлів після нового деплою;
помилку сервера;
блокування запиту браузером.
Для помилок потрібен Error Boundary:
// src/components/RouteErrorBoundary.jsx
import { Component } from "react";
export default class RouteErrorBoundary extends Component {
state = {
hasError: false,
};
static getDerivedStateFromError() {
return {
hasError: true,
};
}
render() {
if (this.state.hasError) {
return (
<main>
<h1>Не вдалося завантажити сторінку</h1>
<button
type="button"
onClick={() => window.location.reload()}
>
Спробувати ще раз
</button>
</main>
);
}
return this.props.children;
}
}Boundary можна розмістити навколо Suspense:
import { lazy, Suspense } from "react";
import RouteErrorBoundary from "./components/RouteErrorBoundary";
const DashboardPage = lazy(() => import("./pages/DashboardPage"));
export default function DashboardRoute() {
return (
<RouteErrorBoundary>
<Suspense fallback={<p>Завантаження панелі...</p>}>
<DashboardPage />
</Suspense>
</RouteErrorBoundary>
);
}Зазвичай Error Boundary розміщують на рівні маршруту або групи маршрутів, щоб помилка однієї сторінки не замінювала весь застосунок.
У production-збірці потрібно перевірити, що код справді розділено на chunks.
У DevTools браузера:
відкрийте вкладку Network;
увімкніть фільтр JS;
перезавантажте головну сторінку;
переконайтеся, що chunk панелі керування ще не завантажений;
перейдіть на /dashboard;
перевірте появу нового JavaScript-файлу.
Під час локальної розробки поведінка може відрізнятися від production через особливості dev-сервера та модульної системи. Остаточно оцінюйте code splitting на production-збірці.
import HomePage from "./pages/HomePage";
import DashboardPage from "./pages/DashboardPage";
import SettingsPage from "./pages/SettingsPage";Такий код завантажує всі сторінки одразу. Для маршрутів, які не потрібні на старті, використовуйте lazy:
const DashboardPage = lazy(() => import("./pages/DashboardPage"));Suspenseconst DashboardPage = lazy(() => import("./pages/DashboardPage"));
function App() {
return <DashboardPage />;
}Lazy-компонент повинен бути нащадком Suspense:
<Suspense fallback={<p>Завантаження...</p>}>
<DashboardPage />
</Suspense>function App() {
const Page = lazy(() => import("./Page"));
return <Page />;
}Оголошуйте lazy-компоненти поза функцією компонента:
const Page = lazy(() => import("./Page"));
function App() {
return <Page />;
}default exportЯкщо модуль не має default export, звичайний виклик lazy не відповідатиме очікуваному формату:
const ReportsPage = lazy(() => import("./ReportsPage"));Для іменованого експорту використовуйте перетворення:
const ReportsPage = lazy(() =>
import("./ReportsPage").then((module) => ({
default: module.ReportsPage,
})),
);Fallback на весь екран під час кожної навігації може створювати відчуття, що застосунок повністю перезапустився. Краще залишати стабільні частини інтерфейсу поза Suspense, а fallback обмежувати областю маршруту.
import() завантажує модуль асинхронно.
React.lazy перетворює динамічний імпорт на компонент.
Suspense показує fallback, поки lazy-компонент завантажується.
Розміщення lazy-компонентів на рівні маршрутів створює route-based code splitting.
Lazy-компонент бажано оголошувати поза функцією компонента.
Модуль для React.lazy має експортувати компонент через default.
Suspense не замінює Error Boundary: помилки завантаження потрібно обробляти окремо.
Результат code splitting потрібно перевіряти у production-збірці через Network DevTools.