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-зависимости | Запасной UI | onErrorCaptured на родителе |
Три состояния держат раздельно: загрузка это 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