Przejdź do treści

Jak przyspieszyć Next.js za pomocą Streamingu i Suspense?

Nie pozwól użytkownikom czekać na pełny SSR. Zobacz, jak ładować stronę kawałek po kawałku, projektować skeletony i wpływać na LCP w App Routerze.

Maciej Sala

Founder StriveLab

8 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 17 lipca 2026)

Na czym polega streaming w Next.js?

W klasycznym renderowaniu serwerowym bez mechanizmu streamowania aplikacja musi zebrać wszystkie dane potrzebne do zbudowania całej strony, zanim użytkownik cokolwiek zobaczy. Mimo że same zapytania mogą biec równolegle, pojedyncze wolne oczekiwanie umieszczone wysoko w drzewie komponentów skutecznie blokuje wyrenderowanie wszystkiego poniżej.

Streaming całkowicie zmienia ten model, pozwalając wysłać wstępną powłokę strony wraz z fallbackami, zanim zakończą się wolniejsze operacje, a następnie sukcesywnie uzupełniać treść w miarę gotowości poszczególnych granic. Warto jednak pamiętać, że natychmiastowe ładowanie nie jest dane raz na zawsze, ponieważ czas dotarcia pierwszego fragmentu zależy między innymi od zadań wykonywanych przed granicą, generowania metadanych dla botów oraz pośredniego buforowania po drodze.

Next.js App Router obsługuje streaming natywnie, wykorzystując React Suspense i architekturę Server Components.

Jak działa Suspense w Server Components?

<Suspense> to granica, która mówi Reactowi: „wyświetl fallback (np. skeleton), a gdy dane będą gotowe, wtedy podmień na właściwą treść".

Code
// app/dashboard/page.tsx
import { Suspense } from 'react'
 
export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
 
      {/* Sekcja statyczna — może znaleźć się w pierwszej powłoce */}
      <WelcomeBanner />
 
      {/* Sekcja dynamiczna — streamowana */}
      <Suspense fallback={<StatsSkeleton />}>
        <DashboardStats />
      </Suspense>
 
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
 
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </main>
  )
}
Code
// components/DashboardStats.tsx — Server Component z wolnym fetchem
async function DashboardStats() {
  // Ten fetch trwa 2 sekundy, ale NIE blokuje reszty strony
  const response = await fetch('https://api.example.com/stats', {
    cache: 'no-store',
  })
 
  if (!response.ok) {
    throw new Error(`Nie udało się pobrać statystyk: ${response.status}`)
  }
 
  const data = await response.json()
 
  return (
    <div className="grid grid-cols-4 gap-4">
      <StatCard label="Przychód" value={`${data.revenue} PLN`} />
      <StatCard label="Zamówienia" value={data.orders} />
      <StatCard label="Klienci" value={data.customers} />
      <StatCard label="Konwersja" value={`${data.conversion}%`} />
    </div>
  )
}

Kolejność wyświetlania przy Suspense

Niezależne granice Suspense mogą ujawniać się zgodnie z gotowością danych, a nie wyłącznie według kolejności w JSX.

Jeśli RevenueChart dostanie dane w 500 ms, a DashboardStats w 2 s, chart pojawi się pierwszy, mimo że w kodzie jest niżej. Każdy <Suspense> boundary to niezależna „wyspa" streamowania.

To nie znaczy, że struktura JSX jest obojętna. Zagnieżdżona granica nie ujawni się przed gotowością granicy nadrzędnej, a fetch uruchomiony dopiero po zakończeniu innego komponentu stworzy waterfall. Granice kontrolują sposób ujawniania UI; moment rozpoczęcia zapytań kontrolujesz strukturą komponentów i miejscem utworzenia obietnic.

loading.tsx: Suspense na poziomie routingu

Next.js App Router ma wbudowany mechanizm, który automatycznie owija page.tsx w <Suspense>. Wystarczy dodać plik loading.tsx w tym samym katalogu.

Code
app/
├── dashboard/
│   ├── layout.tsx      ← pozostaje widoczny podczas nawigacji
│   ├── loading.tsx     ← skeleton podczas ładowania page.tsx
│   └── page.tsx        ← streamowany
Code
// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 w-48 rounded bg-gray-200" />
      <div className="grid grid-cols-4 gap-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="h-24 rounded bg-gray-200" />
        ))}
      </div>
      <div className="h-64 rounded bg-gray-200" />
    </div>
  )
}

