Vue

<Suspense> и асинхронные компоненты

На странице четыре виджета, и каждый сам грузит данные, держит свой ref loading и рисует свой спиннер. Получается мозаика из частично загруженных блоков, которая дёргается по мере прихода ответов. Suspense предлагает другое: компонент заявляет 'я асинхронный', а родитель один раз описывает что показать пока всё грузится. Граница ждёт все асинхронные зависимости разом и переключается на контент целиком. В 2026 Suspense всё ещё помечен как экспериментальный, и это важно учитывать.

  • Страница профиля: один fallback-скелетон пока подгружаются аватар, статистика и лента
  • Дашборд: Suspense оборачивает группу виджетов, показывая единый плейсхолдер до готовности всех
  • Маршрут с тяжёлым редактором: defineAsyncComponent грузит код редактора по требованию
  • Карточка товара: async setup тянет данные товара, граница держит скелетон без ручного флага
  • Модальное окно отчёта: тяжёлый график подгружается асинхронно, fallback это лёгкий плейсхолдер

Предварительные знания

  • Жизненный цикл компонента и хук onMounted
  • Базовая загрузка данных с async/await из предыдущего урока
  • Понимание слотов: что такое именованный слот #default и #fallback

Граница Suspense и fallback

<Suspense> это компонент-граница с двумя слотами. Слот #default содержит контент с асинхронными зависимостями, слот #fallback это плейсхолдер на время загрузки. Граница ждёт, пока все асинхронные зависимости в #default разрешатся, и только тогда показывает контент целиком. Это убирает мозаику из независимых спиннеров.

Граница ждёт все асинхронные зависимости разом. Если внутри два async-компонента, fallback держится пока не готовы оба, а затем показываются вместе. Это сознательный выбор Vue: один согласованный экран вместо дёргающейся мозаики.

В документации Vue Suspense помечен как экспериментальный с момента появления и остаётся таким в 2026 году. API может измениться между минорными версиями, поэтому в проде его обкладывают тестами и не строят на нём критичный путь без запасного плана.

Когда <Suspense> переключается со слота #fallback на слот #default?

Асинхронный setup()

Внутри Suspense компонент может использовать await прямо в setup. С <script setup> верхнеуровневый await делает setup асинхронным: компонент не отрендерится, пока промис не разрешится, а его готовность отслеживает родительская граница Suspense. Так фетч пишут линейно, без ручного loading-флага.

  • Ручной loading (без Suspense) — ref loading, onMounted с фетчем, v-if по флагу в каждом компоненте. Состояние загрузки описывается вручную и многократно
  • async setup + Suspense — await прямо в setup, fallback описан один раз на границе. Состояние загрузки декларативно и в одном месте

Компонент с верхнеуровневым await обязан жить внутри <Suspense>. Без границы Vue не знает, кто отслеживает его готовность, и компонент не отрендерится корректно. Это частая причина пустого экрана при первом знакомстве с Suspense.

Что делает верхнеуровневый await в <script setup>?

defineAsyncComponent

defineAsyncComponent оборачивает динамический import компонента. Код такого компонента не попадает в основной бандл, а грузится отдельным чанком при первом использовании. Это и есть code-splitting: тяжёлый редактор или график не утяжеляют первоначальную загрузку страницы.

Внутри Suspense асинхронная загрузка самого кода компонента тоже считается зависимостью границы. То есть Suspense ждёт и завершения async setup, и подгрузки кода defineAsyncComponent, показывая fallback на оба процесса единообразно.

Краткая форма defineAsyncComponent(() => import(...)) подходит для простого ленивого импорта. Расширенный объект с loading, error, delay и timeout, который управляет отдельным состоянием самого ленивого компонента, разбирается в следующем уроке про async-компоненты.

Какую пользу даёт defineAsyncComponent с динамическим import?

Обработка ошибок при Suspense

Слот #fallback показывается только во время загрузки и не отвечает за ошибки. Если async setup бросил исключение или ленивый компонент не загрузился, это ловят на родителе через хук onErrorCaptured. Хук получает ошибку, может показать запасной UI и вернуть false, чтобы остановить дальнейшее распространение.

СитуацияЧто показатьМеханизм
Идёт загрузкаПлейсхолдерСлот #fallback
Загрузка завершенаКонтентСлот #default
Ошибка в async-зависимостиЗапасной UIonErrorCaptured на родителе

Три состояния держат раздельно: загрузка это fallback, успех это default, ошибка это onErrorCaptured. Попытка показать ошибку через fallback не сработает, потому что fallback живёт только в фазе ожидания.

Как обрабатывают ошибку, если async setup внутри Suspense бросил исключение?

Связь с другими темами

Урок про декларативную асинхронность. Дальше курс углубляет асинхронные компоненты и code-splitting:

  • Загрузка данных — Suspense заменяет ручной loading-флаг, но фетч и обработка ошибок остаются
  • Асинхронные компоненты и code-splitting — Полные опции defineAsyncComponent: delay, timeout, loading и error компоненты

Итог

  • <Suspense> это граница: слот #default показывается когда все асинхронные зависимости внутри разрешились, до этого виден #fallback
  • async setup() позволяет писать await прямо в setup; компонент становится асинхронной зависимостью своей границы Suspense
  • defineAsyncComponent грузит компонент по требованию через динамический import, что даёт code-splitting
  • Ошибки внутри Suspense ловят через onErrorCaptured на родителе или errorCaptured-хук, fallback сам ошибки не обрабатывает
  • Suspense в 2026 году всё ещё экспериментальный: API может измениться, что учитывают при использовании в проде

Связанные уроки

  • vue-30-data-fetching — Suspense убирает ручной loading-флаг, но логика самого фетча остаётся из урока про загрузку данных
  • vue-33-async-components — defineAsyncComponent раскрывается дальше: loading/error-компоненты, delay, timeout, code-splitting
<Suspense> и асинхронные компоненты

0

1

Войти