Zapisy na szkolenie w najniższej cenie do 6 lutego.
00 dni
:
00 godz
:
00 min
:
00 sek
Dołącz teraz

Jak Layout w Next.js może zabić wydajność i TTFB w Twojej aplikacji

Jak Layout w Next.js może zabić wydajność i TTFB w Twojej aplikacji

Jako programiści Next.jsa na pewno znacie i używacie w swoich aplikacjach plików layout.tsx. Pozwalają one świetnie uporządkować strukturę aplikacji i uniknąć powtarzania kodu (DRY). Jednak, mogą one stać się cichym zabójcą wydajności, jeśli nie do końca zrozumiemy, co dzieje się w komponentach renderowanych wewnątrz layoutu.

Ostatnio natrafiłem na przypadek, w którym jeden, ukryty await w komponencie zagnieżdżonym głęboko w RootLayout powodował drastyczny wzrost metryki TTFB (Time To First Byte). Wynik wyglądał fatalnie – renderowanie całej aplikacji było zablokowane. Gdy dodamy do tego wolne łącze internetowe, mamy gotowy przepis na UX niskiej jakości (zakładając, że użytkownik w ogóle poczeka na załadowanie strony).

Naprawa tego problemu była banalnie prosta, ale wymagała zrozumienia mechanizmów, jakie oferują React i Next.js. Odwzorowałem ten przypadek na potrzeby tego posta. Zachęcam Cię do przejścia ze mną przez tę krótką optymalizację – gwarantuję, że blokujący Layout w Next.js przestanie być dla Ciebie problemem.

Zły przykład: Co blokuje renderowanie?

Zacznijmy od kodu, który na pierwszy rzut oka wygląda niewinnie, ale powoduje “waterfall” w renderowaniu.

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="bg-gray-50 min-h-screen">
      <Sidebar userSlot={<UserProfile />} />
      {children}
    </div>
  );
}

Mamy layout aplikacji. Wszystko wygląda poprawnie – nie ma tutaj bezpośredniego await, więc teoretycznie nie powinno być problemu. Renderujemy Sidebar, do którego przekazujemy komponent UserProfile. Sprawdźmy jednak głębiej, co kryje się w środku:

export default async function UserProfile() {
  const user = await getUserProfile();

  return (
    <div className="mt-auto p-4 bg-gray-800 rounded-lg flex items-center gap-3 z-50">
      <Image alt="avatar" src={user.avatar} width={48} height={48} />
      <div>
        <div className="font-bold text-sm">{user.name}</div>
        <div className="text-xs text-gray-400">{user.role}</div>
      </div>
    </div>
  );
}

Oto winowajca: pobieranie danych użytkownika i nieszczęsny await.

Mniej doświadczeni deweloperzy mogą pomyśleć: “Przecież to nie jest bezpośrednio w pliku layout.tsx, więc nie powinno blokować całej strony, prawda?”. To pułapka. Ponieważ Layout jest komponentem serwerowym (RSC) i jest rodzicem dla całej reszty, Next.js musi poczekać na rozwiązanie tego Promisa (pobranie danych użytkownika), zanim wyśle choćby jeden bajt HTML-a do przeglądarki.

Dlaczego lokalnie tego nie widzisz?

Często nie dostrzegamy problemu podczas developmentu. Lokalna baza danych, zerowe opóźnienia sieciowe, mocny sprzęt – wszystko śmiga. Musimy jednak myśleć szerzej: co jeśli serwer API zwolni? Co jeśli użytkownik ma słaby zasięg w pociągu?

Na szczęście rozwiązanie jest szybkie i proste. Musimy przestać blokować renderowanie layoutu, wykorzystując Streaming i mechanizm Suspense.

Rozwiązanie: Streaming i Suspense

export default async function GoodLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="bg-gray-50 min-h-screen">
      <Sidebar
        userSlot={
          <Suspense fallback={<UserSkeleton />}>
            <UserProfile />
          </Suspense>
        }
      />
      {children}
    </div>
  );
}

Komponent UserProfile pozostał niezmieniony. Owinęliśmy go jedynie w Suspense i dodaliśmy estetyczny UserSkeleton jako stan ładowania.