Różnica jest prosta: loading.tsx tworzy granicę Suspense dla page.tsx i potomnych segmentów danego poziomu routingu, podczas gdy współdzielony layout pozostaje widoczny i interaktywny. Ręczne <Suspense> pozwala wydzielić konkretne sekcje niezależnie. W praktyce możesz łączyć oba podejścia.

Projektowanie dobrych skeletonów w Next.js

to narzędzie UX, które redukuje postrzegany czas ładowania, a dobrze zaprojektowany skeleton:

  • Odwzorowuje kształt docelowej treści, czyli użytkownik mentalnie widzi stronę zanim się załaduje.

  • Unika migotania: dla bardzo szybkich danych rozważ opóźnione pokazanie fallbacku; próg dobierz na podstawie pomiarów, nie sztywnej reguły 200 ms.

  • Jest lekki i nie zawiera ciężkich animacji, które obciążają GPU.

Code
// components/skeletons/StatsSkeleton.tsx
export function StatsSkeleton() {
  return (
    <div aria-hidden="true" className="grid grid-cols-4 gap-4">
      {Array.from({ length: 4 }).map((_, i) => (
        <div
          key={i}
          className="animate-pulse rounded-lg border border-gray-100 bg-white p-4 motion-reduce:animate-none"
        >
          <div className="mb-2 h-4 w-20 rounded bg-gray-200" />
          <div className="h-8 w-28 rounded bg-gray-200" />
        </div>
      ))}
    </div>
  )
}

Fallback powinien być w pełni dostępny dla każdego użytkownika, dlatego dekoracyjne kształty należy ukryć przed czytnikami za pomocą atrybutu aria-hidden, a stan ładowania komunikować na kontenerze sekcji przy użyciu aria-busy="true" lub dedykowanego elementu z role="status". Warto również wyłączyć animacje dla użytkowników preferujących ograniczony ruch, używając zapytania prefers-reduced-motion. Po pomyślnym załadowaniu właściwej treści należy niezwłocznie usunąć atrybut aria-busy, pamiętając, aby nie ogłaszać każdej karty osobno, co mogłoby przytłoczyć odbiorcę korzystającego z technologii asystujących.

Unikanie flash of loading state

Jeśli dane ładują się szybko, skeleton migocze na ułamek sekundy i to jest gorsze niż brak skeletonu. W tej sytuacji CSS może pomóc:

Code
/* Wizualne opóźnienie skeletonu; element nadal rezerwuje miejsce w układzie */
.skeleton-delayed {
  animation: fadeIn 0.3s ease-in;
  animation-delay: 200ms;
  animation-fill-mode: backwards;
}
 
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

Skeleton dopasowany do rodzaju sekcji

Nie buduj jednego uniwersalnego placeholdera dla całej aplikacji. Karta produktu potrzebuje miejsca na zdjęcie, cenę i CTA, tabela powinna zachować liczbę kolumn, a artykuł może pokazać jedynie szerokości akapitów. Dzięki temu po doładowaniu danych układ nie przeskakuje, a użytkownik od razu rozumie strukturę widoku.

W ramach prostego testu porównaj wymiary skeletonu z finalnym komponentem i sprawdź CLS w Lighthouse albo danych rzeczywistych. Jeżeli fallback zmienia wysokość sekcji lub spycha najważniejszą treść, ładowanie wizualnie szkodzi nawet wtedy, gdy streaming działa technicznie poprawnie.

Streaming i Suspense a Core Web Vitals

Streaming może wpływać na metryki , których dane rzeczywiste Google wykorzystuje jako jeden z sygnałów jakości strony:

LCP przy streamingu w Next.js

Streaming może poprawić LCP tylko wtedy, gdy kandydat LCP znajduje się w szybko dostarczonej powłoce albo rozwiązuje się wcześniej niż bez streamingu. Jeśli największym elementem jest obraz lub hero ukryte za wolną granicą, LCP nadal będzie późny, a zastąpienie dużego skeletonu właściwą treścią może wyznaczyć nowego kandydata LCP. Mierz wynik w danych rzeczywistych, a nie na podstawie samego użycia Suspense.

