Suspense и потоковая передача: лаборатория границ ожидания
Сравниваем момент доступности оболочки и готовность медленной части. Server Components и SSR — разные оси; Suspense задаёт границу ожидания, а streaming позволяет отправить готовое раньше остального. Он не ускоряет сам запрос данных. Proxy или браузер может буферизовать маленький ответ.
Опыт с серверным React без HTTP
Из apps/web выполните блок Node с существующими React 19 зависимостями.
Новый пакет и сервер не нужны. Данные разрешаются после получения оболочки,
поэтому опыт проверяет порядок без ненадёжного сравнения миллисекунд.
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { PassThrough } from 'node:stream';
import { createElement as h, Suspense, use } from 'react';
import { renderToPipeableStream } from 'react-dom/server';
let finish;
const data = new Promise(resolve => { finish = resolve; });
function Details() { return h('p', null, use(data)); }
const tree = h('main', null,
h('h1', null, 'Оболочка'),
h(Suspense, { fallback: h('p', null, 'Ожидание') }, h(Details)));
const output = new PassThrough();
let html = '';
let released = false;
let renderer;
output.on('data', chunk => {
html += chunk.toString();
if (!released && html.includes('Ожидание')) {
assert.ok(html.includes('Оболочка'));
assert.ok(!html.includes('Готовые данные'));
released = true;
finish('Готовые данные');
}
});
const completed = new Promise(resolve => output.on('end', resolve));
renderer = renderToPipeableStream(tree, {
onShellReady() { renderer.pipe(output); },
onError(error) { throw error; },
});
await completed;
assert.ok(released);
assert.ok(html.includes('Готовые данные'));
console.log('Shell before details; final stream contains details');
JS
Первый chunk содержит fallback; следующие содержат результат и служебные инструкции React. Не разбирайте этот формат собственным production-парсером. Проверка строки подтверждает серверный поток, но не исполнение инструкций в DOM и не гидратацию браузера. Удалите Suspense: оболочка дождётся Details, а условие разрешения Promise придётся перенести наружу, иначе эксперимент сам зависнет.
Перенести опыт на маршруты Next.js
В своём worktree создайте временный учебный маршрут с локальными вымышленными данными и задержкой, без CMS и новых зависимостей. Async компонент медленной части возвращает JSX после await; оболочку оставьте снаружи Suspense. Сравните:
| Вариант | Что ожидает медленные данные |
|---|---|
| await в page до return | Всё дерево, возвращаемое page |
| Suspense вокруг медленного ребёнка | Только эта граница |
| loading.tsx сегмента | Сегмент под автоматически созданной границей |
| Две независимые границы | Каждая может завершаться отдельно |
Запустите dev-сервер на собственном свободном порту. В Network найдите начало
ответа и его завершение; сравните curl -N и видимый fallback браузера.
Не принимайте накопленный вывод terminal или один скриншот за измерение streaming.
При переходе клиентского роутера есть RSC-запрос, при прямом открытии — документ;
это разные наблюдения. Не переносите данные dev-кеша на поведение production.
Исходник схемы
flowchart LR Start[Начало] --> Shell[Готовая оболочка] Shell --> Send[Первый chunk] Start --> Slow[Медленные данные] Slow --> Details[Следующий chunk]
HTTP-статус выбирают до отправки заголовков. Если поток уже начался, поздняя ошибка не превращает отправленный 200 в 500; нужна корректная граница ошибки и наблюдаемость. Не обещайте 404 только по появлению текста «не найдено». Текущие ограничения ISR/404 проекта описаны в SSG.
Приёмка: снимки последовательности, время первого содержимого и завершения, состояние ошибки, прямой заход и клиентский переход. Для опыта с Node фиксируем только порядок серверных chunks; браузер и Caddy проверяются отдельно. Учебный маршрут удаляют из своей ветки после опыта, в production он не нужен.
Источники: renderToPipeableStream, loading.tsx и streaming.