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 — задержка видимого контента

Связанные темы

Ресурсы