INP przy streamingu i Suspense

React łączy streaming z selektywną hydratacją, więc może wcześniej priorytetyzować interaktywne fragmenty. Nie gwarantuje to poprawy INP: ciężki kod klienta, długie zadania na głównym wątku i kosztowne handlery nadal decydują o opóźnieniu interakcji. Server Components bez JavaScriptu klienta nie wymagają hydratacji; hydratowane są interaktywne Client Components.

CLS i stabilność skeletonów

Streaming może pogorszyć CLS, jeśli skeletony mają inne wymiary niż docelowa treść. Rozwiązanie: projektuj skeletony z identyczną wysokością i układem jak finalne komponenty.

Code
// Zły skeleton — inny rozmiar niż docelowy komponent
function BadSkeleton() {
  return <div className="h-20 w-full" /> // Za mały — spowoduje layout shift
}
 
// Dobry skeleton — identyczne wymiary
function GoodSkeleton() {
  return <div className="h-48 w-full" /> // Taki sam rozmiar jak RevenueChart
}

Streaming a SEO: co widzi Googlebot?

Streaming w App Routerze nadal jest renderowaniem serwerowym, ale nie warto opierać SEO na założeniu, że każdy robot poczeka na każdą spóźnioną granicę. Treść kluczową dla znaczenia strony trzymaj w statycznej powłoce albo szybko rozwiązującej się granicy. Cięższe sekcje pomocnicze streamuj później.

Najbezpieczniejsze założenie jest takie:

  1. Tytuł, lead, H1, opis i główna treść powinny być dostępne bez długiego czekania na request-time data.
  2. Skeleton nie jest treścią SEO i traktuj go wyłącznie jako element UX.
  3. Jeśli ważna sekcja streamuje się długo albo zależy od personalizacji, nie zakładaj, że każdy crawler zobaczy ją tak samo szybko jak użytkownik.
  4. Weryfikuj efekt w praktyce przez Google Search Console i renderowany HTML.

W Next.js 16 dynamiczne generateMetadata może być streamowane niezależnie od UI i wstrzykiwane po rozwiązaniu obietnicy. Dla botów wymagających metadanych w <head>, takich jak Twitterbot, Slackbot czy Bingbot, Next.js domyślnie wyłącza ten mechanizm i czeka na metadane przed rozpoczęciem streamingu. Zachowanie można zmienić opcją htmlLimitedBots, ale nie rób tego bez testów crawlerów i podglądów udostępniania.

Weryfikacja: użyj inspekcji adresu URL w Google Search Console, wykonaj test wersji opublikowanej i sprawdź wyrenderowany HTML. Dodatkowo porównaj „Wyświetl źródło strony” z DOM-em w DevTools, bo źródło pokazuje powłokę wysłaną przez serwer, a DOM także fragmenty dołączone później.

Zaawansowane wzorce Suspense w Next.js

Zagnieżdżony Suspense

Możesz zagnieżdżać granice, tworząc hierarchię ujawniania treści:

Code
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
 
  return (
    <main>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={id} />
        {/* Granica wewnętrzna nie ujawni się przed nadrzędną. */}
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews id={id} />
        </Suspense>
      </Suspense>
    </main>
  )
}

Grupowanie granic Suspense

Jeśli kilka komponentów powinno pojawić się razem, grupuj je w jednej granicy. Jeżeli mają niezależne czasy oczekiwania i mogą pojawiać się osobno, oddzielne granice dadzą szybsze częściowe ujawnienie.

Code
<Suspense fallback={<OrderDetailsSkeleton />}>
  {/* Te komponenty pojawią się razem. */}
  <OrderHeader orderId={id} />
  <OrderItems orderId={id} />
  <OrderTotal orderId={id} />
</Suspense>

Streaming z Promise.all dla równoległych fetchów

Code
async function fetchJson<T>(url: string): Promise<T> {
  const response = await fetch(url)
  if (!response.ok) throw new Error(`Błąd ${response.status} dla ${url}`)
  return response.json() as Promise<T>
}
 
