Привет, Хабр!

Есть такой интересный компонент в Vue.js и называется он <Suspense>. Он устроен так, чтобы собирать зависимые async‑компоненты и показывать единый индикатор загрузки вместо кучи спиннеров. Проще говоря, пока внутренняя часть (слот #default) ещё не готова из‑за ожидания данных, показывается запасное содержимое из слота #fallback. Как только все асинхронные зависимости разрешатся — снимаем запасной экран и выводим основное содержимое. По сути, <Suspense> создаёт границу вокруг вложенных компонентов и контролирует их загрузку.

Асинхронные зависимости и цель Suspense

Без <Suspense> каждая вьюшка сама ковыряется со своей загрузкой: в одном месте крутится спиннер, в другом — текст «Загрузка…», и всё это может проявляться в разное время. У Suspense совсем другая логика: если внутри есть хоть один компонент с async‑сборкой или async setup(), он задержит показ основного шаблона до готовности всех таких зависимостей. Это означает единый индикатор загрузки на уровень <Suspense> (слот #fallback), пока всё грядёт.

Звучит круто, но как это применить? Обычно создаёшь асинхронный компонент или асинхронную логику и просто оборачиваешь это <Suspense>. Пример (самый базовый): представим, есть асинхронный компонент <Profile> (который, скажем, подтягивает данные профиля из API). Тогда можно написать так:

<template>
  <Suspense>
    <template #default>
      <Profile />              <!-- Асинхронный компонент -->
    </template>
    <template #fallback>
      <div>Загрузка профиля...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
const Profile = defineAsyncComponent(() =>
  import('./Profile.vue')    // профайл-вид сбрасывается в отдельный чанк
)
</script>

<Profile /> — это асинхронный компонент (я определил его через defineAsyncComponent). Он по умолчанию считается «приостанавливаемым» (suspensible). То есть, раз над ним висит <Suspense>, загрузка управления отключается в нём самом, и именно <Suspense> показывает свой слот #fallback («Загрузка профиля...»), пока компонент подтягивает данные. Как только профиль подгрузится и компонент будет готов, <Suspense> автоматически заменит содержимое на <Profile /> (из слота #default).

Определение асинхронных компонентов

Для Lazy‑загрузки компонентов используем defineAsyncComponent. Обычно это выглядит так:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

Под капотом этот метод возвращает обёртку, которая при первом рендере запускает функцию‑загрузчик. Иногда нужно показывать прогресс или обрабатывать таймауты — defineAsyncComponent позволяет передать объект с опциями: loader, loadingComponent, errorComponent, delay, timeout и т. д.

Но если мы оборачиваем такие async‑компоненты в <Suspense>, их собственные опции загрузки игнорируются. Замечу, что можно отключить этот режим: добавить свойство { suspensible: false } в опции defineAsyncComponent. Тогда компонент будет контролировать свой спиннер сам, не ожидая <Suspense>. Обычно же это не нужно — мы как раз хотим, чтобы <Suspense> решил, когда заканчивать «загружаться» всем вместе.

Async setup и топовый await

В Vue 3 появилась возможность делать async setup() или использовать await прямо в <script setup>. Тогда компонент автоматически становится асинхронным: во время рендера <Suspense> заметит await и перейдёт в «pending»‑состояние. Например:

<script setup>
const res = await fetch('/api/posts')
const posts = await res.json()
</script>

<template>
  <ul>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

Если я поставлю этот компонент внутрь <Suspense>, то на время загрузки JSON‑данных я увижу fallback‑слот, без лишних флагов isLoading, без куч v-if. Vue сам подвесит рендер и пока ждёт промисы — покажет запасной контент.

Слоты #default и #fallback

Как заметили в примерах, <Suspense> работает со слотами: у него два слота — #default (по умолчанию) и #fallback. В слоте по умолчанию размещаем настоящее содержимое (компоненты, которые могут быть асинхронными). В слоте #fallback — то, что будет отображено, пока ждём. Каждый слот допускает только один root‑элемент (пусть и <div> или <template>). Сам <Suspense> при первом рендере в уме рисует default‑слот. Если при этом Vue встречает асинхронную операцию — компонент уходит в состояние «ожидания» и мгновенно показывает #fallback. Включив обработчик timeout, можно задать максимальное время ожидания перед переключением на #fallback, но по умолчанию переключение происходит сразу после обнаружения задержки.

Например, создадим более развёрнутый пример:

<template>
  <Suspense>
    <template #default>
      <DataList />          <!-- Компонент, собирающий данные -->
    </template>
    <template #fallback>
      <div class="spinner">Загрузка данных...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
const DataList = defineAsyncComponent(() =>
  // Предположим, DataList.vue содержит async setup() с fetch
  import('./DataList.vue')
)
</script>

<DataList /> может что‑то асинхронно подгружать (либо сам через async setup, либо просто лэйзи). Пока он грузится, увидим <div class="spinner">Загрузка данных...</div>, а после загрузки — список данных из DataList.

События Suspense

Иногда нужно знать, когда точно началась загрузка, а когда — закончилась. <Suspense> эмиттит три события: pending, resolve и fallback. pending срабатывает, когда группа асинхронных компонентов начинает ожидание, resolve — когда дождались и перешли к default‑слоту, fallback — когда показали запасной слот. Их можно слушать так:

<template>
  <Suspense 
    @pending="onPending" 
    @resolve="onResolved"
  >
    <template #default>
      <AsyncCompA />
      <AsyncCompB />
    </template>
    <template #fallback>
      <div>Ждём...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { ref } from 'vue'
const loading = ref(false)

function onPending() {
  loading.value = true
  console.log('Началась загрузка...')
}
function onResolved() {
  loading.value = false
  console.log('Готово!')
}
</script>

Обычно связывают эти события с индикатором (например, глобальным прелоадером в layout или локальным стейтом). Например, в onPending можно включать прелоадер, а в onResolved — выключать. Событие fallback можно пропустить в простых случаях, его редко требуется обрабатывать.

Таймаут и обработка ошибок

У <Suspense> есть проп timeout, который позволяет задержать переход на fallback: если в течение timeout миллисекунд default‑слот не разрешился, тогда показывается запасной.

Если поставить timeout="0", запасной покажется сразу при любом обновлении (т. е. поведение как мы описывали).

По дефолту внутри <Suspense> не будет автоматической обработки ошибок. Если какая‑то из асинхронных зависимостей упадёт, <Suspense> сам по себе этого не схватит, нужно ловить ошибки внешними средствами. Можно повесить errorCaptured или onErrorCaptured() в родительском компоненте с <Suspense>. Примерно так:

<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)

onErrorCaptured(err => {
  error.value = err
  return false  // Останавливаем всплытие (пока не хочу, чтобы ошибка уходила дальше)
})
</script>

<template>
  <Suspense>
    <ErrorProneComponent />
    <template #fallback>
      <div>Загрузка...</div>
    </template>
  </Suspense>
  <div v-if="error" class="error">
    Ошибка: {{ error.message }}
  </div>
</template>

Так перехватим исключение из ErrorProneComponent и сможем сообщить пользователю об ошибке.

Вложенные Suspense (Nested Suspense)

С Vue 3.3+ можно вкладывать несколько <Suspense> друг в друга. Это нужно, когда в одной и той же иерархии есть несколько асинхронных уровней. По умолчанию Suspense обрабатывает все зависимые async‑компоненты, которые находятся ниже него. Но если есть глубоко вложенные, иногда удобно создать несколько границ. Важная опция здесь — suspensible у вложенного <Suspense>. Если её не указывать, внутренний <Suspense> будет считаться синхронным для внешнего, и у каждого появится свой fallback при одновременной смене: может получиться пустой узел на экране. Если же у вложенного поставить suspensible (без значения, просто флаг), родительский <Suspense> «возьмёт под контроль» и его зависимые компоненты, а сам внутренний станет дополнительной границей для правильного патчинга. Пример вложения:

<template>
  <Suspense>
    <OuterAsyncComponent>
      <Suspense suspensible>
        <InnerAsyncComponent />
      </Suspense>
    </OuterAsyncComponent>
  </Suspense>
</template>

Тут и для OuterAsyncComponent, и для InnerAsyncComponent есть асинхронные загрузки. Внешний <Suspense> ждёт сначала OuterAsyncComponent, а затем — вложенный <Suspense>, у которого включено suspensible, следит за InnerAsyncComponent.

Suspense и Vue Router

Ещё один кейс — маршрутизация. Если в проекте используется vue-router с ленивой загрузкой компонентов (import() в routes), можно оборачивать <RouterView> в <Suspense>. Пример из документации:

<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Suspense>
      <component :is="Component" />
      <template #fallback>
        <div>Загрузка страницы...</div>
      </template>
    </Suspense>
  </template>
</RouterView>

Так каждый переход по роуту покажет единый спиннер вместо мелькания контента. Стандартная фича Vue Router (лёгкая динамическая загрузка компонентов) сама по себе не считается async компонентом для Suspense, то есть вьюха через <RouterView> просто подхватит лэндинг, и <Suspense> его не услышит. Но если внутри загруженной компоненты есть свои async‑зависимости, то Suspense их уже обработает как обычно.

Итоги

Итак, <Suspense> — мощный инструмент для корреляции загрузочных состояний в Vue 3. Он позволяет одним махом решить проблему всплывающих индикаторов в крупных приложениях и собрать их под единым флагом. Мы его используем для обёртки асинхронных компонентов или блоков с await в setup(), определяем слот #fallback с тем, что показать пользователю пока грузится контент, и не мучаемся с локальными флагами загрузки. Помним, что Suspense пока экспериментален (API может меняться), и что ошибки нужно ловить через errorCaptured. Кроме того, не забываем про события pending/resolve/fallback для настройки UX.


Suspense в Vue.js помогает грамотно управлять состояниями загрузки и избавляет проект от множества разрозненных индикаторов. Такой подход особенно важен в сложных интерфейсах, где пользователю важно видеть целостную картину, а не набор отдельных спиннеров. Чтобы уверенно работать с этим и другими инструментами Vue, стоит глубже изучить сам фреймворк и практики его применения.

Для этого вы можете пройти курс «Vue.js разработчик», где разберёте архитектуру, современные возможности и подходы к построению приложений. А если хотите проверить уровень своих знаний и навыков прямо сейчас, доступно тестирование по курсу. Оно поможет понять, насколько хорошо вы ориентируетесь в теме, и выявить пробелы для дальнейшего изучения.

Отзыв студента курса Vue.js разработчик
Отзыв студента курса Vue.js разработчик

А тем, кто настроен на серьезное системное обучение, крайне рекомендуем рассмотреть Подписку — выбираете курсы под свои задачи, экономите на обучении, получаете профессиональный рост. Узнать подробнее

Комментарии (0)