Пошук уроків, статей та іншого контенту
Застосуєте Suspense для меж завантаження та Streaming для поступової передачі UI із сервера.
Suspense — це компонент React, який визначає, що показати користувачу, поки дочірня частина UI ще не готова.
<Suspense fallback={<Loading />}>
<Profile />
</Suspense>Поки Profile не може бути відрендерений, React показує Loading. Коли компонент стає готовим, React замінює fallback на реальний UI.
Важливо: Suspense сам по собі не завантажує дані. Він лише створює межу очікування. Причиною призупинення можуть бути:
React.lazy;
бібліотека або фреймворк, що інтегрує Suspense із завантаженням даних;
серверний механізм React, який підтримує Suspense;
інші API React, що можуть призупинити рендеринг.
Звичайний fetch усередині useEffect не активує Suspense:
function Profile() {
const [profile, setProfile] = useState(null);
useEffect(() => {
fetch("/api/profile")
.then((response) => response.json())
.then(setProfile);
}, []);
if (!profile) {
return <p>Завантаження...</p>;
}
return <h2>{profile.name}</h2>;
}У цьому випадку компонент самостійно керує станом завантаження. Suspense не бере участі в такому сценарії.
Suspense найкраще використовувати для окремих частин сторінки, які можуть завантажуватися незалежно.
function Dashboard() {
return (
<main>
<h1>Панель керування</h1>
<Suspense fallback={<p>Завантаження профілю...</p>}>
<Profile />
</Suspense>
<Suspense fallback={<p>Завантаження статистики...</p>}>
<Statistics />
</Suspense>
</main>
);
}Тут профіль і статистика мають власні межі. Якщо статистика повільна, профіль не обов’язково повинен чекати на неї.
<Suspense fallback={<PageSkeleton />}>
<Profile />
<Statistics />
<Recommendations />
</Suspense>Цей варіант простіший, але весь внутрішній UI з'явиться лише після завершення найповільнішої частини.
<>
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
<Suspense fallback={<StatisticsSkeleton />}>
<Statistics />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</>Такий підхід дає змогу показувати готові частини сторінки поступово.
Межу варто розміщувати там, де користувач сприймає UI як окремий блок:
картка профілю;
список повідомлень;
графік;
рекомендації;
панель із другорядними даними.
Не варто створювати межу для кожного маленького текстового елемента. Надто дрібні fallback-елементи можуть зробити інтерфейс нестабільним і візуально миготливим.
Fallback — це не просто текст Завантаження.... Краще, щоб він приблизно повторював розмір і структуру майбутнього контенту.
function ProfileSkeleton() {
return (
<section aria-busy="true">
<div className="skeleton skeleton-avatar" />
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-text" />
</section>
);
}
function ProfileSection() {
return (
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
);
}Стабільний розмір fallback допомагає уникнути різкого зміщення сторінки після завантаження даних.
Під час традиційного SSR сервер спочатку повністю рендерить сторінку, а потім надсилає один готовий HTML-документ:
сервер отримує запит;
рендерить увесь React-UI;
чекає на найповільніші частини;
надсилає відповідь браузеру.
Якщо одна частина сторінки повільна, користувач може довго не бачити навіть ті частини, які вже готові.
Streaming змінює цей процес:
сервер швидко надсилає готову оболонку сторінки;
для незавершених частин надсилаються fallback-елементи;
готові частини поступово додаються до відповіді;
браузер замінює fallback на справжній контент.
У React Streaming працює разом із межами Suspense. Межа повідомляє React, яку частину можна тимчасово замінити fallback-ом, а серверний renderer надсилає результат частинами.
Для Node.js React надає API renderToPipeableStream з пакета react-dom/server.
Нижче наведено повний приклад без JSX, тому його можна запустити без додаткового транспілювання.
const http = require("node:http");
const React = require("react");
const { renderToPipeableStream } = require("react-dom/server");
const { createElement: h, Suspense } = React;
function Header() {
return h(
"header",
null,
h("h1", null, "Панель керування"),
h("p", null, "Ця частина готова одразу.")
);
}
function FastContent() {
return h(
"section",
null,
h("h2", null, "Швидкий блок"),
h("p", null, "Його сервер може відрендерити без затримки.")
);
}
const SlowContent = React.lazy(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve({
default: function SlowContentComponent() {
return h(
"section",
null,
h("h2", null, "Повільний блок"),
h("p", null, "Цей контент прийшов окремим фрагментом.")
);
},
});
}, 2000);
})
);
function App() {
return h(
"main",
null,
h(Header),
h(FastContent),
h(
Suspense,
{
fallback: h(
"section",
{ "aria-busy": "true" },
h("h2", null, "Повільний блок"),
h("p", null, "Завантаження...")
),
},
h(SlowContent)
)
);
}
const server = http.createServer((request, response) => {
if (request.url !== "/") {
response.statusCode = 404;
response.end("Not found");
return;
}
let didError = false;
const stream = renderToPipeableStream(h(App), {
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.write("<!doctype html><html><body>");
// Передаємо готову оболонку та fallback до браузера одразу.
stream.pipe(response, { end: false });
},
onAllReady() {
response.end("</body></html>");
},
onShellError(error) {
console.error(error);
response.statusCode = 500;
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.end("Помилка під час рендерингу сторінки");
},
onError(error) {
didError = true;
console.error(error);
},
});
setTimeout(() => {
stream.abort();
}, 10000);
});
server.listen(3000, () => {
console.log("Сервер запущено: http://localhost:3000");
});Для запуску:
npm init -y
npm install react react-dom
node server.jsВідкрийте http://localhost:3000. Спочатку сервер може віддати заголовок, швидкий блок і fallback повільного блоку. Приблизно через дві секунди React передасть результат SlowContent.
У production-застосунку до відповіді також додають клієнтський JavaScript для гідратації. Це дає змогу після отримання HTML під'єднати обробники подій і зробити UI інтерактивним.
renderToPipeableStream має кілька важливих callback-функцій.
onShellReadyВикликається, коли готова оболонка сторінки:
кореневий UI уже можна надсилати;
fallback-и для призупинених меж уже визначені;
браузер може почати отримувати HTML.
Для звичайної відповіді сторінки найчастіше використовують саме onShellReady.
onAllReadyВикликається після завершення всіх частин рендерингу.
Цей callback може бути корисним, коли потрібно дочекатися повністю готового HTML, наприклад для:
генерації HTML для пошукового робота;
збереження повної відповіді;
сценаріїв, де поступова передача не потрібна.
Якщо завжди чекати на onAllReady, перевага Streaming для звичайного браузерного запиту втрачається.
onShellErrorВикликається, якщо React не зміг створити навіть початкову оболонку. У такому випадку сервер може повернути окрему сторінку помилки.
onErrorВикликається для помилок під час рендерингу. Такі помилки потрібно записувати в журнали. Також доцільно змінити статус відповіді на 500, якщо помилка сталася до завершення початкової оболонки.
Streaming не гарантує, що всі компоненти з'являться в тому самому порядку, у якому завершилися їхні дані.
React зберігає структуру дерева:
<>
<Header />
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
<Suspense fallback={<MessagesSkeleton />}>
<Messages />
</Suspense>
</>Але Profile і Messages можуть бути готовими в різний час. Кожна межа замінюється окремо, тому користувач бачить поступове заповнення сторінки.
Це особливо корисно для сторінок, де:
основний контент готовий швидко;
додаткові блоки залежать від повільних запитів;
не потрібно блокувати всю сторінку через один компонент.
Suspense обробляє стан очікування, але не є обробником помилок.
Якщо під час рендерингу сталася помилка, для її обробки потрібен Error Boundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError() {
return {
hasError: true,
};
}
render() {
if (this.state.hasError) {
return <p>Не вдалося завантажити цей блок.</p>;
}
return this.props.children;
}
}
function Page() {
return (
<ErrorBoundary>
<Suspense fallback={<p>Завантаження...</p>}>
<Reports />
</Suspense>
</ErrorBoundary>
);
}У реальному застосунку межі помилок і Suspense часто використовують разом:
Suspense показує UI під час очікування;
Error Boundary показує UI після помилки.
Для середовищ із Web Streams, зокрема деяких edge-середовищ, React має API renderToReadableStream.
На відміну від renderToPipeableStream, який орієнтований на Node.js streams, renderToReadableStream повертає Web Stream:
import React, { Suspense } from "react";
import { renderToReadableStream } from "react-dom/server";
export default async function handler() {
const stream = await renderToReadableStream(
<Suspense fallback={<p>Завантаження...</p>}>
<App />
</Suspense>
);
return new Response(stream, {
headers: {
"Content-Type": "text/html; charset=utf-8",
},
});
}Конкретний код обробника залежить від середовища виконання. Принцип залишається тим самим: React повертає потік HTML, який можна передавати клієнту поступово.
Створюйте межі навколо самостійних блоків, а не навколо всієї сторінки без потреби.
Показуйте основний контент якомога раніше.
Використовуйте fallback, який має приблизно такий самий розмір, як фінальний UI.
Не сприймайте Suspense як автоматичний механізм отримання даних.
Обробляйте помилки окремо через Error Boundary та серверні callback-функції.
Для браузерних запитів зазвичай починайте передачу в onShellReady.
Не використовуйте onAllReady для кожного запиту, якщо мета — саме поступове відображення.
Перевіряйте Streaming у production-оточенні: проксі та CDN можуть буферизувати відповідь і приховувати ефект потокової передачі.
useEffect працюватиме через SuspenseuseEffect запускається після монтування компонента в браузері. Він не призупиняє серверний рендеринг і не активує fallback Suspense.
Для Suspense-сценарію потрібен механізм, який інтегрований із Suspense.
<Suspense fallback={<FullPageLoader />}>
<EntirePage />
</Suspense>Якщо лише один невеликий блок повільний, користувач не побачить навіть готовий контент інших частин.
Краще розділити незалежні блоки на кілька меж.
Якщо кожен рядок має власний fallback, сторінка може постійно змінювати структуру під час завантаження. Це ускладнює сприйняття UI.
Межа повинна відповідати помітній для користувача частині інтерфейсу.
Саме використання Suspense не означає, що HTML автоматично передаватиметься потоково. Для цього сервер має використовувати renderer, який підтримує Streaming, наприклад renderToPipeableStream або renderToReadableStream.
Якщо сервер чекає завершення всіх компонентів перед відправленням відповіді, поступової передачі UI не буде.
Для браузерних запитів початкову оболонку зазвичай передають у onShellReady.
Fallback, який сильно відрізняється за розміром від реального контенту, може спричинити зміщення сторінки після завантаження.
Використовуйте skeleton-елементи або стабільний контейнер.
Suspense визначає межу, всередині якої React може показати fallback.
Suspense не завантажує дані самостійно.
Кілька меж дають змогу відображати незалежні частини UI окремо.
Streaming передає HTML із сервера частинами.
renderToPipeableStream використовується для Node.js streams.
renderToReadableStream використовується в середовищах із Web Streams.
onShellReady дає змогу швидко передати оболонку та fallback-и.
onAllReady очікує завершення всіх частин.
Помилки очікування та помилки виконання потрібно обробляти різними механізмами.