async function DashboardStats() {
  // Równoległe pobieranie — łączny czas = najwolniejsze zapytanie
  const [revenue, orders, customers] = await Promise.all([
    fetchJson<{ total: number }>('https://api.example.com/revenue'),
    fetchJson<{ count: number }>('https://api.example.com/orders'),
    fetchJson<{ active: number }>('https://api.example.com/customers'),
  ])
 
  return (
    <div className="grid grid-cols-3 gap-4">
      <StatCard label="Przychód" value={revenue.total} />
      <StatCard label="Zamówienia" value={orders.count} />
      <StatCard label="Klienci" value={customers.active} />
    </div>
  )
}

Promise.all uruchamia niezależne operacje równolegle, ale pokazuje cały komponent dopiero po zakończeniu najwolniejszej i odrzuca całość, gdy jedna obietnica zawiedzie. Użyj go, gdy dane tworzą jedną logiczną sekcję. Jeżeli części mogą pojawiać się lub ulegać awarii niezależnie, rozdziel je na osobne komponenty i granice Suspense. Gdy jedno zapytanie wymaga wyniku poprzedniego, sekwencja jest uzasadniona, ponieważ streaming może pokazać wcześniejszy etap, ale nie usunie zależności.

Suspense, cache i dane dynamiczne w Next.js 16

Samo owinięcie komponentu w <Suspense> nie powoduje streamingu. Granica pokaże fallback dopiero wtedy, gdy potomek faktycznie zawiesi renderowanie, np. czekając na asynchroniczne dane. Komponent synchroniczny zostanie wyrenderowany normalnie. W Next.js 16 wyniki fetch nie są domyślnie zapisywane w cache; świeże dane możesz streamować w czasie żądania, a stabilne dane objąć use cache, aby znalazły się w prerenderowanej powłoce.

Jeżeli używasz Cache Components, dostęp do danych runtime, takich jak cookies(), headers() lub searchParams, powinien znaleźć się pod odpowiednią granicą Suspense. Next.js może wtedy prerenderować zawartość ponad granicą, umieścić fallback w statycznej powłoce i wykonać część zależną od żądania później. Granice umieszczaj możliwie blisko wolnej sekcji, ponieważ zbyt wysoka granica zastąpi fallbackiem również treść, która mogłaby pojawić się od razu.

Błędy i kody HTTP podczas streamingu

Suspense odpowiada za stan oczekiwania, nie jest pełnym mechanizmem obsługi błędów. Dodaj error.tsx dla segmentu routingu i loguj błędy po stronie serwera. React może podczas renderowania serwerowego wysłać fallback najbliższej granicy, a następnie ponowić render problematycznego drzewa po stronie klienta; jeśli błąd wystąpi ponownie, przejmie go najbliższa Error Boundary.

Po wysłaniu pierwszego fragmentu nagłówki odpowiedzi są już zatwierdzone, więc serwer nie może zmienić kodu HTTP. Późne notFound() lub redirect() mogą przekazać odpowiednią instrukcję w streamie, ale transportowa odpowiedź może pozostać 200. Autoryzację, sprawdzenie istnienia głównego zasobu i inne decyzje wpływające na status wykonuj przed rozpoczęciem streamingu, jeżeli poprawny kod HTTP jest wymagany przez klienta API, cache lub monitoring.

Hosting i testowanie streamingu

Na hostingu własnym sprawdź cały łańcuch: serwer Next.js, reverse proxy, load balancer i CDN. Nginx oraz część integracji chmurowych może domyślnie buforować odpowiedź, przez co użytkownik dostanie powłokę i wolną sekcję jednocześnie. Next.js zaleca wyłączenie buforowania, np. nagłówkiem X-Accel-Buffering: no, oraz potwierdzenie obsługi strumieniowania HTTP/2 albo odpowiedzi chunked w HTTP/1.1.

Testuj produkcyjny build, a nie tylko next dev. W DevTools sprawdź timing odpowiedzi i moment pojawienia się kolejnych sekcji; z terminala możesz użyć curl --no-buffer URL, aby obserwować fragmenty bez lokalnego buforowania. Dodaj kontrolowane opóźnienie wyłącznie w środowisku testowym i zweryfikuj osobno czas do powłoki, czas do każdej granicy, CLS oraz zachowanie po błędzie i po przerwaniu nawigacji.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy streaming działa tylko na Vercel?

