Co dziś znaczy „frontend” w Next.js?
Choć frontend odpowiada za całościowe doświadczenie użytkownika, nie cały jego kod musi być wykonywany bezpośrednio w przeglądarce. Serwer doskonale sprawdza się przy przygotowaniu listy produktów, opisów usług czy całej struktury witryny. Samej przeglądarce warto pozostawić jedynie płynną obsługę zdarzeń i stanu lokalnego, czyli otwieranie paneli, interakcje drag and drop czy bieżącą wartość pola wyszukiwania.
W App Routerze strony i layouty stanowią domyślnie Server Components i właśnie to określenie definiuje miejsce wykonania kodu, a nie sztywny wymóg renderowania przy każdym żądaniu. Komponent serwerowy może być renderowany zarówno w fazie prerenderowania, jak i dynamicznie podczas obsługi żądania — w zależności od strategii cache'owania oraz samej natury pobieranych danych.
| Warstwa | Gdzie się wykonuje | Co o tym decyduje |
|---|---|---|
| Server Component | Na serwerze, podczas prerenderowania lub żądania | Użycie w drzewie serwerowym oraz wymagania danych |
| Client Component | Przy pierwszym wejściu zwykle także na serwerze dla HTML; następnie w przeglądarce | Granica modułów wyznaczona przez 'use client' |
| Obsługa kliknięć, efekty, API przeglądarki | W przeglądarce | Interakcja, cykl życia i dostęp do środowiska klienta |
| Server Function użyta jako akcja | Na serwerze po wywołaniu | Serwerowa funkcja udostępniona przez framework |
| CSS i układ responsywny | W przeglądarce | Reguły stylów oraz możliwości i rozmiar obszaru wyświetlania |
Dyrektywa 'use client' wyznacza ścisłą granicę w drzewie zależności: moduł oznaczony tą dyrektywą oraz wszystkie importowane przez niego komponenty i biblioteki wchodzą do paczki klienckiej, nawet jeśli same jej nie posiadają. Z tego powodu umieszczenie 'use client' na poziomie dużego kontenera ma znacznie poważniejsze konsekwencje dla rozmiaru bundle'a niż wydzielenie małego, interaktywnego przycisku. Co istotne, wizualne zagnieżdżenie w komponencie klienckim nie wymusza wykonania kodu po stronie przeglądarki — komponent serwerowy może przekazać wstępnie wyrenderowaną zawartość za pomocą właściwości children. O ostatecznym środowisku wykonania decyduje bowiem architektura importów i sposób kompozycji, a nie sama struktura drzewa JSX.
Komponent kliencki nie oznacza braku renderowania po stronie serwera (SSR). Jego bazowy kod HTML nadal może powstać na serwerze, podczas gdy późniejszy proces hydratacji odpowiada jedynie za dołączenie obsługi zdarzeń i interakcji. Wymaga to bezwzględnej spójności pomiędzy pierwszym renderem serwerowym a klienckim. Przyczyny niezgodności hydratacji często wynikają z użycia dynamicznego czasu, losowych wartości czy odczytu ustawień przeglądarki bezpośrednio podczas renderowania, ale mogą mieć też prozaiczne podłoże, jak niepoprawne zagnieżdżenie znaczników HTML — nie stanowią więc automatycznie dowodu błędnej architektury całej aplikacji.
Praktyczny podział dla karty produktu: opis i parametry na serwerze, wybór wariantu i galeria po stronie klienta. Wpływ takiej decyzji na widoczność treści oraz koszt przeglądarki rozwija tekst o React Server Components, SEO i performance.
Router i model renderowania
W nowym projekcie opartym na opisanym modelu naturalnym wyborem jest App Router. Umożliwia korzystanie z RSC, zagnieżdżonych layoutów i granic oczekiwania na dane. Działająca aplikacja w Pages Routerze wymaga jednak innej kalkulacji: co konkretnie zespół zyska po migracji?
| Sytuacja | Kierunek decyzji | Uzasadnienie |
|---|---|---|
| Nowy projekt z treścią i interaktywnymi fragmentami | App Router | Pozwala zaplanować granicę serwer–klient od początku |
| Stabilny produkt w Pages Routerze | Utrzymanie albo migracja etapami | Przepisanie routingu musi rozwiązywać konkretny problem |
| Rozbudowa o odrębny obszar produktu | Pilotaż App Routera w wybranych trasach | Ogranicza zakres jednorazowej zmiany |
| Strona łączy wspólną treść i wolne dane dynamiczne | Ocena streamingu oraz PPR | Użytkownik może otrzymać część widoku wcześniej |
App i Pages Router mogą współistnieć podczas migracji, ale nie powinny obsługiwać tej samej ścieżki. Kosztem zmiany są także providery, pobieranie danych, obsługa błędów i testy nawigacji. Kryteria wyboru opisuje porównanie App Routera i Pages Routera, a kolejność prac — przewodnik migracji do App Routera.
Streaming pozwala przesyłać gotowe części odpowiedzi, gdy pozostałe jeszcze czekają na dane. Granica Suspense powinna odpowiadać fragmentowi, który ma sens samodzielnie: rekomendacjom pod produktem, a nie przypadkowemu opakowaniu całej strony. Streaming skraca oczekiwanie na część widoku, lecz nie przyspiesza samego zapytania. Szczegóły podziału znajdziesz w artykule o streamingu i Suspense w Next.js.
Partial Prerendering łączy przygotowaną wcześniej część strony z fragmentami uzupełnianymi dynamicznie. W Next.js 16 jest powiązany z modelem Cache Components, włączanym przez cacheComponents: true. Starszych przykładów z experimental.ppr nie należy przenosić bez zmian. Wyjaśnienie PPR warto czytać z uwzględnieniem wersji projektu i sposobu konfiguracji cache.
Osobna decyzja dotyczy interfejsów, w których adres reprezentuje panel lub modal. Parallel i Intercepting Routes mają sens, gdy trzeba zachować kontekst poprzedniego widoku i zapewnić bezpośrednie wejście pod URL. Zwykły lokalny modal nie potrzebuje od razu takiej architektury.
Jeśli projekt startował na starszej wersji frameworka, porównanie Next.js 15 i 14 pomaga zrozumieć wcześniejsze zmiany. Nie zastępuje jednak instrukcji aktualizacji do wersji 16.
Gdzie pobierać dane
Pytanie brzmi: kto potrzebuje danych i kiedy mają się zmienić? Opis kategorii potrzebny do pierwszego renderowania to inny przypadek niż lista zadań odświeżana podczas pracy kilku użytkowników. Jeżeli dane są potrzebne serwerowemu widokowi, pobierz je w warstwie serwerowej. RSC może korzystać z bazy lub zewnętrznego API bez dodatkowego wywoływania własnego endpointu HTTP wyłącznie po to, aby wrócić do tej samej logiki. Niezależne zapytania planuj współbieżnie: przeniesienie kilku kolejnych await na serwer nie usuwa kaskady oczekiwania.
Cache jest osobną decyzją. Sam Server Component nie określa, jak długo wynik pozostaje aktualny. Trzeba ustalić akceptowalną nieświeżość, moment unieważnienia i to, czy dane są wspólne, czy przypisane użytkownikowi. TanStack Query lub SWR zaczyna uzasadniać swoją obecność przy odświeżaniu w tle, współdzieleniu wyników między interaktywnymi widokami czy częstych zmianach danych. RSC i cache kliencki mogą współpracować: serwer dostarcza pierwszy widok, a klient obsługuje dalszy cykl życia danych.
Najczęstszy nadmiar to dołożenie React Query do treści, która przez całą wizytę pozostaje taka sama. Powstaje druga warstwa zarządzania danymi, chociaż interfejs nie potrzebuje jej możliwości. Z drugiej strony wymuszanie wyłącznie serwerowego modelu w intensywnie interaktywnym panelu też potrafi komplikować produkt.
Stan, który nie należy do serwera
Stan aplikacji nie jest jednym workiem. Inne wymagania ma otwarty tooltip, inne wybrana strona wyników, a jeszcze inne status płatności.
| Rodzaj stanu | Główne miejsce przechowywania | Dlaczego |
|---|---|---|
| Filtry, sortowanie, paginacja | URL | Widok można odtworzyć, udostępnić i przywrócić przyciskiem Wstecz |
| Otwarte menu, fokus, roboczy tekst | Stan lokalny komponentu | To chwilowy kontekst interakcji |
| Dane pobrane ze źródła | Serwer lub świadomie zarządzany cache klienta | Ich aktualność wynika z cyklu życia danych |
| Uprawnienia, cena rozliczeniowa, wynik zapisu | Serwer | Klient nie może być źródłem zaufania |
W wyszukiwarce wpisywany tekst może chwilowo żyć lokalnie, a zatwierdzone zapytanie trafić do URL. Trzeba przy tym ustalić, które zmiany dopisują wpis do historii, bo inaczej przycisk Wstecz zacznie odtwarzać każdą wpisaną literę.
Parametry URL nie wykluczają indeksowania. To, które kombinacje filtrów mają być osobnymi stronami w wyszukiwarce, a które tylko wariantami użytkowymi, jest decyzją SEO. Rozwija ją artykuł o parametrach w URL i duplikacji treści.
Spójne zachowanie adresu i interfejsu opisuje wyszukiwarka z filtrami, searchParams i stanem w URL. Dopiero powtarzalną logikę warto wydzielać do własnych hooków z typami generycznymi. Abstrakcja ma utrwalać dobry model stanu, a nie ukrywać jego duplikację.
Formularze i mutacje
React 19 Actions pozwalają organizować modyfikację danych wokół samej operacji oraz jej wyniku. W Next.js Server Actions umożliwiają bezpieczne wykonanie zapisu bezpośrednio po stronie serwera, podczas gdy hook useActionState ułatwia zarządzanie stanem oczekiwania i prezentacją rezultatu, a useOptimistic pozwala natychmiast wyświetlić przewidywany efekt interakcji. Solidnym punktem wyjścia dla prostego formularza pozostają natywne pola HTML, akcja serwerowa oraz walidacja po stronie serwera. Walidacja w przeglądarce skraca co prawda czas reakcji na błędy użytkownika, jednak w żadnym stopniu nie zastępuje weryfikacji serwerowej — każda operacja zapisu musi bezwzględnie ponownie sprawdzić tożsamość użytkownika oraz jego uprawnienia do modyfikowanego zasobu.
Po mutacji należy odpowiedzieć na jeszcze jedno pytanie: skąd reszta interfejsu dowie się o wprowadzonej zmianie? Jeśli widok korzysta jednocześnie z pamięci podręcznej serwera i klienta, odświeżenie tylko jednej z tych warstw może prowadzić do niespójności danych. Sposób unieważniania (ang. invalidation) i rewalidacji cache'u musi być zatem integralną częścią projektu samej mutacji, a nie kwestią odkładaną na etap dopracowywania UI. Formularz wykorzystujący Server Action przekazaną bezpośrednio do atrybutu action i renderowany w komponencie serwerowym potrafi obsłużyć wysyłkę danych nawet bez załadowanego kodu JavaScript. Nie oznacza to jednak, że każdy formularz z akcją serwerową zachowuje tę właściwość — własne zdarzenia interfejsu, pola renderowane dynamicznie po interakcji czy kliencka nawigacja między krokami natychmiast tworzą bezwzględną zależność od środowiska przeglądarki.
Stan kliencki nadal jest potrzebny w rozbudowanym kreatorze: do warunkowych pól, podglądu, zależności między krokami i zachowania wersji roboczej. Dobór mechanizmów pokazuje tekst o Actions, useOptimistic i useActionState, a projekt dłuższej ścieżki — poradnik o formularzu wielokrokowym w Next.js.
Warstwa UI
W architekturze RSC design system jest także kontraktem dotyczącym zależności. Prosty nagłówek czy kontener nie powinien wymagać klienta tylko dlatego, że korzysta ze wspólnego pakietu UI. Interaktywne elementy warto wydzielić tak, aby import zwykłego elementu prezentacyjnego nie sprowadzał niepotrzebnej logiki przeglądarkowej. Taką organizację rozwija design system z Tailwind, CVA i Radix UI.
Trzy przypadki szybko weryfikują jakość tego kontraktu:
- Tabela danych: sortowanie i zaznaczanie wymagają interakcji, ale nie uzasadniają pobierania całej bazy do przeglądarki. Przy dużym zbiorze najpierw ustal miejsce filtrowania i paginacji; później wybierz mechanikę tabel z TanStack Table.
- Animacje: przejście ma wyjaśniać zmianę, respektować ograniczenie ruchu i nie opóźniać zadania. Bibliotekę dodawaj do fragmentów, które jej potrzebują, zgodnie z zasadami wydajnych animacji Framer Motion.
- Responsywność: układ zależny od szerokości zwykle należy do CSS. Odczytywanie
window.innerWidthdo wyboru podstawowej wersji strony dokłada JavaScript i komplikuje zgodność pierwszego renderowania. Punktem wyjścia pozostaje RWD, mobile first i media queries.
Wydajność: co naprawdę kosztuje
Optymalizacja wydajności rozpoczyna się od precyzyjnej diagnozy, na co dokładnie czeka użytkownik: powolny serwer, zbyt duży plik graficzny czy przeciążony główny wątek przeglądarki wymagają zupełnie innych działań naprawczych. W samej warstwie frontendu sprawdza się taka kolejność:
- Sprawdź granice klienta i ciężkie importy. Czy cała strona potrzebuje hydratacji swoich interaktywnych komponentów, czy wystarczy kilka wyodrębnionych fragmentów?
- Zmierz zasoby dla konkretnej trasy. Oddziel początkowy JavaScript od kodu ładowanego później i sprawdź faktyczny transfer oraz czas wykonania.
- Usuń zbędną pracę i kaskady oczekiwania. Oceń duplikowane zapytania, wielkość przekazywanych danych, obrazy, fonty i skrypty zewnętrzne.
- Dopiero potem profiluj rendery. Znajdź kosztowne obliczenie, zbyt szeroką subskrypcję lub efekt wywołujący kolejną aktualizację.
Wykorzystanie React Server Components nie jest równoznaczne z całkowitym wyeliminowaniem transferu danych. Do przeglądarki klienta wciąż przesyłana jest struktura HTML, strumień danych protokołu RSC oraz kod źródłowy komponentów klienckich. W związku z powyższym przekazanie tysięcy rekordów do niewielkiego widgetu potrafi w praktyce w całości zniwelować korzyści wydajnościowe wynikające z ograniczenia liczby komponentów klienckich. Metody pomiaru przedstawia analiza i zmniejszanie paczki JavaScript w Next.js.
React Compiler zmienia opłacalność ręcznego dodawania useMemo, useCallback i memo, ale tylko w kodzie, który kompilator faktycznie obejmuje. Sama obecność Reacta 19 nie dowodzi, że kompilator działa w projekcie, a kosztu sieci ani nadmiarowych zależności i tak nie usunie. Istniejącej memoizacji nie usuwaj hurtowo bez sprawdzenia rezultatów. Ten podział rozwija artykuł o React Compilerze i przyszłości useMemo oraz useCallback.
Sama liczba renderów również nie jest wynikiem wydajnościowym. Dodatkowe wywołania w trybie deweloperskim nie muszą odpowiadać zachowaniu produkcji. Istotny jest koszt i wpływ na interakcję — temu służy debugowanie niepotrzebnego renderowania w React.
Efekt zmian sprawdzaj na produkcyjnym buildzie oraz, jeśli masz takie dane, w rzeczywistych wizytach, bo wynik laboratoryjny pomaga diagnozować, lecz nie zastępuje doświadczeń użytkowników. Co mierzą LCP, INP i CLS oraz jak je poprawiać, opisuje przewodnik po Core Web Vitals.
Bezpieczeństwo po stronie klienta
Typ w TypeScripcie nie waliduje danych z zewnątrz. Parametry URL, odpowiedź API i dane formularza wymagają kontroli w runtime. Rzutowanie odpowiedzi na oczekiwany interfejs nie potwierdza jej poprawności.
Kod wykonywany w przeglądarce oraz przesyłane do niej dane są w pełni dostępne dla użytkownika, dlatego tajne klucze API muszą bezwzględnie pozostać po stronie serwera. Zasada ta dotyczy również samej struktury danych — nawet komponent serwerowy nie ochroni sekretu, jeśli przekaże go bezpośrednio w propsach do komponentu klienckiego. Do warstwy interfejsu należy przekazywać wyłącznie te pola, które są niezbędne do jego prawidłowego działania.
W aplikacji Next.js z własnym zapleczem rozsądnym punktem wyjścia jest sesja oparta na cookie HttpOnly, z Secure i właściwym SameSite. Token dostępu do zewnętrznego API może pozostać na serwerze, a przeglądarka otrzymać identyfikator sesji. HttpOnly uniemożliwia odczyt cookie przez JavaScript strony, ale nie usuwa skutków XSS ani wszystkich zagrożeń związanych z wykonywaniem żądań.
localStorage nie powinien być odruchowym schowkiem na długowieczne tokeny. Samodzielna SPA korzystająca z zewnętrznego dostawcy to odrębny przypadek: sposób logowania i odświeżania musi odpowiadać architekturze oraz zaleceniom dostawcy. Warianty opisuje poradnik o tokenach, odświeżaniu sesji i autentykacji w React.
Jak to testować
Wyraźna granica serwer–klient ułatwia także rozdzielenie strategii testowej. Czysta funkcja ustalająca dostępne warianty produktu nie wymaga środowiska przeglądarki. Z kolei pełna ścieżka, czyli „zmień wariant, zapisz, odśwież stronę”, to już test integracyjny, sprawdzający współpracę kilku warstw aplikacji.
| Zakres | Rodzaj testu | Co warto potwierdzić |
|---|---|---|
| Reguły domenowe i transformacje | Jednostkowy | Poprawność wyniku i przypadki brzegowe |
| Interaktywny komponent | Komponentowy | Zdarzenia, dostępność i komunikaty |
| Dostęp do danych i mutacje | Integracyjny | Walidację, autoryzację i skutki zapisu |
| Asynchroniczna strona RSC | E2E | Wynik rzeczywistego renderowania i współpracę z routingiem |
Wsparcie dla asynchronicznych komponentów serwerowych w popularnych bibliotekach do testów jednostkowych pozostaje ograniczone, dlatego oficjalna dokumentacja Next.js rekomenduje weryfikację tego obszaru na poziomie testów E2E. Nie oznacza to jednak konieczności rezygnacji z testowania samej logiki biznesowej — wystarczy odseparować ją od warstwy prezentacji i weryfikować jako niezależne, czyste funkcje serwerowe.
Mockuj zewnętrzną granicę wtedy, gdy chcesz odizolować konkretną regułę. Jeżeli celem jest sprawdzenie autoryzacji, zastąpienie jej zawsze pozytywnym mockiem odbiera testowi sens. Dla głównej ścieżki warto potwierdzić zapis, ponowne wejście i widoczność aktualnych danych. Dobór zakresu opisuje testowanie Server Components: mocki, logika i E2E.
Lista kontrolna przed wdrożeniem
- Każda granica
'use client'ma uzasadnienie w interakcji, stanie lub zależności od przeglądarki; sprawdzono jej ciężkie importy. - Pierwsze wejście i nawigacja działają na produkcyjnym buildzie bez błędów hydratacji, również przy innych ustawieniach języka i czasu.
- Sekrety nie trafiają do paczki ani propsów klienta, a każda wrażliwa operacja sprawdza uprawnienia po stronie serwera.
- Filtry i paginacja odtwarzają się z URL, przycisk Wstecz zachowuje się zgodnie z projektem, a warianty adresów mają ustalone reguły indeksowania.
- Formularze przewidziane do działania bez JavaScriptu przetestowano z wyłączonym JS; pozostałe mają sprawdzone oczekiwanie, błędy i ponowienie.
- Po mutacji użytkownik widzi aktualne dane, a test z dwiema sesjami nie ujawnia danych innego użytkownika przez cache.
- Zmierzono początkowy JavaScript, zasoby i główne interakcje dla kluczowych tras; wyniki porównano z przyjętym budżetem wydajności.
- Główna ścieżka przeszła E2E, kontrolę obsługi klawiaturą i sprawdzenie na wąskim ekranie.


