Streaming SSR, Suspense i selektywna hydracja – jak React uruchamia stronę kawałek po kawałku
Otwierasz stronę produktu. Nazwa i zdjęcie pojawiają się szybko, ale opinie potrzebują jeszcze chwili. Czy cały ekran musi czekać, aż serwer je pobierze? A gdy HTML już dotrze, czy trzeba załadować JavaScript wszystkich sekcji, żeby zacząć korzystać ze strony?

Właśnie takie zależności pomagają rozplątać streaming SSR, Suspense i selektywna hydracja. Każdy z tych mechanizmów odpowiada za inny kawałek pracy.
HTML na ekranie to jeszcze nie gotowy React
Przy renderowaniu po stronie serwera przeglądarka dostaje HTML, który może wyświetlić przed uruchomieniem kodu aplikacji. Hydracja pozwala Reactowi przejąć ten istniejący DOM i uruchomić logikę komponentów.
W aplikacji z kontenerem #root punktem wejścia może być:
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(document.getElementById("root"), <App />);
Pierwszy render klienta powinien zgadzać się z HTML-em z serwera. Jeśli serwer wyrenderuje datę, a klient inną, mamy potencjalny hydration mismatch.
Nie oznacza to, że przed hydracją strona jest całkowicie martwa. Zwykły link nadal może prowadzić pod wskazany adres. Przycisk wymagający Reactowego onClick potrzebuje już działającej logiki aplikacji. Dokumentacja hydrateRoot
Streaming: wyślij to, co już masz
renderToString zwraca HTML jako jeden string. Nie obsługuje streamingu ani oczekiwania na zawieszone komponenty – jeśli coś się zawiesi, w wyniku pojawi się odpowiedni fallback. Dokumentacja renderToString
Streaming pozwala dostarczać wynik etapami. Dla naszej strony produktu przebieg może wyglądać tak:
| Etap | Co widzi użytkownik |
|---|---|
| Gotowy podstawowy widok | Nazwę, zdjęcie i placeholder opinii |
| Gotowe opinie | Treść opinii zastępującą placeholder |
| Gotowa logika danej sekcji | Możliwość korzystania z jej interakcji Reactowych |
Serwer dosyła HTML gotowej sekcji wraz z instrukcjami pozwalającymi umieścić go w odpowiednim miejscu. Samo pokazanie tej treści nie musi czekać na załadowanie całego Reacta. To nadal osobny etap od hydracji. Dokumentacja streamingu
Suspense wyznacza granice oczekiwania
Załóżmy, że komponent opinii korzysta ze źródła danych zintegrowanego z Suspense:
function ProductPage() {
return (
<main>
<ProductHeader />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</main>
);
}
Jeżeli Reviews zawiesi renderowanie, React może pokazać ReviewsSkeleton, a resztę widoku przygotować niezależnie. Gdy zawartość będzie gotowa, zastąpi fallback.
Ważny szczegół: samo opakowanie komponentu w Suspense nie wystarczy. Zwykłe pobieranie danych w useEffect nie aktywuje tej granicy. Potrzebujesz integracji frameworka, rozwiązania obsługującego Suspense albo odczytu odpowiednio zarządzanego Promise przez use. Dokumentacja Suspense
Jak uruchomić stream na serwerze?
W Node.js służy do tego renderToPipeableStream. Uproszczony fragment obsługi odpowiedzi wygląda tak:
import { renderToPipeableStream } from "react-dom/server";
function handleRequest(request, response) {
const { pipe } = renderToPipeableStream(<Document />, {
bootstrapScripts: ["/client.js"],
onShellReady() {
response.setHeader("Content-Type", "text/html; charset=utf-8");
pipe(response);
},
onShellError() {
response.statusCode = 500;
response.end("Nie udało się wyświetlić strony.");
},
onError(error) {
console.error(error);
},
});
}
Tutaj Document reprezentuje cały dokument, łącznie z <html>. Odpowiadający mu skrypt klienta wywołuje hydrateRoot(document, <Document />).
onShellReady oznacza gotowość początkowego widoku, zawierającego również potrzebne fallbacki. To moment, w którym zaczynamy wysyłanie. Czekanie z pipe do onAllReady odebrałoby użytkownikowi korzyść ze stopniowego pojawiania się treści.
Po rozpoczęciu odpowiedzi nie zmienisz już jej statusu HTTP. To jeden z powodów, dla których obsługę błędów trzeba przemyśleć wcześniej. Dokumentacja API serwera
Selektywna hydracja: niech opinie poczekają na swoją kolej
Załóżmy teraz, że HTML opinii już widać, ale JavaScript potrzebny do ich obsługi jeszcze się ładuje.
Przy odpowiednich granicach Suspense React może hydratować inne części strony bez czekania na tę sekcję. Może też zmienić priorytet pracy, gdy użytkownik wejdzie w interakcję z fragmentem, który nie został jeszcze zhydratowany.
To nie jest obietnica natychmiastowego działania każdego kliknięcia. Kod wymaganej sekcji nadal musi być dostępny, a React musi wykonać potrzebną pracę.
„Selektywna” nie oznacza również, że React czeka z całą hydracją, aż ktoś kliknie. Zaczyna ją możliwie wcześnie, a interakcje pomagają ustalić, co jest najpilniejsze. Mechanizm operuje na granicach Suspense i uwzględnia zależności od rodziców. Wyjaśnienie zespołu Reacta
Suspense nie dzieli automatycznie twojego bundla
Żeby kod opinii mógł ładować się osobno, można wykorzystać lazy i dynamiczny import:
import { lazy, Suspense } from "react";
const Reviews = lazy(() => import("./Reviews"));
function ProductPage() {
return (
<main>
<ProductHeader />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</main>
);
}
lazy odracza ładowanie modułu do momentu próby wyrenderowania komponentu. Wymaga tu eksportu domyślnego z ./Reviews. W środowisku SSR potrzebna jest też konfiguracja budowania i serwera obsługująca taki podział.
To przykład oczekiwania na kod komponentu. Nie sprawia automatycznie, że pobieranie danych wewnątrz Reviews zacznie współpracować z Suspense. Dokumentacja lazy
Gdzie stawiać granice?
Jedno Suspense wokół całej strony może skończyć się wielkim spinnerem. Osobna granica wokół każdego drobiazgu może z kolei zamienić ekran w serię migających placeholderów.
Najlepiej myśleć sekcjami, które mają sens dla użytkownika: informacje o produkcie, opinie, rekomendacje. Elementy, które powinny pojawić się razem, trzymaj w jednej granicy. Niezależnym sekcjom pozwól ładować się osobno. Taką rolę granic opisuje również dokumentacja Suspense.
Na stronie sklepu zacząłbym od pytania: czy wolne opinie naprawdę powinny opóźniać zobaczenie produktu? Dopiero potem ustalałbym granice w kodzie.
Streaming pozwala wcześniej pokazać treść. Selektywna hydracja pozwala uruchamiać obsługę strony etapami. Dobrze ustawione Suspense sprawia, że te etapy odpowiadają temu, czego potrzebuje użytkownik.