Учебник веб-разработки
Разделы учебника
На этой странице

VI. HTTP, Next.js и кеширование

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.