SSR, SSG, ISR, CSR - kiedy powstaje HTML i jakim kosztem

Cztery strategie renderowania nie są wyborem z menu, tylko decyzją per route: gdzie powstaje HTML i kiedy. Każda ma inny moment generacji, inny TTFB, inny koszt infrastruktury i inny stosunek 'świeże dane vs prędkość'. Rozkładam mechanikę czterech podejść i pokazuję, kiedy które ma sens.
Cztery wersje tego samego komponentu - strona z postem blogowym:
// 1. SSR - świeży HTML na każdy request
export async function getServerSideProps() {
const post = await fetchPost();
return { props: { post } };
}
// 2. SSG - HTML powstaje w build time
export async function getStaticProps() {
const post = await fetchPost();
return { props: { post } };
}
// 3. ISR - SSG + okresowe odświeżanie w tle
export async function getStaticProps() {
const post = await fetchPost();
return { props: { post }, revalidate: 60 };
}
// 4. CSR - HTML powstaje u usera w przeglądarce
function Post() {
const [post, setPost] = useState(null);
useEffect(() => {
fetchPost().then(setPost);
}, []);
return post ? <Article {...post} /> : <Skeleton />;
}Używam tu API z Pages Routera, bo jest dobrze rozpoznawalne. Mechanika jest taka sama w Astro, Remix, SvelteKit i innych - tylko nazwy się różnią. Sam wybór strategii (gdzie i kiedy powstaje HTML) jest ponad-framework’owy.
Dla użytkownika końcowego wynik wygląda tak samo. Ale czas do pierwszego renderu, koszt infrastruktury i świeżość danych - tu różnice są ogromne. Wybór między tymi czterema podejściami to nie kwestia gustu, tylko świadoma decyzja o tym, kiedy powstaje HTML i kto za to płaci.
#Klient czy serwer - gdzie powstaje HTML
Każda strona webowa, którą widzi user, to ostatecznie HTML w jego przeglądarce. Pytanie jest tylko gdzie i kiedy ten HTML powstał:
- Server-side - HTML jest gotowy zanim trafi do przeglądarki. Serwer lub build system robi pracę, browser tylko wyświetla.
- Client-side - serwer wysyła minimalny shell + JS bundle. Browser pobiera, parsuje, wykonuje, dopiero wtedy generuje HTML w drzewie DOM.
Server-side dzieli się dalej w zależności od tego, kiedy dokładnie ten HTML powstał:
- Przy każdym requeście → SSR
- Raz, w build time → SSG
- Raz w build time + okresowe odświeżanie w tle → ISR
Plus jeden client-side wariant:
- W przeglądarce usera, po wykonaniu JS → CSR
Cztery momenty generacji HTML. Każdy ma inne konsekwencje dla performance, SEO, kosztu i tego, jak świeże są dane, kiedy user je zobaczy.
#SSR (Server-Side Rendering) - świeżo na każdy request
W SSR każde wejście usera na stronę uruchamia handler na serwerze:
user → request → handler na serwerze:
1. fetch danych (z bazy, z API)
2. render komponentu do HTML
3. response z gotowym HTML
→ browser dostaje pełny HTML
→ JS hydruje (dodaje interaktywność)Każdy user dostaje świeży HTML, generowany w momencie requestu. Dane są tak nowe, jak ostatnie wywołanie bazy.
Plusy
- Dane zawsze świeże (każdy request to nowe fetch).
- HTML kompletny w pierwszym response - dobry dla SEO, dobry dla user’a z wolnym JS-em (treść widać zanim cokolwiek się sparsuje).
- Personalizacja per user (response zależy od cookies, sesji, query params).
Minusy
- TTFB zależy od czasu renderu na serwerze. Powolne API → powolna strona.
- Każdy request kosztuje CPU + RAM na serwerze. Skalowanie = więcej maszyn.
- Trudniej cache’ować na CDN (per-user response).
- Wymagany działający serwer 24/7.
Kiedy SSR: dane muszą być świeże w momencie wyświetlenia (dashboard z live metrykami, ecommerce z zawsze-aktualnym stockiem, personalizowane feed’y), albo response zależy od konkretnego usera (cookies, A/B test, ip-geo).
web.dev - Rendering on the Web rozkłada SSR razem z innymi strategiami i pokazuje konkretne metryki TTFB / FCP / TTI per podejście.
#SSG (Static Site Generation) - raz na build
W SSG cała praca renderowania dzieje się przed deployem. Build system iteruje po wszystkich routach, dla każdego fetch’uje dane, renderuje HTML, zapisuje plik. Deploy push’uje statyczne pliki na CDN. Od tego momentu każdy user dostaje gotowy plik prosto z CDN:
build time:
1. dla każdego routa:
fetch danych
render do HTML
zapis pliku
2. deploy gotowych plików na CDN
runtime:
user → request → CDN node odpowiada gotowym plikiem
→ browser dostaje HTML
→ JS hydrujePlusy
- Bardzo szybki TTFB (CDN serwuje plik, nic nie renderuje).
- Tani hosting (statyczne pliki serwuje każdy CDN za grosze).
- Łatwo skalować (CDN robi to za ciebie).
- Idealny dla SEO i Core Web Vitals (LCP/FCP na poziomie milisekund).
- Działa nawet jeśli backend padł - pliki są już na CDN.
Minusy
- Dane są “zamrożone” w momencie build’a. Zmiana w bazie nie pokaże się aż do następnego deploya.
- Każda zmiana = pełny build. Dla 50 routów to ok, dla 50 000 routów to godziny.
- Brak personalizacji per user (każdy widzi ten sam plik).
Kiedy SSG: treść zmienia się rzadko, można planować deploye (blog, dokumentacja, marketing pages, landing pages, portfolio). Praktycznie default dla content-driven sites.
#ISR (Incremental Static Regeneration) - SSG plus odświeżanie
ISR to hybryda SSG i SSR. Strona jest pre-rendered w build time (jak SSG), ale serwer trzyma timestamp ostatniej regeneracji. Po przekroczeniu progu (revalidate: 60 sekund) następny request dostaje stary cached HTML, a w tle uruchamia się regeneracja. Kolejni userzy dostają już nową wersję:
build: render i zapis wszystkich routów do cache (jak SSG).
cache startuje jako świeży, revalidate = 60s.
t=0s deploy, cache świeży
t=10s user A → jesteśmy w oknie 60s, cache świeży
→ serwuj z cache od razu, nic w tle
t=45s user B → wciąż w oknie, cache świeży
→ serwuj z cache od razu, nic w tle
t=65s user C → okno 60s minęło, cache przeterminowany
→ serwuj STARĄ wersję z cache OD RAZU (user nie czeka)
→ w tle: fetch + render + podmiana cache
t=66s user D → regeneracja w tle wciąż trwa
→ dalej STARA wersja z cache
t=80s user E → cache podmieniony, okno liczy od nowa
→ serwuj NOWĄ wersję z cacheTo, co właśnie zobaczyłeś, nazywa się stale-while-revalidate: serwuj starą wersję od ręki, odśwież w tle. Sedno: nikt nigdy nie czeka na regenerację. User dostaje cache natychmiast (jak user C), a świeższa wersja trafia do kolejnych (jak user E).
Plusy
- TTFB bliski SSG (cache hit serwowany od ręki).
- Dane świeższe niż w czystym SSG (revalidate co N sekund).
- Skalowalność CDN-like (większość requestów idzie z cache).
- Można regenerować pojedyncze routy bez pełnego build’a.
Minusy
- Część userów widzi stare dane (między revalidate a faktyczną regeneracją).
- Komplikacja deployment - wymaga serwera, który zna timestamp cache’a (nie czysty CDN).
- Pokazuje czasem nieaktualne dane, a nie wszędzie to akceptowalne. Na przykład stan magazynowy w sklepie - ISR może serwować “dostępny”, gdy produkt już się wyprzedał (stary cache do najbliższej regeneracji). Tu lepszy SSR - zawsze świeży stan.
- Vendor lock-in: implementacje różnią się między platformami (Vercel, Netlify, Cloudflare).
Kiedy ISR: treść zmienia się okazjonalnie i opóźnienie kilku sekund nie jest problemem (blog z dużą liczbą postów, ecommerce katalog produktów, listingi ofert). Złoty środek między prędkością SSG a świeżością SSR.
#CSR (Client-Side Rendering) - HTML powstaje u usera
W CSR serwer wysyła minimalny HTML shell plus JS bundle. Shell to pusty szkielet strony - skorupa bez treści, zwykle nie więcej niż <div id="root"></div>, w którą dopiero JS wstrzyknie zawartość. Browser pobiera, parsuje, wykonuje JS, dopiero wtedy JS generuje DOM. Dane fetch’owane są z przeglądarki (zwykle przez useEffect + fetch):
user → request → serwer odpowiada HTML shellem + JS bundle
→ browser pobiera + parsuje
→ JS się wykonuje
→ useEffect odpala fetch danych
→ response wraca z API
→ React (lub inny framework) renderuje komponent
→ user widzi treśćKażdy z tych kroków zajmuje czas. Treść użytkownik widzi dopiero w ostatnim kroku.
Plusy
- Wysoka interaktywność - po pierwszym renderze wszystko reaguje natychmiast (CSR nie ma osobnego kroku hydracji - drzewo komponentów powstaje w browserze od razu jako interaktywne).
- Real-time updates łatwiej zrobić (WebSocket → setState).
- Wystarczy najprostszy hosting, który tylko oddaje gotowe pliki - bez żadnego serwera renderującego (cały render dzieje się w przeglądarce).
- Granularne kontrolowanie tego, kiedy i co pobierać (lazy loading, infinite scroll, optimistic UI).
Minusy
- Pusty HTML w pierwszym response - dla crawlerów SEO bez JS-a strona jest pusta. Google sobie radzi (rendery JS), ale wolniej i nie tak dobrze jak z gotowym HTML.
- Pierwsza treść pojawia się późno - user widzi skeleton/spinner, zanim cokolwiek się wyrenderuje (opóźniony FCP). Pełna interaktywność (TTI) przychodzi jeszcze później, gdy JS się sparsuje i wykona.
- Cały render odbywa się na urządzeniu usera - słabsze urządzenie = wolniejsza strona.
- Dwie wymiany z serwerem do pierwszej treści: najpierw HTML+JS, potem fetch danych.
Kiedy CSR: aplikacje typu SaaS dashboard / admin panel / wewnętrzne narzędzia, gdzie SEO nie ma znaczenia, user już zalogowany, długa sesja, i interaktywność jest sercem produktu. Klasyczne SPA case.
#Co wybrać i dlaczego
Cztery strategie porównane po siedmiu kryteriach:
| SSR | SSG | ISR | CSR | |
|---|---|---|---|---|
| TTFB | zmienny (zależy od API) | bardzo niski | bardzo niski | niski (HTML shell) |
| FCP | dobry | bardzo dobry | bardzo dobry | słaby |
| SEO | bardzo dobry | bardzo dobry | bardzo dobry | zależy od crawlera |
| Świeżość danych | maksymalna | tylko z build’em | revalidate timer | per fetch w runtime |
| Koszt infra | wysoki (serwer 24/7) | bardzo niski (CDN) | średni | bardzo niski (CDN) |
| Komplikacja | średnia | niska | wysoka | średnia |
| Personalizacja | tak | nie | nie (bez tricków) | tak |
Klucz w wyborze: decyduje per route, nie per app. Realna aplikacja używa mixa:
/(landing) - SSG (rzadko się zmienia, SEO ważne)/blog/[slug]- SSG lub ISR (treść stała, nowe posty co tydzień)/shop/[product]- ISR (stock zmienia się okresowo, ranking SEO ważny)/dashboard- CSR (zalogowany user, full interactivity)/checkout- SSR (zawsze świeże dane, personalizacja per user)/search?q=...- SSR (dynamiczne na podstawie query params)
Frameworki jak Next.js, Astro czy Remix dają wybór per route. Nie ma już sytuacji “ta aplikacja jest SSR” - jest “ta aplikacja używa mieszanki strategii, każdy route ma swoją”.
Spróbuj poniżej - kliknij strategię, żeby zobaczyć, gdzie na osi czasu powstaje HTML i jakie ma to konsekwencje dla TTFB, świeżości, SEO i kosztu:
- build
- deploy
- requestHTML powstaje tu
- browser
- Moment generacji HTML
- request
- TTFB
- Zmienny - zależy od czasu renderu na serwerze (typowo 100-500 ms).
- Świeżość danych
- Maksymalna - każdy request to świeże dane.
- SEO
- Bardzo dobre - HTML kompletny w pierwszym response.
- Koszt infrastruktury
- Wysoki - serwer musi działać 24/7 i obsłużyć każdy request.
- Kiedy używać
- Dashboardy, ecommerce, personalizowane feed'y, search - wszędzie tam, gdzie dane muszą być świeże w momencie wyświetlenia albo response zależy od konkretnego usera.
#Modern - streaming, partial hydration, RSC
Cztery podstawowe strategie wyczerpują cały wybór. To, co niżej, nie jest kolejną opcją obok nich - to warianty nałożone na tę czwórkę, najczęściej na SSR. Nadal wybierasz SSR / SSG / ISR / CSR; te dodatki zmieniają tylko jak HTML jest dostarczany albo ile JS-a ląduje u klienta, nie kiedy i gdzie HTML powstaje.
Streaming SSR (React 18, Suspense) - to wciąż SSR, tylko dostarczany kawałkami. Serwer nie czeka, aż cały HTML się zrenderuje - wysyła fragmenty (chunks) w miarę gotowości. User widzi nagłówek strony, podczas gdy serwer dalej liczy listę produktów. TTFB dużo niższy niż w zwykłym SSR - serwer wysyła górę strony od razu, zamiast czekać, aż doliczą się wszystkie dane. FCP też szybsze: pierwszą treść widać, zanim reszta strony się dorenderuje.
Partial hydration / Islands (Astro, częściowo Next.js z RSC) - server-side renderuje cały HTML, ale hydruje tylko niektóre fragmenty (te które potrzebują interaktywności). Reszta zostaje statycznym HTML-em. Mniej JS-a do pobrania, mniej JS-a do wykonania, lepsze metryki interaktywności na słabszych urządzeniach. (Qwik idzie jeszcze dalej z resumability - w ogóle pomija hydrację, wznawiając stan z serializowanego HTML, zamiast odtwarzać go w przeglądarce.)
React Server Components (RSC) - hybryda server-rendered i client-side w obrębie jednego drzewa komponentów. Komponenty oznaczone jako “server” renderują się i pozostają na serwerze (nie trafiają jako JS do browsera). “Client” komponenty są hydratowane normalnie. Granica jest płynna - server component może osadzić w sobie client component. Najlepiej pasuje, gdy strona to w większości statyczna treść (server components, zero JS-a wysyłanego do klienta), a interaktywność potrzebna jest tylko gdzieniegdzie (client components).
Wszystkie te warianty są rozszerzeniami tej samej zasady (kiedy i gdzie powstaje HTML), nie nowymi strategiami. Można je czytać jako “SSR z lepszą dostarczalnością” (streaming), “SSR z mniej JS-a po stronie klienta” (islands), “SSR z granicą sterowaną na poziomie komponentu” (RSC).
Patterns.dev - Rendering Patterns rozkłada każdy z tych wariantów w osobnej notce z diagramami i kodem.
#Typowe pułapki - co najczęściej psuje się w każdej strategii
Każda strategia ma swoją kolekcję problemów, które wracają w prawdziwych projektach. To dobry zestaw do interview-prep i do code review.
SSR
window/documentna serwerze. Node.js nie ma DOM API. Próba ich użycia w komponencie renderowanym na serwerze rzucaReferenceError. Fix: warunkować przeztypeof window !== 'undefined', użyćuseEffect, albo oznaczyć komponent jako client-only.- Hydration mismatch. Klient wyrenderuje inaczej niż serwer (np.
new Date().toLocaleString()z innej strefy,Math.random(), dane zlocalStorage). Framework odbudowuje fragment od zera, tracąc benefit pre-renderu i logując warning. Fix: deterministyczny render (ten sam input zawsze daje ten sam HTML - bezMath.random(), czasu czy strefy w markupie),suppressHydrationWarning(prop Reacta mówiący „wiem, że tu serwer i klient się różnią, nie ostrzegaj”) dla świadomie dopuszczonych różnic, client-only dla naprawdę zmiennych fragmentów. - Singleton współdzielony między requestami. Globalny obiekt (np. cache, config, user state) zainicjalizowany raz przy starcie serwera widzą wszyscy. Jeden user może zobaczyć dane drugiego - klasyczny memory + security bug.
- Powolne API blokuje cały render. SSR czeka, aż wszystkie dane są dostępne, zanim wyśle HTML. Jedno powolne zapytanie → TTFB sekundy. Fix: streaming SSR z
Suspense, równoległe zapytania, timeouty.
SSG
- Stale content bez rebuilda. Zmienisz post w CMS-ie, ale dopóki nie odpalisz
npm run buildi nie zdeployujesz, user widzi starą wersję. Rozwiązanie: webhook → CI → automatyczny redeploy, albo ISR. - Czas buildu rośnie lawinowo przy dużej liczbie stron. SSG dla 100k produktów = build trwa godziny. Sam się o to potknąłem - w jednym z moich projektów buildy Gatsby dochodziły do 15-20 minut (wąskim gardłem był pipeline obrazów) i Netlify potrafił uciąć je na limicie czasu. Fix:
fallback: blocking/ on-demand generation, albo SSR per route z agresywnym caching. - Brak dynamicznej ścieżki = 404. Strona, której nie wygenerowałeś w build, nie istnieje. Klasyczna pułapka przy nowych itemach dodanych po deployu.
- Environment variables baked-in. Wartości
process.env.*użyte w build trafiają do statycznego HTML. Zmiana wymaga rebuilda. Nigdy nie wstawiaj sekretów do publicznych zmiennych.
ISR
- Stary content widoczny “tuż po revalidate”. Pierwszy user po upływie czasu
revalidatedostaje stary HTML (cache), a regeneracja idzie w tle. Drugi user widzi nową wersję. Konsekwencja: dwóch userów na tej samej stronie w tej samej sekundzie widzi różne dane. Akceptowalne dla bloga, problematyczne dla cen produktów. - Zbyt krótki
revalidate= de facto SSR. Ustawisz 5 sekund, każdy request regeneruje, koszty rosną do poziomu SSR bez benefitów cache. - Vendor lock-in. Pełny ISR (z on-demand revalidation, granular cache tags) najpełniej działa na Vercel + Next.js. Netlify i Cloudflare mają własne odpowiedniki (Netlify np. przez standardowe
stale-while-revalidateheaders), ale o innej semantyce i konfiguracji. - Race condition przy regeneracji. Dwa requesty trafiają jednocześnie po upływie czasu
revalidate- oba próbują regenerować. Niektóre platformy deduplikują, inne nie.
CSR
- SEO disaster. Crawler dostaje pusty
<div id="root"></div>i nic więcej. Google od dawna umie wykonać JS, ale: opóźnione indeksowanie, social-media crawlery (FB, Twitter, Slack) generalnie JS-a nie odpalają. Open Graph + Twitter Cards przepadają. - Layout shift przy ładowaniu data. User widzi skeleton, potem treść, potem dane się dociągają, potem reszta doskakuje. CLS w Lighthouse leci na czerwono.
- Auth flicker. Strona renderuje “Sign in” przez chwilę przed sprawdzeniem tokena z
localStorage. Klasyczny migający header logged-in / logged-out. - Memory leaks z
useEffect. Subscribe do WebSocketa,setInterval, listener nawindow- bez cleanup w długo żyjącej stronie SPA, użycie pamięci rośnie z każdą nawigacją.
Cross-cutting (każda strategia z server-side renderem)
- Theme flicker (FOUC). FOUC = Flash of Unstyled Content: krótki błysk strony, zanim zaaplikują się docelowe style. Tu konkretnie: dark mode trzymany w
localStorage- serwer go nie zna i wysyła HTML w jednym motywie, klient się hydratuje i przełącza na drugi. Błysk białego/czarnego ekranu. Fix: inline script przed<body>, który ustawia klasę zlocalStorageprzed pierwszym paintem. - Locale mismatch. Serwer renderuje w jednym języku, klient ma preferowany inny - albo dwa renders (czas i koszt), albo flash treści.
- Date / timezone divergence.
new Date().toLocaleString()zwróci coś innego w Node (zwykle UTC) niż w browserze (lokalna strefa). Klasyczna przyczyna hydration mismatch.
#Strategie to nie wybór z menu, to decyzja per route
Cztery podstawowe strategie - SSR, SSG, ISR, CSR - różnią się tak naprawdę jednym: kiedy i gdzie powstaje HTML. Wszystkie inne różnice (TTFB, SEO, koszt, świeżość, interaktywność) wynikają z tego jednego wyboru.
Pragmatyczna ścieżka wyboru:
- Default = SSG. Treść statyczna, deploy = re-build, CDN serwuje. Najtaniej, najszybciej, najmniej rzeczy może się zepsuć.
- Jeśli dane się zmieniają, ale dopuszczalne jest opóźnienie kilku sekund → ISR. SSG performance, świeżość rośnie automatycznie.
- Jeśli dane muszą być świeże w momencie wyświetlenia lub zależą od usera → SSR. Ceną jest TTFB i koszt serwera.
- Jeśli SEO nie ma znaczenia i interaktywność jest sercem produktu → CSR. Klasyczne SPA.
- Per route, nie per app. Mix strategii to norma, nie wyjątek.
Framework to detal implementacyjny - Next.js, Astro, Remix, SvelteKit, Nuxt dają te same cztery opcje pod różnymi API. Wybór strategii nie zaczyna się od “jaki framework”, tylko od “jakie dane i jak często się zmieniają”.
Komentarze
Wczytywanie komentarzy…