Этот пост рассказывает о том, как работает Suspense, что он делает, и о том, как он может интегрироваться в настоящее веб-приложение. Мы рассмотрим, как интегрировать маршрутизацию и загрузку данных с Suspense in React. Для маршрутизации я буду использовать vanilla JavaScript и свою собственную библиотеку GraphQL micro-graphql-Reaction для данных.
Если вас интересует React Router, он выглядит великолепно, но у меня никогда не было возможности его использовать. У моего собственного стороннего проекта есть достаточно простая история маршрутизации, которую я всегда делал вручную. Кроме того, использование ванильного JavaScript поможет нам лучше понять, как работает Suspense.
Содержание статьи
Небольшой фон
Давайте поговорим о самом Suspense. Кингсли Сайлас (Kingsley Silas) дает подробный обзор этого, но первое, что нужно отметить, это то, что это все еще экспериментальный API. Это означает — и документы React говорят то же самое — пока не полагаться на него для готовой к работе работы. Всегда есть вероятность, что он изменится между настоящим моментом и когда он будет полностью завершен, поэтому имейте это в виду.
Тем не менее, Suspense — это все о поддержании согласованного пользовательского интерфейса в условиях асинхронных зависимостей, таких как лениво загруженные компоненты React, данные GraphQL и т. Д. В Suspense предусмотрены низкоуровневые API, которые позволяют легко поддерживать пользовательский интерфейс во время работы приложения. управляет этими вещами.
Но что означает «последовательный» в данном случае? Это означает, что не отображать частично завершенный пользовательский интерфейс. Это означает, что если на странице есть три источника данных, и один из них завершен, мы не хотим рендерить этот обновленный фрагмент состояния с вращением рядом с устаревшими двумя другими. кусочки государства.
Что мы хотим сделать это указать пользователю, что данные загружаются, продолжая показывать либо старую версию Пользовательский интерфейс или альтернативный пользовательский интерфейс, который указывает, что мы ожидаем данных; Приостановка поддерживает либо, в который я войду.
Что именно делает Suspense
Это все менее сложно, чем может показаться. Традиционно в React вы устанавливаете состояние, и ваш интерфейс обновляется. Жизнь была проста. Но это также привело к разным несоответствиям, описанным выше. Что добавляет Suspense, так это способность компонента уведомлять React во время рендеринга, что он ожидает асинхронных данных; это называется приостановкой, и это может происходить в любом месте дерева компонента столько раз, сколько необходимо, пока дерево не будет готово. Когда компонент приостанавливается, React будет отказываться отображать обновление состояния ожидания до тех пор, пока не будут удовлетворены все приостановленные зависимости.
Так что же происходит, когда компонент приостанавливается? React найдет дерево, найдет первый компонент
и предоставит его запасной вариант. Я приведу множество примеров, но пока знаю, что вы можете предоставить это:
<Откат ожидания = { }>
… и компонент
будут визуализироваться, если приостановлены какие-либо дочерние компоненты
.
Но что, если у нас уже есть действительный, согласованный пользовательский интерфейс, и пользователь загружает новые данные, вызывая приостановку компонента? Это может привести к отмене рендеринга всего существующего пользовательского интерфейса и появлению запасного варианта. Это все равно будет последовательным, но вряд ли хорошим UX. Мы бы предпочли, чтобы старый интерфейс оставался на экране во время загрузки новых данных.
Для поддержки этого React предоставляет второй API, useTransition, который эффективно выполняет изменение состояния в памяти . Другими словами, он позволяет вам устанавливать состояние в памяти, сохраняя существующий интерфейс на экране; React в буквальном смысле сохранит вторую копию вашего дерева компонентов, отображенную в памяти, и установит состояние на этого дерева . Компоненты могут приостанавливаться, но только в памяти, поэтому ваш существующий пользовательский интерфейс будет продолжать отображаться на экране. Когда изменение состояния завершено и все приостановки разрешены, изменение состояния в памяти будет отображаться на экране. Очевидно, что вы хотите предоставить обратную связь своему пользователю, пока это происходит, поэтому useTransition
предоставляет ожидающий логический
лог, который можно использовать для отображения своего рода встроенного уведомления о «загрузке», пока приостановки разрешается в памяти.
Когда вы думаете об этом, вы, вероятно, не хотите, чтобы существующий пользовательский интерфейс отображался бесконечно, пока ваша загрузка еще не завершена. Если пользователь пытается что-то сделать, и до его завершения проходит длительный период времени, вы, вероятно, должны считать существующий пользовательский интерфейс устаревшим и недействительным. В этот момент вы, вероятно, захотите чтобы ваше дерево компонентов было приостановлено, и ваш
откроется для отображения.
Для этого useTransition
принимает значение timeoutMs
. Это указывает количество времени, которое вы готовы дать изменению состояния в памяти, прежде чем вы приостановите.
const Component = props => {
const [startTransition, isPending] = useTransition ({timeoutMs: 3000});
// .....
};
Здесь startTransition
является функцией. Когда вы хотите запустить изменение состояния «в памяти», вы вызываете startTransition
и передаете лямбда-выражение, которое изменяет ваше состояние.
startTransition (() => {
рассылка ({тип: LOAD_DATA_OR_SOMETHING, значение: 42});
})
Вы можете позвонить startTransition
куда угодно. Вы можете передать его дочерним компонентам и т. Д. При его вызове любое изменение состояния, которое вы выполняете, произойдет в памяти. Если произойдет приостановка, isPending
станет истинным, что можно использовать для отображения какого-либо встроенного индикатора загрузки.
Вот и все. Это то, что делает Suspense.
Оставшаяся часть этого поста будет посвящена конкретному коду для использования этих функций.
Чтобы связать навигацию с Suspense, вы будете рады узнать, что React предоставляет примитив для этого: React.lazy
. Это функция, которая принимает лямбда-выражение, которое возвращает Promise, который разрешается в компонент React. Результатом этого вызова функции становится ваш лениво загруженный компонент. Звучит сложно, но выглядит так:
const SettingsComponent = lazy (() => import ("./ modules / settings / settings"));
SettingsComponent
теперь является компонентом React, который при визуализации (но не раньше) будет вызывать переданную нами функцию, которая будет вызывать import ()
и загружать модуль JavaScript находится в ./ модули / настройки / настройки
.
Ключевой момент таков: пока import ()
находится в полете, компонент рендеринга SettingsComponent
будет приостановлен. Кажется, у нас есть все части в руках, поэтому давайте соберем их вместе и создадим некоторую навигацию на основе Suspense.
Но сначала для контекста я кратко расскажу о том, как управлять состоянием навигации в этом приложении, поэтому код Suspense будет иметь больше смысла.
Я буду использовать приложение из своего списка книг. Это всего лишь мой сторонний проект, который я в основном продолжаю возиться с новейшими веб-технологиями. Он был написан мною один, так что ожидайте, что его части будут немного не уточнены (особенно дизайн).
Приложение маленькое, с восемью различными модулями, которые пользователь может просматривать без какой-либо более глубокой навигации. Любое состояние поиска, которое может использовать модуль, сохраняется в строке запроса URL. Имея это в виду, есть несколько методов, которые очищают текущее имя модуля и состояние поиска от URL. Этот код использует пакеты строки запросов
и history
из npm и выглядит примерно так (некоторые детали были удалены для простоты, например, аутентификации).
импортировать createHistory из "history / createBrowserHistory";
импортировать queryString из "строки запроса";
экспортировать const history = createHistory ();
функция экспорта getCurrentUrlState () {
let location = history.location;
let parsed = queryString.parse (location.search);
возвращение {
путь: location.pathname,
searchState: проанализирован
};
}
функция экспорта getCurrentModuleFromUrl () {
let location = history.location;
return location.pathname.replace (/ // g, "") .toLowerCase ();
}
У меня есть редуктор appSettings
который содержит текущий модуль и значения searchState
для приложения, и использует эти методы для синхронизации с URL при необходимости.
Давайте начнем с некоторой работы Suspense. Сначала давайте создадим лениво загруженные компоненты для наших модулей.
const ActivateComponent = lazy (() => import ("./ modules / activate / activ"));
const AuthenticateComponent = lazy (() =>
импорт ( "./ модули / Аутентифицировать / Аутентифицировать")
);
const BooksComponent = lazy (() => import ("./ modules / books / books"));
const HomeComponent = lazy (() => import ("./ modules / home / home"));
const ScanComponent = lazy (() => import ("./ modules / scan / scan"));
const SubjectsComponent = lazy (() => import ("./ modules / subject / subject"));
const SettingsComponent = lazy (() => import ("./ modules / settings / settings"));
const AdminComponent = lazy (() => import ("./ modules / admin / admin"));
Теперь нам нужен метод, который выбирает правильный компонент на основе текущего модуля. Если бы мы использовали React Router, у нас были бы хорошие компоненты
. Поскольку мы выполняем это вручную, подойдет переключатель
.
export const getModuleComponent = moduleToLoad => {
if (moduleToLoad == null) {
вернуть ноль;
}
switch (moduleToLoad.toLowerCase ()) {
кейс "активировать":
вернуть ActivateComponent;
дело "аутентифицировать":
return AuthenticateComponent;
Кейс "Книги":
вернуть BooksComponent;
Дело "дома":
возврат HomeComponent;
кейс "скан":
возврат ScanComponent;
кейс "предметы":
return SubjectsComponent;
case "settings":
вернуть SettingsComponent;
дело "админ":
вернуть AdminComponent;
}
возврат HomeComponent;
};
Все это вместе взятые
Со всеми скучными настройками, давайте посмотрим, как выглядит весь корень приложения. Здесь много кода, но я обещаю, относительно небольшое количество этих строк относится к Suspense, и я расскажу обо всем этом.
const App = () => {
const [startTransitionNewModule, isNewModulePending] = useTransition ({
timeoutMs: 3000
});
const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition ({
timeoutMs: 3000
});
let appStatePacket = useAppState ();
let [appState, _, dispatch] = appStatePacket;
let Component = getModuleComponent (appState.module);
useEffect (() => {
startTransitionNewModule (() => {
рассылка ({type: URL_SYNC});
});
}, []);
useEffect (() => {
вернуть history.listen (location => {
if (appState.module! = getCurrentModuleFromUrl ()) {
startTransitionNewModule (() => {
рассылка ({type: URL_SYNC});
});
} еще {
startTransitionModuleUpdate (() => {
рассылка ({type: URL_SYNC});
});
}
});
}, [appState.module]);
возвращение (
{isNewModulePending? : null}
<Suspense fallback = { }>
{Компонент ? : null}
);
};
Во-первых, у нас есть два разных вызова useTransition
. Мы будем использовать один для маршрутизации на новый модуль, а другой — для обновления состояния поиска для текущего модуля. Почему разница? Что ж, когда состояние поиска модуля обновляется, этот модуль, вероятно, захочет отобразить индикатор встроенной загрузки. Это состояние обновления хранится в переменной moduleUpdatePending
которую вы увидите, я добавлю контекст для захвата активного модуля и при необходимости используем:
{isNewModulePending? : null}
<Suspense fallback = { }>
{Компонент ? : null} // выделение
appStatePacket
является результатом средства уменьшения состояния приложения, которое я обсуждал выше (но не показывал). Он содержит различные части состояния приложения, которые редко меняются (цветовая тема, автономный статус, текущий модуль и т. Д.).
let appStatePacket = useAppState ();
Чуть позже я беру любой активный компонент, основываясь на текущем имени модуля. Первоначально это будет нулевым.
let Component = getModuleComponent (appState.module);
Первый вызов useEffect
сообщит нашему редуктору appSettings
о синхронизации с URL при запуске.
useEffect (() => {
startTransitionNewModule (() => {
рассылка ({type: URL_SYNC});
});
}, []);
Поскольку это начальный модуль, к которому обращается веб-приложение, я включаю его в startTransitionNewModule
чтобы указать, что загружается новый модуль. Хотя может показаться заманчивым, что редуктор appSettings
имеет начальное имя модуля в качестве своего начального состояния, но это не позволяет нам вызывать наш обратный вызов startTransitionNewModule
что означает, что наша граница приостановки выдаст отступление немедленно, а не после тайм-аута.
Следующий вызов useEffect
устанавливает подписку истории. Независимо от того, что, когда URL изменяется, мы говорим нашим настройкам приложения синхронизироваться с URL. Разница лишь в том, что startTransition
содержит тот же вызов.
useEffect (() => {
вернуть history.listen (location => {
if (appState.module! = getCurrentModuleFromUrl ()) {
startTransitionNewModule (() => {
рассылка ({type: URL_SYNC});
});
} еще {
startTransitionModuleUpdate (() => {
рассылка ({type: URL_SYNC});
});
}
});
}, [appState.module]);
Если мы просматриваем новый модуль, мы вызываем startTransitionNewModule
. Если мы загружаем компонент, который еще не был загружен, React.lazy
будет приостановлен, и будет установлен индикатор ожидания, видимый только для корня приложения, который будет отображать загрузочный счетчик в верхней части приложение в то время как ленивый компонент извлекается и загружается. Из-за того, как работает useTransition
текущий экран будет отображаться в течение трех секунд. Если это время истечет, а компонент все еще не готов, наш пользовательский интерфейс будет приостановлен, и будет выполнен откат, который покажет компонент
:
{isNewModulePending? : null}
<Suspense fallback = { }>
{Компонент ? : null}
Если мы не меняем модули, мы вызываем startTransitionModuleUpdate
:
startTransitionModuleUpdate (() => {
рассылка ({type: URL_SYNC});
});
Если обновление вызывает приостановку, сработает индикатор ожидания, который мы помещаем в контекст. Активный компонент может обнаружить это и показать любой индикатор встроенной загрузки, который он хочет. Как и раньше, если приостановка занимает более трех секунд, сработает та же самая граница приостановки, что и раньше … если, как мы увидим позже, граница приостановки в дереве ниже.
Важно отметить, что эти трехсекундные тайм-ауты применяются не только к загрузке компонента, но и к готовности к отображению. Если компонент загружается через две секунды и при рендеринге в памяти (поскольку мы находимся внутри вызова startTransition
) приостанавливается, useTransition
будет продолжать до подождите еще одну секунду до приостановки.
При написании этого поста в блоге я использовал медленные сетевые режимы Chrome, чтобы ускорить загрузку, чтобы проверить границы Suspense. Настройки находятся на вкладке Сеть инструментов разработчика Chrome.
Давайте откроем наше приложение для модуля настроек. Это будет называться:
рассылка ({type: URL_SYNC});
Наш редуктор appSettings
синхронизируется с URL-адресом, а затем устанавливает модуль в «настройки». Это будет происходить внутри startTransitionNewModule
так что, когда лениво загруженный компонент пытается выполнить рендеринг, он будет приостановлен. Так как мы внутри startTransitionNewModule
isNewModulePending
переключится на true
и компонент
будет отображаться.
]
Так что же происходит, когда мы просматриваем что-то новое? В основном то же самое, что и раньше, за исключением этого вызова:
рассылка ({type: URL_SYNC});
… прибудет из второго экземпляра useEffect
. Давайте перейдем к модулю книг и посмотрим, что произойдет. Во-первых, встроенный счетчик показывает, как и ожидалось:
Поиск и обновление
Давайте останемся в модуле книг и обновим строку поиска URL, чтобы начать новый поиск. Напомним, что ранее мы обнаруживали один и тот же модуль во втором вызове useEffect
и использовали для него выделенный вызов useTransition
. Оттуда мы помещали индикатор ожидания в контекст для любого модуля, который был активен для нас, чтобы захватить и использовать.
Давайте посмотрим некоторый код, чтобы фактически использовать это. Здесь не так много кода, связанного с Suspense. Я беру значение из контекста и, если оно истинно, отображаю встроенный счетчик поверх моих существующих результатов. Напомним, что это происходит, когда начался вызов useTransition
и приложение приостановлено в памяти . Пока это происходит, мы продолжаем показывать существующий пользовательский интерфейс, но с этим индикатором загрузки.
const BookResults: SFC <{ books: any; uiView: any }> = ({books, uiView}) => {
const isUpdating = useContext (ModuleUpdateContext);
возвращение (
<>
{! books.length? (
Книги не найдены
) : значение NULL}
{isUpdating? : null}
{uiView.isGridView? (
): uiView.isBasicList? (
): uiView.isCoversList? (
) : значение NULL}
>
);
};
Давайте установим поисковый запрос и посмотрим, что произойдет. Сначала отображается встроенный счетчик.
Тогда, если истечет тайм-аут useTransition
мы получим откат границы приостановки. Модуль books определяет собственную границу Suspense, чтобы обеспечить более точный индикатор загрузки, который выглядит следующим образом:
Это ключевой момент. При создании резервных границ Suspense старайтесь не выбрасывать какие-либо прядильщики и «загружать» сообщения. Это имело смысл для нашей навигации верхнего уровня, потому что больше ничего не нужно делать. Но когда вы находитесь в определенной части вашего приложения, попытайтесь заставить ваш запасной вариант повторно использовать многие из тех же самых компонентов с каким-то индикатором загрузки, где будут данные - но с другим отключенным.
Вот как выглядят соответствующие компоненты для модуля моих книг:
const RenderModule: SFC <{}> = ({}) => {
const uiView = useBookSearchUiView ();
const [lastBookResults, setLastBookResults] = useState ({
Всего страниц: 0,
ResultsCount: 0
});
возвращение (
<Suspense fallback = { }>
);
};
const Fallback: SFC <{
uiView: BookSearchUiView;
totalPages: number;
resultsCount: number;
}> = ({uiView, totalPages, resultsCount}) => {
возвращение (
<>
{uiView.isGridView? (
): (
Книги загружаются
)}
>
);
};
Краткое примечание о согласованности
Прежде чем мы продолжим, я хотел бы отметить одну вещь из предыдущих снимков экрана. Посмотрите на встроенный счетчик, который отображается во время ожидания поиска, затем посмотрите на экран, когда поиск приостановлен, и затем законченные результаты:
Обратите внимание, что справа от области поиска есть метка «C ++» с возможностью ее удаления из поискового запроса? Или, точнее, обратите внимание, как эта метка есть только на вторых двух скриншотах? В момент обновления URL-адреса состояние приложения, управляющее этой меткой обновляется ; однако это состояние изначально не отображается. Первоначально обновление состояния приостанавливается в памяти (так как мы использовали useTransition), а пользовательский интерфейс до продолжает отображаться.
Затем отступает. Откат отображает отключенную версию той же панели поиска, которая показывает текущее состояние поиска (по выбору). Теперь мы удалили наш предыдущий пользовательский интерфейс (поскольку он довольно старый и устаревший) и ждем поиска, показанного в отключенной строке меню.
Такого рода последовательность дает вам Suspense бесплатно.
Вы можете тратить свое время на создание хороших состояний приложения, а React выполняет всю работу по предположению, готовы ли вещи, без необходимости подтасовывать обещания.
Вложенные границы Suspense
Предположим, что нашей навигации верхнего уровня требуется некоторое время, чтобы загрузить наш компонент книги до такой степени, что рендеринг «Все еще загружается, извините» с границы приостановки. Оттуда загружается компонент books и создается новая граница Suspense внутри компонента books. Но затем, когда рендеринг продолжается, наш запрос поиска книг срабатывает и приостанавливается. Что случится? Будет ли продолжать отображаться граница Подвески верхнего уровня до тех пор, пока все не будет готово, или верхняя граница Подвески в книгах вступит во владение?
Ответ последний. По мере того, как новые границы Suspense отрисовываются в дереве ниже, их запасной вариант заменит запасной вариант того, что ранее демонстрировалось. В настоящее время существует нестабильный API-интерфейс для его переопределения, но если вы хорошо справляетесь с созданием запасных вариантов, это, вероятно, именно то поведение, которое вам нужно. Вы не хотите, чтобы «Все еще загружается, извините» просто показывать Скорее, как только компонент books будет готов, вам абсолютно необходимо отобразить эту оболочку с более целенаправленным ожидающим сообщением.
Теперь, что, если наш модуль «Книги» загружается и начинает рендериться, пока спиннер startTransition
все еще показывает, а затем приостанавливается? Другими словами, представьте, что наше startTransition
имеет тайм-аут в три секунды, рендеринг компонента books, вложенная граница Suspense находится в дереве компонентов через одну секунду, а поисковый запрос приостанавливается. Пройдут ли оставшиеся две секунды до того, как новая вложенная граница Suspense отобразит запасной вариант, или резервный отклик отобразится немедленно? Ответ, возможно удивительный, заключается в том, что новый запасной вариант Suspense покажет сразу по умолчанию. Это потому, что лучше всего показывать новый, действительный пользовательский интерфейс как можно быстрее, чтобы пользователь мог видеть, что что-то происходит и прогрессирует.
Как данные вписываются в
Навигация в порядке, но как загрузка данных вписывается во все это?
Он полностью и прозрачно вписывается. Загрузка данных вызывает приостановки точно так же, как навигация с React.lazy
и она подключается к тем же useTransition
и Suspense границам. Вот что удивительного в Suspense: все ваши асинхронные зависимости бесперебойно работают в этой же системе. Управление этими различными асинхронными запросами вручную для обеспечения согласованности было кошмаром перед Suspense, именно поэтому никто не делал этого. Веб-приложения были печально известны тем, что каскадные вращатели останавливались в непредсказуемое время, создавая непоследовательные интерфейсы, которые были только частично завершены.
Хорошо, но как мы можем связать загрузку данных с этим? Загрузка данных в Suspense, как это ни парадоксально, является одновременно более сложной и простой.
Я объясню.
Если вы ожидаете данных, вы добавите обещание в компонент, который читает (или пытается прочитать) данные. Обещание должно быть последовательным в зависимости от запроса данных. Таким образом, четыре повторных запроса для одного и того же поискового запроса "C ++" должны давать одно и то же обещание. Это подразумевает некоторый уровень кэширования для управления всем этим. Вы, вероятно, не будете писать это сами. Вместо этого вы просто будете надеяться и ждать, пока библиотека данных, которую вы используете, обновится для поддержки Suspense.
Это уже сделано в моей библиотеке micro-graphql-реагировать. Вместо использования ловушки useQuery
вы будете использовать ловушку useSuspenseQuery
которая имеет идентичный API, но выдает непротиворечивое обещание, когда вы ожидаете данных.
Подождите, а как насчет предварительной загрузки?!
Ваш мозг переключился на чтение других вещей на Suspense, в которых говорилось о водопадах, выборке при рендеринге, предварительной загрузке и т. Д.? Не беспокойся об этом. Вот что все это значит.
Допустим, вы лениво загружаете компонент books, который отображает а затем запрашивает некоторые данные, что вызывает новый Suspense. Сетевой запрос для компонента и сетевой запрос данных будут выполняться один за другим - в виде водопада.
Но вот ключевая часть: состояние приложения, которое привело к любому начальному запросу, который выполнялся, когда загруженный компонент был уже доступен, когда вы начали загружать компонент (который, в данном случае, является URL-адресом). Так почему бы не «запустить» запрос, как только вы узнаете, что он вам понадобится? Как только вы перейдете к / books
почему бы сразу не запустить текущий поисковый запрос, так что он уже в полете, когда загружается компонент.
Модуль micro-graphql-реакции действительно имеет метод предварительной загрузки
и я призываю вас использовать его. Предварительная загрузка данных - это хорошая оптимизация производительности, но она не имеет ничего общего с Suspense. Классические приложения React могут (и должны) предварительно загружать данные, как только узнают, что они им понадобятся. Приложения Vue должны предварительно загружать данные, как только узнают, что они им понадобятся. Приложения Svelte должны ... Вы понимаете, в чем дело.
Предварительная загрузка данных перпендикулярна Suspense, что вы можете сделать буквально в любой среде. Это также то, что мы все должны были уже делать, хотя никто другой этого не делал.
А если серьезно, как вы предварительно загружаете?
Это зависит от вас. По крайней мере, логика для запуска текущего поиска должна быть полностью разделена на отдельный модуль. Вы должны буквально убедиться, что эта функция предварительной загрузки находится в файле сама по себе. Не полагайтесь на веб-пакет на treehake; вы, скорее всего, столкнетесь с грустью в следующий раз, когда будете проверять свои связки.
У вас есть метод preload ()
в его собственном пакете, поэтому вызывайте его. Позвоните, когда узнаете, что собираетесь перейти к этому модулю. Я предполагаю, что React Router имеет своего рода API для запуска кода при смене навигации. Для приведенного выше ванильного кода маршрутизации я вызываю метод в этом коммутаторе маршрутизации ранее. Для краткости я его опустил, но на самом деле запись в книге выглядит так:
переключатель (moduleToLoad.toLowerCase ()) {
кейс "активировать":
вернуть ActivateComponent;
дело "аутентифицировать":
return AuthenticateComponent;
Кейс "Книги":
// предзагрузка !!!
booksPreload ();
возвращение BooksComponent;
Вот и все. Вот живая демонстрация, с которой можно поиграть:
Чтобы изменить значение времени ожидания приостановки, которое по умолчанию составляет 3000 мс, перейдите в «Настройки» и откройте вкладку «Разное». Обязательно обновите страницу после ее изменения.
Подведение итогов
Я редко был настолько взволнован чем-либо в экосистеме веб-разработчиков, как я - Suspense. Это невероятно амбициозная система для решения одной из самых сложных проблем в веб-разработке: асинхронности.