Kodokon kodokon.com

Suspense, lazy и разделение кода

Освой механизм приостановки, ленивую загрузку компонентов и разделение бандла.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Suspense опирается на малоизвестный механизм: компонент, у которого пока нет ни данных, ни кода, приостанавливает свой рендеринг. Под капотом текущая реализация выбрасывает промис, который React перехватывает так же, как перехватил бы ошибку. Затем React поднимается до ближайшей границы <Suspense>, показывает её fallback, а потом проигрывает рендер поддерева заново, как только промис разрешится. Это деталь реализации (никогда не выбрасывай промис сам), но она объясняет всё наблюдаемое поведение: граница работает как декларативный catch для асинхронной работы. Исторический сценарий использования - разделение кода через React.lazy.

JSX
import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart points={data} />
    </Suspense>
  );
}
Код Chart скачивается только при его первом рендере.

lazy ожидает функцию, которая возвращает промис, разрешающийся в модуль с экспортом default. Само разделение - работа сборщика: каждый динамический import() становится точкой разделения и порождает отдельный чанк, загружаемый по требованию. Частая тонкость в реальных кодовых базах: твои компоненты нередко являются именованными экспортами. Тогда модуль приходится адаптировать на лету.

JAVASCRIPT
const Chart = lazy(() =>
  import('./charts').then((module) => ({
    default: module.Chart,
  }))
);
Адаптация именованного экспорта под контракт React.lazy.

Сложность в продакшене: во время навигации уже показанный контент может снова приостановиться (новый маршрут, новый чанк) и резко смениться на fallback. Начиная с React 18 обновление, обёрнутое в startTransition, меняет это поведение: React оставляет предыдущий интерфейс на экране, пока новый рендер приостановлен в фоне, и показывает fallback только для контента, который ещё ни разу не был раскрыт. Границы к тому же композируются в глубину: несколько вложенных границ <Suspense> позволяют раскрывать интерфейс зона за зоной, от общего скелетона до деталей.

JSX
const [isPending, startTransition] = useTransition();

function openTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}
// The current tab stays visible while the new
// one loads, instead of dropping back to the fallback.
Переход не даёт откатиться к fallback во время навигации.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что должна возвращать функция, переданная в React.lazy?
    • Напрямую React-компонент
    • Промис, разрешающийся в модуль с экспортом default
    • Объект { component, loading }
    • Строку с путём к файлу
  2. Как не дать уже показанному контенту смениться на fallback, когда навигация приостанавливается?
    • Обернуть обновление состояния в startTransition
    • Передать fallback={null} границе Suspense
    • Использовать useEffect, чтобы отложить навигацию
    • Убрать родительскую границу Suspense
  3. Кто на самом деле выполняет разделение кода на отдельные чанки?
    • React, во время фазы коммита
    • Браузер, через HTTP-кэш
    • Сборщик, исходя из каждого динамического import()