Efekt: Layout nie jest już przez nic blokowany. Kod HTML (szkielet strony + layout + children) zostaje natychmiast wysłany do przeglądarki. W miejscu profilu pojawia się skeleton, a gdy dane asynchroniczne będą gotowe, Next.js strumieniuje resztę odpowiedzi i podmienia fallback na gotowy komponent.

Jak to wygląda w liczbach?

Przeprowadziłem test w WebPageTest. Funkcja zwracająca dane użytkownika została celowo opóźniona o 3 sekundy, aby zasymulować realne opóźnienia sieciowe lub obciążenie bazy danych.

Poniższe zestawienie pokazuje porównanie wersji “Złej” i “Dobrej”:

  1. Wersja Zła: Metryka TTFB wynosi ponad 3 sekundy. Przeglądarka przez ten czas widzi biały ekran, czekając na serwer.
  2. Wersja Dobra (z Suspense): Ta sama funkcja, to samo opóźnienie, ale wynik TTFB jest ponad 10-krotnie lepszy.
Porównanie wyników WebPageTest - blokujący layout vs streaming

Kliknij w zdjęcie, aby powiększyć wynik testu

Choć to przykład syntetyczny, problem ten nagminnie powtarza się w produkcyjnych aplikacjach Next.js. Nawet jeśli poprawa nie będzie 10-krotna, zyskanie kilkuset milisekund na starcie (First Paint) jest kluczowe dla UX. Layout to pierwsza warstwa, którą Next.js próbuje wyrenderować – nie pozwól, by stała się wąskim gardłem.

Porównanie wizualne (z perspektywy użytkownika)

Liczby to jedno, ale jak to odczuwa użytkownik? Nagrałem krótkie wideo pokazujące wejście na stronę.

  • Lewa strona (“Zła”): Przeglądarka “muli”, pasek ładowania stoi w miejscu. Użytkownik nie wie, czy strona działa, czy zawiesił mu się internet.
  • Prawa strona (“Dobra”): Natychmiastowa reakcja interfejsu (Instant Feedback). Użytkownik widzi szkielet aplikacji od razu.

Anatomia Streamingu: Zobacz jak serwer “karmi” przeglądarkę

Wykresy w DevTools pokazują nam tylko czas oczekiwania, ale nie pokazują, co dzieje się “pod maską” protokołu HTTP. Aby zrozumieć, dlaczego Streaming jest tak skuteczny, musimy zobaczyć, w jaki sposób fizyczne paczki danych (Chunki HTML) docierają do klienta.

Poniższe nagranie to nie tylko logi – to wizualizacja tego, jak Next.js buduje i wysyła odpowiedź w obu architekturach:

Wizualizacja przesyłu pakietów HTML: Monolit vs Chunki

  1. Wersja BAD (Blokująca): Tutaj widzimy klasyczny “Zator”. Serwer generuje HTML w pamięci, czekając na najwolniejszy await. Połączenie jest aktywne, ale nie przesyła ani jednego bajtu. Dopiero na samym końcu wysyłany jest jeden wielki, monolityczny blok. Przeglądarka przez 3 sekundy nie ma czego renderować, bo fizycznie nic nie dostała.
  2. Wersja GOOD (Streaming): Tutaj widzimy podział na Chunki.
    • Chunk #1-4: To natychmiastowy strzał – Next.js wysyła nagłówek, szkielet aplikacji i fallback. Przeglądarka ma już co robić.
    • Chunk #5-6: Po 3 sekundach, gdy dane są gotowe, serwer “dopycha” resztę HTML-a i skrypt podmieniający skeleton.

To nagranie obnaża prostą prawdę: Przeglądarka nie może wyrenderować czegoś, czego jeszcze nie otrzymała. W wersji “Złej” trzymamy HTML jako zakładnika serwera. W wersji “Dobrej” uwalniamy go kawałek po kawałku, drastycznie poprawiając odczucia użytkownika, mimo że czas generowania danych jest prawie taki sam.

Przetestuj to sam

