Перейти к содержимому

31. Streaming SSR

Streaming SSR — это техника серверного рендеринга, при которой HTML отправляется клиенту частями по мере готовности, а не целиком после того, как все данные загружены. Solid.js поддерживает потоковую передачу из коробки через renderToStream.

В классическом SSR (renderToString) клиент ждёт, пока сервер:

  1. Получит запрос
  2. Загрузит все данные для страницы (API, БД)
  3. Сгенерирует полный HTML
  4. Отправит ответ

Если один из API-запросов медленный — весь пользователь ждёт:

Клиент запрашивает страницу
Сервер: загружает данные (500мс)...............
Сервер: рендерит HTML ─────────────────────────→ Клиент получает HTML (500мс задержки)

При потоковом SSR клиент сразу получает “скелет” страницы, а контент подгружается по мере готовности:

Клиент запрашивает страницу
▼ 0мс
Сервер: → <html><head>...</head><body>... (отправляет сразу!)
│ 50мс
Сервер: → <header>...</header><nav>... (готово, отправляем)
│ 200мс
Сервер: → <article>...</article> (загружен из БД, отправляем)
│ 500мс
Сервер: → <aside>рекомендации...</aside></body></html> (последний блок)
import { renderToString, renderToStream } from 'solid-js/web';
// Классический SSR — ждёт всех данных
const html = await renderToString(() => <App />);
res.send(html);
// Streaming SSR — отдаёт HTML по мере готовности
const stream = renderToStream(() => <App />);
// stream — это Node.js ReadableStream
stream.pipe(res);
// С заголовками (чтобы браузер не буферизовал)
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
renderToStream(() => <App />).pipe(res);

Ключевая концепция: <Suspense> определяет границы потока. Каждый Suspense отдаётся независимо:

function Page() {
return (
<html>
<body>
{/* Этот блок придёт сразу — нет async данных */}
<Header />
<nav>...</nav>
{/* Этот блок придёт, когда загрузится статья (200мс) */}
<Suspense fallback={<ArticleSkeleton />}>
<Article />
</Suspense>
{/* Этот блок придёт последним (500мс) */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</body>
</html>
);
}

При потоковом SSR браузер:

  1. Получает статичный HTML немедленно, показывает скелетон
  2. Получает chunk с <Article> через 200мс, заменяет скелетон
  3. Получает chunk с <Sidebar> через 500мс, заменяет скелетон
import { lazy } from 'solid-js';
import { Suspense } from 'solid-js';
// Lazy-компонент разделяет код (code splitting) и интегрируется со Streaming
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<QuickStats /> {/* Приходит первым */}
<Suspense fallback={<div>Загрузка графика...</div>}>
<HeavyChart /> {/* Приходит когда JS-бандл загружен */}
</Suspense>
</div>
);
}

Solid.js поддерживает out-of-order streaming — Suspense-блоки могут разрешаться в любом порядке, не только последовательно:

// Сервер отправляет блоки в порядке готовности:
// 1. Сначала <Header> (0мс)
// 2. Потом <Sidebar> (100мс) — хотя стоит ПОСЛЕ <Article> в JSX!
// 3. Потом <Article> (300мс)
function Page() {
return (
<>
<Header />
<Suspense fallback={<ArticleSkeleton />}>
<Article /> {/* 300мс */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* 100мс — придёт первым из двух! */}
</Suspense>
</>
);
}

Под капотом Solid вставляет <script> теги, которые манипулируют DOM для замены скелетонов в правильные позиции.

app.config.ts
import { defineConfig } from '@solidjs/start/config';
export default defineConfig({
server: {
// Включаем streaming
experimental: {
asyncContext: true,
}
}
});
// src/routes/index.tsx — streaming работает автоматически с Suspense
export default function Home() {
return (
<main>
<h1>Мой сайт</h1>
<Suspense fallback={<p>Загрузка постов...</p>}>
<PostList />
</Suspense>
</main>
);
}
МетрикаrenderToStringrenderToStream
TTFB (Time to First Byte)После загрузки всех данныхНемедленно
FCP (First Contentful Paint)После всего HTMLПосле первого chunk
LCP (Largest Contentful Paint)После всего HTMLЗависит от расположения
TTI (Time to Interactive)После гидратацииProgressive
СложностьПрощеТребует настройки

Симуляция потокового SSR — наблюдай, как HTML-чанки прибывают один за другим: