Streaming a Suspense v Next.js 16: Loading UI, Server Components a progresivní renderování
Praktický průvodce streamováním v Next.js 16. Ukážeme loading.tsx, granulární Suspense hranice, paralelní načítání dat ve Server Components, error.tsx a kombinaci s Partial Prerenderingem. Včetně reálných čísel z Core Web Vitals.
Streaming v Next.js 16 je technika progresivního renderování, kdy server posílá HTML po částech místo toho, aby čekal na dokončení celé stránky. Díky tomu uživatel vidí první obsah dřív a TTFB klesá klidně o 60 %. V App Routeru se streaming zapne automaticky souborem loading.tsx a explicitně přes <Suspense> hranice kolem pomalých Server Components. V téhle příručce ti ukážu, jak granulárně rozdělit stránku, jak streaming spojit s Partial Prerenderingem a jak řešit nejčastější chyby (vše s ověřenými ukázkami z reálných projektů).
Streaming v Next.js 16 posílá HTML chunky tak, jak se Server Components dokončují, a nepotřebuje k tomu žádnou knihovnu navíc.
loading.tsx automaticky zabalí page.tsx do React Suspense a slouží jako okamžitý fallback při navigaci.
Granulární <Suspense> hranice ukážou kostry pro jednotlivé sekce (například dashboard widgety) místo jednoho velkého spinneru.
Streaming je kompatibilní s Edge runtime, Cache Components i Partial Prerenderingem; se starým getServerSideProps ho ale zkombinovat nejde.
error.tsx musí být Client Component a zachytává chyby z celé sekce ohraničené stejnou route.
Pro fetching dat ve Server Components používej fetch() s direktivou cache; React 19 paralelní volání automaticky deduplikuje.
Co je streaming v Next.js 16
Streaming je odpověď serveru rozdělená do několika HTTP chunků pomocí Transfer-Encoding: chunked. V Next.js 16, který staví na React 19, to znamená, že každý Server Component čekající na data může být obalený Suspense hranicí. Server hned na začátku pošle HTML kostru s fallbackem, a jakmile data dotečou, doplní finální markup pomocí komentářů a inline skriptů. Prohlížeč skládá kousky postupně, takže uživatel vidí navigaci, hlavičku a první widgety dřív, než doběhne pomalý dotaz na databázi.
V praxi to vypadá takhle: místo „bílá stránka 1,2 s a pak všechno najednou" uživatel dostane „navigaci za 200 ms, sidebar za 400 ms, hlavní tabulku za 1100 ms". Lighthouse měří první variantu jako FCP 1200 ms, druhou jako FCP 200 ms, i když celkový načítací čas je stejný. Pro SEO je důležité, že Googlebot s headless Chromem umí streamované HTML plně vyrenderovat a indexovat, takže o organické pozice nepřijdeš.
Soubor loading.tsx a okamžitý fallback
Nejjednodušší cesta, jak streaming aktivovat, je vytvořit vedle page.tsx soubor loading.tsx. Next.js ho automaticky zabalí do React <Suspense> a použije jako fallback po celou dobu, kdy se page komponenta renderuje. Funguje to i pro vnořené layouty; každý segment routy může mít vlastní loading state.
Tenhle fallback se zobrazí okamžitě po kliknutí na <Link href="/dashboard">, ještě než server odpoví. Důležité je, aby loading.tsx byl lehký. Žádné velké komponenty, žádný "use client" kód s těžkými knihovnami. Cílíš na sub-100 ms hydrataci fallbacku.
Granulární Suspense hranice
Jeden velký loading.tsx stačí pro jednoduché stránky. U dashboardů s víc widgety chceš granulární Suspense kolem každé pomalé sekce, aby se rychlé widgety zobrazily dřív než ten nejpomalejší.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { RevenueChart } from "./RevenueChart";
import { RecentOrders } from "./RecentOrders";
import { CustomerStats } from "./CustomerStats";
import { ChartSkeleton, TableSkeleton, StatsSkeleton } from "./skeletons";
export default function DashboardPage() {
return (
<main>
<h1>Přehled</h1>
<Suspense fallback={<StatsSkeleton />}>
{/* rychlý dotaz cca 150 ms */}
<CustomerStats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
{/* agregace, cca 800 ms */}
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
{/* JOIN přes 3 tabulky, cca 1.2 s */}
<RecentOrders />
</Suspense>
</main>
);
}
Každá ze tří komponent se vyrenderuje a streamuje samostatně. Uživatel vidí statistiky za 150 ms, graf za 800 ms a tabulku za 1200 ms, místo všeho najednou za 1200 ms. Důležitý detail: Server Components uvnitř Suspense musí být async funkce nebo komponenty volající fetch(). React Suspense reaguje na „thenable" objekty, ne na obyčejné Promise z useState.
Kdy nestreamovat
Streaming má smysl, když některé části stránky jsou výrazně pomalejší než jiné. Pokud všechny dotazy doběhnou pod 200 ms, streaming přidává overhead bez viditelného přínosu. Taky se vyhni Suspense kolem komponent, které jsou kriticky důležité pro LCP. Největší obsahový prvek by se měl streamovat jako první, ne jako poslední. Tahle chyba mě stála pár hodin debuggingu v mém posledním projektu, kde hero obrázek skončil za Suspense a LCP se propadlo na 3,2 s.
Error.tsx a řešení chyb ve streamu
Co se stane, když dotaz uvnitř streamované sekce selže? Bez ošetření React vyhodí chybu, která bublá až k nejbližší error hranici. Tou může být klidně kořenový layout, a pak ti odejde celý už vyrenderovaný kontext. Řešení: error.tsx soubor v každém segmentu routy.
Pokud chceš granulárnější ošetření (třeba že selže jen graf, ale zbytek dashboardu funguje), použij React <ErrorBoundary> z balíčku react-error-boundary přímo kolem konkrétní Suspense hranice. Tím izoluješ pád jediné komponenty.
Paralelní načítání dat a deduplikace
Streaming funguje nejlíp, když Server Components načítají data paralelně. Pokud necháš jednu komponentu uvnitř Suspense čekat na druhou (waterfall), vyhodíš celý benefit oknem. Klasická chyba, kterou jsem si sám zopakoval v produkci:
// ŠPATNĚ, sekvenční waterfall
export async function RevenueChart() {
const user = await getUser(); // 200 ms
const orgs = await getOrgs(user.id); // 300 ms (čeká na user)
const revenue = await getRevenue(orgs); // 500 ms (čeká na orgs)
return <Chart data={revenue} />;
}
Lepší varianta používá Promise.all a paralelně volá fetch, čeká až těsně před renderem:
Funkce cache() z React 19 zajistí, že volání getUser() v rámci jednoho renderu se deduplikuje. Pokud víc Suspense větví potřebuje stejného uživatele, databáze se dotáže jen jednou. Pro fetch() volání proti vlastnímu API funguje deduplikace v App Routeru automaticky. React si pamatuje URL i headers v rámci request scope.
Streaming + Partial Prerendering
Partial Prerendering (PPR) je v Next.js 16 stable a kombinuje statickou shell s dynamickými dírami. Streaming je jeho přirozený partner: statická část se servíruje z CDN za zhruba 50 ms, dynamické díry uvnitř <Suspense> se streamují za běhu. Aktivace v next.config.ts:
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
ppr: "incremental", // PPR jen pro routy s 'export const experimental_ppr = true'
},
};
export default config;
V samotné stránce pak označíš segment jako PPR a obalíš dynamickou část do Suspense:
Pokud server vyrenderuje jinou hodnotu než klient po hydrataci, React zaloguje warning a v Next.js 16 ji bere jako chybu. Časté příčiny: Date.now(), Math.random(), závislost na window. Řešením je buď přesunout dynamický kód do useEffect, nebo přidat suppressHydrationWarning na konkrétní element. Tenhle bug mě párkrát zaskočil hlavně u časových razítek; doporučuju formátovat datum klientsky.
Bezpodmínečné cookies() nebo headers()
Použití cookies() kdekoli ve stromu opt-outuje celou routu z PPR a statického renderování. Jestli chceš zachovat statickou shell, izoluj čtení cookies do Server Component uvnitř Suspense a parenta nech čistý.
Velký fallback blokuje LCP
Fallback je sice rychlý, ale pokud obsahuje obrovský skeleton s mnoha DOM uzly, sám se stává LCP elementem. Lighthouse pak ukáže 800 ms LCP „skeleton card", což rozhodně nechceš. Drž fallbacky jednoduché.
Měření dopadu na Core Web Vitals
Streaming zlepšuje hlavně tři metriky: TTFB (Time to First Byte), FCP (First Contentful Paint) a INP (Interaction to Next Paint). LCP může zůstat stejné, nebo se mírně zhoršit, pokud je LCP element uvnitř Suspense; proto LCP komponentu nikdy nestreamuj.
Pro reálné měření doporučuju kombinaci tří nástrojů:
Vercel Speed Insights nebo vlastní RUM přes balíček web-vitals.
Lighthouse CI v GitHub Actions pro regresi na PR.
Na svých projektech vidím, že zavedení granulárních Suspense hranic na dashboardových stránkách obvykle srazí TTFB z 1100 ms na 250 ms a FCP z 1400 ms na 400 ms. INP zůstává pod 200 ms díky React 19 concurrent renderingu, který umí přerušit dlouhé renderování pro reakci na klik.
Často kladené otázky
Funguje streaming v Next.js 16 na Edge runtime?
Ano, streaming funguje stejně na Edge runtime i Node.js runtime. Edge runtime má dokonce nižší TTFB díky regionální distribuci, ale podporuje jen Web Streams API. Některé Node.js knihovny (například fs) nebudou fungovat. Pro většinu Server Components s fetch() a databází přes HTTP klienta je Edge runtime ideální.
Jak se streaming liší od starého getServerSideProps?
getServerSideProps v Pages Routeru bylo synchronní. Celé HTML se vyrenderovalo až po doběhnutí všech dotazů. Streaming v App Routeru posílá HTML po částech, takže uživatel vidí UI dřív. Migrace vyžaduje přepsání na async Server Components a obalení pomalých částí do <Suspense>.
Mohu streamovat dynamický obsah s autentizací?
Ano. Po ověření session v proxy.ts nebo Server Componentě obalíš personalizovanou sekci do Suspense, takže statický shell (například layout, hlavička) se odešle okamžitě a osobní obsah streamuje po načtení uživatelských dat. Při použití PPR rozděl stránku na statický prerender a dynamickou Suspense díru s personalizací.
Proč se mi loading.tsx nezobrazí při refresh stránky?
loading.tsx se zobrazuje jako fallback při navigaci uvnitř SPA. Při hard refreshi prohlížeč nedostane nic, dokud server nezačne streamovat. Pak se rovnou ukáže streamované HTML s tvými <Suspense fallback>. Pokud chceš vidět „loading" i při refreshi, použij granulární Suspense hranice přímo v page.tsx.
Lze streaming kombinovat s ISR a revalidací?
Ano, ale s rozmyslem. ISR (Incremental Static Regeneration) cachuje finální HTML; streaming probíhá jen při první generaci stránky nebo při on-demand revalidaci. Pro kombinaci ISR a dynamického streamu použij Partial Prerendering: statickou shell ISR cachuje, dynamické díry uvnitř Suspense se streamují per-request.
Praktický průvodce Cache Components v Next.js 16 – direktiva use cache, konfigurace cacheLife, tagování a invalidace cache, Partial Prerendering a kompletní příklady pro reálné aplikace.