Zachęcam do audytu własnych layoutów. Być może podobny problem występuje w Twojej aplikacji? Pamiętaj, że streaming warto stosować nie tylko w layout.tsx, ale wszędzie tam, gdzie asynchroniczne pobieranie danych blokuje wyświetlenie ważnej części interfejsu (np. głównej treści strony).

Jeżeli chcesz “na żywo” przestudiować opisany tu przypadek, przygotowałem demo:
👉 Demo aplikacji: good-bad-layout-rendering-demo.vercel.app

Oraz kod źródłowy, który łatwo przenieść do własnego projektu:
👉 Repozytorium GitHub: JarGad23/good-bad-layout-rendering-demo

A co z SEO? Client Components vs Streaming

Analizując ten przypadek, warto zadać sobie pytanie: “Czy ja w ogóle muszę to renderować po stronie serwera?”.

W przypadku elementu takiego jak Sidebar z profilem użytkownika, odpowiedź często brzmi: nie. Dane zalogowanego użytkownika są prywatne i zazwyczaj nie mają żadnej wartości dla SEO. Googlebot nie loguje się na konta użytkowników, więc i tak ich nie zobaczy.

Alternatywnym rozwiązaniem dla Suspense byłoby zamienienie Sidebaru na Client Component i pobranie danych już po stronie przeglądarki (np. używając biblioteki typu TanStack Query). Odciążyłoby to serwer i również zapewniło szybki TTFB.

Decyzja o architekturze powinna zawsze zależeć od celu biznesowego:

  • Strona marketingowa/Blog: Priorytetem jest SEO. Treść musi być w kodzie HTML.
  • Dashboard/Aplikacja SAAS: Priorytetem jest interaktywność. SEO (dla treści prywatnych) jest mniej istotne.

Czy Streaming zabija SEO? A co, jeśli strumieniujemy treść, która jest ważna dla SEO (np. listę produktów)? Czy Googlebot “zrozumie” nadchodzące kawałki (chunks) HTML-a?

Dobra wiadomość: Googlebot radzi sobie ze strumieniowaniem. Nowoczesne crawlery Google obsługują Chunked Transfer Encoding. Oznacza to, że bot potrafi “poczekać” na dociągnięcie reszty strumienia HTML i poprawnie go zaindeksować. Co więcej, dzięki Streamingowi poprawiasz metryki Core Web Vitals (szczególnie TTFB i LCP), co jest bezpośrednim sygnałem rankingowym. Streaming jest więc bezpiecznym “złotym środkiem” między wydajnością a widocznością w wyszukiwarce.


📝 Podsumowanie: 3 szybkie lekcje

  1. Uważaj na await w Layout: To potencjalne wąskie gardło całej aplikacji, ponieważ blokuje renderowanie wszystkiego, co znajduje się “pod nim”.
  2. Stosuj Streaming (Suspense): Poprawisz Perceived Performance. Użytkownik wybaczy 3-sekundowe ładowanie awatara, jeśli reszta strony (nawigacja, treść) pojawi się natychmiast.
  3. Weryfikuj Waterfall: Używaj narzędzi takich jak WebPageTest lub zakładki Network w DevTools. Nie ufaj temu, jak szybko aplikacja działa na localhost.

Słowo na koniec

Jeżeli spodobał Ci się ten temat, sprawdź inne nasze posty po więcej konkretnych porad wydajnościowych w świecie Next.js i Reacta.

A jeśli chcesz w pełni zgłębić tematykę Web Performance – przejść przez optymalizację Core Web Vitals, zaawansowane wzorce renderowania w Next.js i wydajność samego Reacta – zachęcamy do zapisu na nasze szkolenie. Wszystkie te tematy (i wiele więcej) analizujemy dogłębnie z uczestnikami na żywo. To świetna okazja na realne podniesienie swoich kompetencji.

👉 Sprawdź agendę: Szkolenie WDI - Web Performance City


Zobacz nasze wideo

Nie zwlekaj. Dołącz do gangu i przejdź przez misje.

To szkolenie to masa wiedzy, praktyki, ale też dobrej zabawy. Wierzymy, że edukacja może być po prostu FAJNA :-)

Dołączam i widzimy się 10-11. lutego 2026
WDI Web Performance City - trenerzy Bartek Miś i Jarek Gad