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

20. Streaming SSR

Streaming SSR позволяет браузеру получать и рендерить HTML постепенно, не дожидаясь полной генерации страницы на сервере. Remix поддерживает стриминг через defer() и React Streaming.

Без стриминга:
Сервер → [ждёт 3000ms] → [полный HTML] → Браузер показывает страницу
Со стримингом:
Сервер → [100ms] → [начало HTML] → Браузер начинает рендер
→ [500ms] → [секция 1] → Браузер обновляет
→ [3000ms]→ [секция 2] → Браузер обновляет
import { renderToPipeableStream } from "react-dom/server";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const userAgent = request.headers.get("user-agent");
const callbackName = isbot(userAgent ?? "")
? "onAllReady" // Боты ждут полного HTML
: "onShellReady"; // Пользователи — стриминг
return new Promise((resolve, reject) => {
let didError = false;
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} />,
{
[callbackName]() {
shellRendered = true;
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
})
);
pipe(body);
},
onShellError(error) {
reject(error);
},
onError(error) {
didError = true;
if (shellRendered) console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
export async function loader({ request }) {
// Параллельно запускаем запросы
const [user, meta] = await Promise.all([
getUser(request), // ~50ms — блокируем
getPageMeta(request), // ~100ms — блокируем
]);
// Медленные запросы — стримим
return defer({
user,
meta,
products: getProducts(), // ~1000ms — стриминг
reviews: getReviews(), // ~1500ms — стриминг
recommendations: getRecs(), // ~2000ms — стриминг
});
}

Для поисковых ботов стриминг отключается (они получают полный HTML):

const callbackName = isbot(userAgent)
? "onAllReady" // Ждём полной генерации для ботов
: "onShellReady"; // Стриминг для пользователей
// Если данные не пришли за 3 секунды — показываем fallback
const slowPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 3000)
);
return defer({
data: Promise.race([getSlowData(), slowPromise]),
});