Пошук уроків, статей та іншого контенту
Розділимо код застосунку на незалежні частини, щоб зменшити початковий JavaScript-бандл.
Code splitting — це поділ JavaScript-коду застосунку на кілька частин, або chunk-файлів. Замість завантаження всього коду під час першого відкриття сторінки браузер отримує лише необхідну частину, а решту завантажує пізніше.
Без code splitting застосунок може мати приблизно таку структуру:
main.jsУ цьому файлі опиняється код усіх сторінок, компонентів і бібліотек. Навіть якщо користувач відкрив лише головну сторінку, браузер завантажує також код налаштувань, профілю чи адміністративної панелі.
Після code splitting структура може виглядати так:
main.js
settings-page.js
admin-page.js
profile-page.jsПочатково завантажується лише main.js. Інші файли браузер запитує тоді, коли вони стають потрібними.
Звичайний статичний імпорт виконується під час побудови модуля:
import SettingsPage from "./SettingsPage";Такий модуль зазвичай потрапляє до початкового бандла.
Динамічний імпорт використовує функцію import():
const module = await import("./SettingsPage");import() повертає Promise. Сучасні збирачі, зокрема ті, що використовуються React-проєктами, можуть перетворити такий імпорт на окремий chunk.
Наприклад:
async function loadSettingsPage() {
const module = await import("./SettingsPage");
return module.default;
}Файл SettingsPage не завантажується під час початкового запуску цього модуля. Він буде завантажений, коли виконається loadSettingsPage.
React надає lazy для завантаження компонента через динамічний імпорт:
const SettingsPage = lazy(() => import("./SettingsPage"));Функція, передана до lazy, має повертати Promise, який розв’язується модулем із компонентом за замовчуванням:
export default function SettingsPage() {
return <h1>Налаштування</h1>;
}Коли React уперше намагається відобразити SettingsPage, він очікує завершення динамічного імпорту.
Поки lazy-компонент завантажується, React має показати запасний інтерфейс. Для цього використовується Suspense:
<Suspense fallback={<p>Завантаження...</p>}>
<SettingsPage />
</Suspense>fallback відображається лише під час очікування компонента.
Повний приклад:
src/SettingsPage.jsxexport default function SettingsPage() {
return (
<section>
<h1>Налаштування</h1>
<p>Тут користувач може змінити налаштування свого облікового запису.</p>
</section>
);
}src/App.jsximport { lazy, Suspense, useState } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
export default function App() {
const [page, setPage] = useState("home");
return (
<main>
<nav>
<button type="button" onClick={() => setPage("home")}>
Головна
</button>
<button type="button" onClick={() => setPage("settings")}>
Налаштування
</button>
</nav>
{page === "home" && (
<section>
<h1>Головна сторінка</h1>
<p>Цей компонент входить до початкового коду застосунку.</p>
</section>
)}
{page === "settings" && (
<Suspense fallback={<p>Завантаження сторінки налаштувань...</p>}>
<SettingsPage />
</Suspense>
)}
</main>
);
}Під час відкриття застосунку:
завантажується код App;
компонент SettingsPage ще не завантажується;
після натискання кнопки React починає динамічний імпорт;
на час завантаження показується fallback;
після завантаження відображається SettingsPage.
lazy поза компонентомВиклик lazy потрібно робити на рівні модуля, а не всередині функції компонента.
Правильно:
import { lazy } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
export default function App() {
return <SettingsPage />;
}Неправильно:
import { lazy } from "react";
export default function App() {
const SettingsPage = lazy(() => import("./SettingsPage"));
return <SettingsPage />;
}Якщо створювати lazy-компонент під час кожного рендерингу, React отримуватиме новий тип компонента. Це може призводити до повторного монтування компонента та втрати його локального стану.
React.lazy очікує, що завантажений модуль матиме default export.
Цей варіант працює безпосередньо:
export default function ProfilePage() {
return <h1>Профіль</h1>;
}Якщо компонент експортується іменовано:
export function ProfilePage() {
return <h1>Профіль</h1>;
}потрібно перетворити результат динамічного імпорту на об’єкт із властивістю default:
import { lazy } from "react";
const ProfilePage = lazy(() =>
import("./ProfilePage").then((module) => ({
default: module.ProfilePage,
}))
);У простих випадках зручніше використовувати default export для компонентів, які завантажуються через React.lazy.
Найчастіше окремими chunk-файлами роблять сторінки або великі функціональні частини застосунку:
import { lazy, Suspense, useState } from "react";
const HomePage = lazy(() => import("./pages/HomePage"));
const ProfilePage = lazy(() => import("./pages/ProfilePage"));
const ReportsPage = lazy(() => import("./pages/ReportsPage"));
export default function App() {
const [page, setPage] = useState("home");
const pages = {
home: <HomePage />,
profile: <ProfilePage />,
reports: <ReportsPage />,
};
return (
<>
<nav>
<button type="button" onClick={() => setPage("home")}>
Головна
</button>
<button type="button" onClick={() => setPage("profile")}>
Профіль
</button>
<button type="button" onClick={() => setPage("reports")}>
Звіти
</button>
</nav>
<Suspense fallback={<p>Завантаження сторінки...</p>}>
{pages[page]}
</Suspense>
</>
);
}У такій схемі кожна сторінка може бути окремим chunk-файлом. Користувач, який відкриває лише головну сторінку, не повинен одразу завантажувати код звітів.
Для великих застосунків code splitting часто поєднують із маршрутизацією: кожен маршрут завантажує власний компонент лише під час переходу на цей маршрут.
SuspenseSuspense можна розташувати навколо всього застосунку:
<Suspense fallback={<p>Завантаження застосунку...</p>}>
<App />
</Suspense>Але тоді під час завантаження будь-якого lazy-компонента може бути замінений увесь вміст застосунку.
Часто зручніше розташувати межу Suspense ближче до компонента, який завантажується:
<section>
<h1>Панель користувача</h1>
<Suspense fallback={<p>Завантаження звітів...</p>}>
<ReportsPage />
</Suspense>
</section>У цьому випадку заголовок і решта інтерфейсу залишаються видимими, а стан завантаження замінює лише частину сторінки.
Збирач аналізує динамічні імпорти й створює окремі файли:
assets/index.js
assets/SettingsPage-abc123.js
assets/ProfilePage-def456.jsНазви файлів залежать від інструмента та його налаштувань.
У браузері послідовність може бути такою:
HTML завантажує початковий JavaScript;
React відображає доступну частину інтерфейсу;
при появі lazy-компонента виконується import();
браузер завантажує відповідний chunk;
React відображає компонент замість fallback.
Саме тому code splitting зменшує розмір початкового JavaScript-бандла, але не обов’язково зменшує загальний обсяг усіх файлів застосунку.
Динамічний імпорт може завершитися помилкою, наприклад через проблеми з мережею. Suspense показує стан очікування, але не є компонентом для обробки помилок.
Для помилок завантаження використовують error boundary. У спрощеному вигляді її можна розмістити навколо lazy-компонента:
<ErrorBoundary fallback={<p>Не вдалося завантажити сторінку.</p>}>
<Suspense fallback={<p>Завантаження...</p>}>
<SettingsPage />
</Suspense>
</ErrorBoundary>Error boundary має бути реалізована окремо або надана бібліотекою, яку вже використовує проєкт. Важливо розділяти два стани:
Suspense — компонент ще завантажується;
error boundary — завантаження або рендеринг завершилися помилкою.
Code splitting особливо корисний для:
сторінок, які відкривають не всі користувачі;
великих редакторів, графіків і звітів;
адміністративних розділів;
рідко використовуваних діалогових вікон;
функціональності, що містить великі залежності.
Не кожен маленький компонент потрібно робити lazy. Надмірний поділ може створити багато дрібних мережевих запитів і додаткові стани завантаження.
Орієнтуйтеся на розмір і частоту використання частини застосунку, а не лише на кількість файлів.
SuspenseLazy-компонент повинен бути нащадком Suspense:
import { lazy } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
export default function App() {
return <SettingsPage />;
}Без Suspense React не матиме компонента для відображення під час завантаження.
Правильно:
import { lazy, Suspense } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
export default function App() {
return (
<Suspense fallback={<p>Завантаження...</p>}>
<SettingsPage />
</Suspense>
);
}default exportТакий файл не буде напряму сумісним із React.lazy:
export function SettingsPage() {
return <h1>Налаштування</h1>;
}Або змініть експорт:
export default function SettingsPage() {
return <h1>Налаштування</h1>;
}Або адаптуйте іменований експорт через .then().
lazy викликається всередині компонентаНе створюйте lazy-компонент під час кожного рендерингу. Визначайте його на рівні модуля:
const SettingsPage = lazy(() => import("./SettingsPage"));Після code splitting частина інтерфейсу з’являється раніше, а lazy-компонент — після завантаження свого chunk-файлу. Для цього потрібно передбачити зрозумілий fallback, наприклад текст, скелетон або індикатор завантаження.
Code splitting має сенс, коли відкладений код достатньо великий або використовується рідко. Створення окремого chunk-файлу для кожної маленької кнопки може ускладнити завантаження без помітної користі.
Code splitting ділить JavaScript-застосунок на незалежні chunk-файли.
Динамічний import() дає змогу завантажувати модуль лише за потреби.
React.lazy перетворює динамічний імпорт на lazy-компонент.
Lazy-компонент потрібно відображати всередині Suspense.
Компонент, який завантажується через React.lazy, зазвичай має бути default export.
lazy слід викликати поза функцією компонента.
Найчастіше code splitting застосовують для великих сторінок і рідко використовуваних частин застосунку.
Менший початковий бандл прискорює перше завантаження, але додає окремі запити під час відкриття відкладених частин.