Реализация Streaming SSR для веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Streaming SSR для веб-приложения
Сложная
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация Streaming SSR для веб-приложения

Классический SSR блокирует: сервер ждёт всех данных, рендерит полный HTML, отправляет. Пока самый медленный запрос к базе не завершится — браузер не получит ни байта. Streaming SSR разрывает этот блок: HTML отдаётся браузеру по мере готовности частей страницы, через HTTP chunked transfer encoding.

Браузер начинает парсить и отображать HTML немедленно. Slow parts появляются позже. Пользователь видит что-то на экране за 100–200ms даже если полный рендер занимает секунду.

Механика Streaming SSR

HTTP/1.1 поддерживает Transfer-Encoding: chunked — сервер отправляет данные частями без заголовка Content-Length. React и Vue используют это для стриминга:

Клиент получает:
[Chunk 1 — 50ms] <html><head>...</head><body><header>...</header>
[Chunk 2 — 120ms] <main><nav>...</nav><div id="products-suspense"><!--$?--><template id="B:0">...skeleton...</template>
[Chunk 3 — 340ms] <div hidden id="S:0"><ul><li>Товар 1</li>...<!-- Данные готовы --></ul></div><script>$RC("B:0","S:0")</script>
[Chunk 4 — 890ms] <aside><!-- Рекомендации --></aside></main></body></html>

Браузер рендерит каждый chunk сразу. $RC — скрипт React для замены skeleton на реальный контент.

Реализация в React 18 + Next.js

// app/catalog/page.tsx
import { Suspense } from 'react';

// Намеренно медленный компонент — симулирует запрос к внешнему API
async function FeaturedProducts() {
  // Медленный внешний сервис — 800ms
  const products = await fetch('https://api.example.com/featured', {
    next: { revalidate: 300 }
  }).then(r => r.json());

  return (
    <ul>
      {products.map((p: Product) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

async function Categories() {
  // Быстрый внутренний запрос — 50ms
  const cats = await db.category.findMany();
  return <nav>{cats.map(c => <a key={c.id} href={`/catalog/${c.slug}`}>{c.name}</a>)}</nav>;
}

export default function CatalogPage() {
  return (
    <div>
      {/* Рендерится немедленно — синхронный */}
      <h1>Каталог</h1>

      {/* Быстрый блок — появится за ~50ms */}
      <Suspense fallback={<CategoriesSkeleton />}>
        <Categories />
      </Suspense>

      {/* Медленный блок — появится за ~800ms, остальное не блокирует */}
      <Suspense fallback={<ProductsGridSkeleton count={6} />}>
        <FeaturedProducts />
      </Suspense>
    </div>
  );
}

Параллельный фетчинг данных

Критическая ошибка — последовательные await (waterfall):

// Плохо: 200ms + 500ms + 300ms = 1000ms ожидания
async function Page() {
  const user = await getUser();           // 200ms
  const orders = await getOrders(user.id); // 500ms
  const stats = await getStats(user.id);   // 300ms
  ...
}

// Хорошо: max(200ms, 500ms, 300ms) = 500ms
async function Page() {
  const userPromise = getUser();
  const [user, orders, stats] = await Promise.all([
    userPromise,
    userPromise.then(u => getOrders(u.id)),
    userPromise.then(u => getStats(u.id)),
  ]);
  ...
}

Ещё лучше — разнести независимые данные по разным Suspense-компонентам, чтобы они стримились параллельно без единого Promise.all:

export default function DashboardPage() {
  // Все три запроса запускаются параллельно
  // Каждый блок появляется как только его данные готовы
  return (
    <main>
      <Suspense fallback={<UserCardSkeleton />}>
        <UserCard />         {/* getUser() — 200ms */}
      </Suspense>

      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />     {/* getOrders() — 500ms */}
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <DashboardStats />   {/* getStats() — 300ms */}
      </Suspense>
    </main>
  );
}

Стриминг в Node.js без фреймворка

Для кастомных серверов — renderToPipeableStream:

import { renderToPipeableStream } from 'react-dom/server';
import { createServer } from 'http';

createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');

  const { pipe, abort } = renderToPipeableStream(<App url={req.url} />, {
    bootstrapScripts: ['/static/js/main.js'],

    onShellReady() {
      // Shell готов (контент до первого Suspense) — начинаем стриминг
      res.statusCode = 200;
      pipe(res);
    },

    onShellError(error) {
      // Shell не рендерится — fallback к CSR
      res.statusCode = 500;
      res.end('<html><body><div id="root"></div></body></html>');
    },

    onError(error) {
      console.error('Streaming error:', error);
    },
  });

  // Таймаут для очень медленных компонентов
  setTimeout(() => abort(), 10000);
}).listen(3000);

Edge Streaming с Cloudflare Workers

// worker.ts
import { renderToReadableStream } from 'react-dom/server';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    const stream = await renderToReadableStream(
      <App url={url.pathname} env={env} />,
      {
        bootstrapScripts: ['/main.js'],
        onError: console.error,
      }
    );

    // Ждём shell (критический путь) но не ждём Suspense
    await stream.allReady; // Опционально: ждать всего для ботов

    return new Response(stream, {
      headers: {
        'Content-Type': 'text/html; charset=utf-8',
        'Transfer-Encoding': 'chunked',
      },
    });
  },
};

Стриминг метаданных и заголовков

Проблема стриминга: <head> отправляется первым, до готовности данных. Метаданные из асинхронных компонентов нужно передавать отдельно:

// Next.js решает через generateMetadata — выполняется отдельно от рендера страницы
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await getProduct(params.id);
  return {
    title: product.name,
    description: product.description,
  };
}

// generateMetadata и Page компонент выполняются параллельно
// Стриминг начинается как только generateMetadata завершится

Loading UI и skeleton-стратегии

app/
  catalog/
    loading.tsx     # Автоматический Suspense fallback для всего маршрута
    page.tsx
// app/catalog/loading.tsx — рендерится пока page.tsx загружает данные
export default function CatalogLoading() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {Array.from({ length: 9 }).map((_, i) => (
        <div key={i} className="animate-pulse bg-gray-200 rounded-lg h-64" />
      ))}
    </div>
  );
}

Метрики влияния Streaming SSR

Метрика Без стриминга Со стримингом
TTFB После рендера (500–1500ms) До рендера (< 100ms)
FCP После TTFB + парсинг Сразу после shell
LCP Зависит от медленных данных Зависит только от нужного блока
INP Блокируется до гидратации Гидратация начинается раньше

Сроки реализации

  • Неделя 1–2: аудит текущего SSR, выявление медленных запросов, рефакторинг под Suspense-границы
  • Неделя 3: параллелизация Promise.all и независимые Suspense-блоки
  • Неделя 4: skeleton UI для всех Suspense fallback, loading.tsx для маршрутов
  • Неделя 5: измерение Core Web Vitals до/после, оптимизация shell-времени
  • Неделя 6: мониторинг стриминга в production, алертинг на медленные чанки