Nie. Streaming działa na każdej platformie obsługującej Next.js w trybie serwerowym i umożliwiającej przesyłanie odpowiedzi strumieniowej, np. Vercel, Netlify, AWS czy samodzielnie hostowany Node.js. Reverse proxy, load balancer i CDN nie mogą buforować odpowiedzi. Przy statycznym eksporcie (output: 'export') nie ma streamingu danych w czasie żądania, bo nie działa serwer Next.js.

Czy każdą stronę powinienem streamować?

Nie. Strony w pełni prerenderowane lub korzystające z szybkich danych z cache często nie potrzebują fallbacku. Streaming ma największy sens w miejscach, gdzie poszczególne sekcje strony charakteryzują się zupełnie odmiennym czasem ładowania, pozwalając na natychmiastowe dostarczenie szybkiej powłoki oraz sukcesywne dopełnianie wolniejszych fragmentów w trakcie trwania tego samego żądania.

Czy streaming zwiększa zużycie serwera?

Zależy od implementacji. Sam streaming nie musi dodawać zapytań, ale renderowanie fallbacków, dłużej otwarte połączenie i większa liczba fragmentów odpowiedzi mają koszt. Źle ułożone komponenty mogą też tworzyć sekwencyjny waterfall zapytań. Mierz czas renderowania, liczbę zapytań i zużycie zasobów zamiast zakładać zerowy narzut.

Czy streaming szkodzi SEO?

Streaming jest renderowaniem serwerowym, ale ważną treść nadal warto umieszczać w statycznej powłoce albo szybko rozwiązującej się granicy. Next.js osobno obsługuje streaming metadanych i wyłącza go dla botów wymagających metadanych w head. Sprawdzaj wyrenderowany HTML i wynik inspekcji adresu, zamiast zakładać identyczne zachowanie każdego crawlera.

Dlaczego streaming może pogorszyć CLS?

Bo jeśli skeleton ma inne wymiary niż docelowa treść, układ przeskakuje po doładowaniu danych. Rozwiązanie: projektuj skeletony o identycznej wysokości i strukturze jak finalny komponent, a wymiary zweryfikuj porównawczo w Lighthouse.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Jak React Server Components wpływają na SEO i performance?

Przez lata React pchał, coraz więcej i więcej pracy do przeglądarki, w efekcie czego mamy większe bundle, hydratacja trwa dłużej, są puste loadingi i strony, które bez JavaScriptu straciły sens. App Router odwraca ten kierunek. React Server Components pozwalają renderować treść na serwerze bez wysyłania całej logiki do klienta, a Server Actions upraszczają formularze i mutacje. To ma znaczenie dla SEO, bo crawler szybciej dostaje HTML. Ma też znaczenie dla biznesu, bo użytkownik szybciej widzi i dostaje to, po co przyszedł.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak działa Partial Prerendering (PPR) w Next.js?

Przez lata rendering w Next.js stawiał nas przed skrajnym wyborem: cała strona była albo w pełni statyczna, albo w pełni dynamiczna. Partial Prerendering PPR przełamuje ten podział, sprawiając, że wstępnie wyrenderowana powłoka pojawia się natychmiast, podczas gdy dynamiczne fragmenty są strumieniowane w tle. W Next.js 16 mechanizm ten ewoluował w stronę Cache Components. Zobacz, w jakich sytuacjach rozwiązanie realnie przyspiesza aplikację, a kiedy staje się głównie niepotrzebną komplikacją.

Maciej Sala

Maciej Sala

Founder StriveLab

Core Web Vitals — jak przyspieszyć stronę i poprawić pozycję w Google

Google od wielu lat podkreśla, że wydajność i doświadczenie użytkownika mają znaczenie i Core Web Vitals są częścią sygnałów page experience. Najważniejsze jest jednak to, że wreszcie mierzą coś bardzo konkretnego: czy strona błyskawicznie pokazuje treść, czy odpowiednio reaguje na interakcje i czy nie „skacze" nieprzyjemnie podczas ładowania.

Maciej Sala

Maciej Sala

Founder StriveLab