React — Suspense и lazy
React.lazyиSuspense— механизм code splitting на уровне компонентов: компонент загружается только при первом рендере, показывая fallback (skeleton, spinner) во время загрузки, что сокращает начальный размер бандла и ускоряет TTI.
Зачем нужно
Без code splitting весь код приложения грузится при первом посещении — даже страницы, которые пользователь никогда не откроет. React.lazy загружает компоненты по требованию, сокращая начальный bundle на 30-60% для больших приложений.
Где используется
- Маршруты (routes) — главный сценарий применения
- Тяжёлые компоненты ниже fold (графики, rich text редактор, карты)
- Модальные окна и диалоги, открываемые редко
- Admin панели и dashboard-компоненты для обычных пользователей
Основной контент
Базовое использование
import React, { lazy, Suspense } from 'react';
// Динамический импорт — Webpack/Vite автоматически создаёт chunk
const HeavyChart = lazy( => import('./HeavyChart'));
const AdminPanel = lazy( => import('../admin/AdminPanel'));
function App() {
return (
<Suspense fallback={<div className="skeleton">Loading chart...</div>}>
<HeavyChart data={data} />
</Suspense>
);
}
Route-based code splitting (React Router)
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Каждый маршрут — отдельный chunk
const Home = lazy( => import('./pages/Home'));
const Products = lazy( => import('./pages/Products'));
const Cart = lazy( => import('./pages/Cart'));
const Admin = lazy( => import('./pages/Admin'));
function AppRoutes() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/cart" element={<Cart />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Suspense>
);
}
Prefetch — загрузка заранее при hover
// Prefetch компонента при наведении на ссылку
function NavLink({ to, children, component: importFn }) {
const handleMouseEnter = () => {
// Запускаем загрузку chunk при hover
importFn;
};
return (
<a href={to} onMouseEnter={handleMouseEnter}>
{children}
</a>
);
}
// Использование:
<NavLink
to="/dashboard"
component={ => import('./pages/Dashboard')}
>
Dashboard
</NavLink>
Suspense для асинхронных данных (React 18+)
// use хук для data fetching с Suspense
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise); // Бросает promise, Suspense поймает
return <div>{user.name}</div>;
}
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Error Boundary для lazy компонентов
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError { return { hasError: true }; }
render {
if (this.state.hasError) {
return <button onClick={ => this.setState({ hasError: false })}>
Retry
</button>;
}
return this.props.children;
}
}
// Оборачиваем lazy компонент
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
Частые ошибки
- Определение lazy компонента внутри render — компонент пересоздаётся на каждый рендер
- Отсутствие Error Boundary — сетевая ошибка при загрузке chunk'а крашит приложение
- Слишком мелкое дробление — overhead от лишних HTTP-запросов превышает выгоду
- Lazy loading компонентов выше fold — задержка видимого контента
Связанные темы
- _MOC Производительность
- React -- виртуальный DOM и reconciliation
- React.memo и useMemo
- Islands Architecture
- Метрики -- FCP, TTFB, TTI, TBT