Co dziś znaczy „frontend” w Next.js?
Frontend odpowiada za doświadczenie użytkownika, ale nie cały jego kod musi wykonywać się w przeglądarce. Serwer może przygotować listę produktów, opis usługi i układ strony. Klient obsłuży otwieranie panelu, przeciąganie elementów czy roboczą wartość pola wyszukiwania.
W App Routerze strony i layouty są domyślnie komponentami serwerowymi. Server Component nie oznacza jednak renderowania przy każdym żądaniu. Może wykonać się podczas prerenderowania albo obsługi żądania — zależnie od danych i konfiguracji cache.
| 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 |
'use client' wyznacza granicę w grafie importów. Importowane przez ten moduł komponenty i zależności mogą wejść do paczki klienta, nawet jeśli same nie zawierają dyrektywy. Dlatego dodanie jej do dużego kontenera strony ma inne konsekwencje niż wydzielenie małego przycisku.
Nie oznacza to, że wszystko wizualnie zagnieżdżone w komponencie klienckim musi stać się klienckie. Serwerowy rodzic może przekazać przygotowaną zawartość przez children. Liczy się sposób kompozycji i importowania, a nie samo położenie elementu w JSX.
Komponent kliencki nie oznacza „bez SSR”. Jego początkowy HTML może powstać na serwerze, a hydratacja dołączy obsługę interakcji. Pierwszy render w obu środowiskach musi być zgodny. Czas, losowe wartości czy odczyt ustawień przeglądarki podczas renderowania mogą tę zgodność naruszyć. Hydration mismatch ma również inne przyczyny, np. niepoprawne zagnieżdżenie HTML; nie jest automatycznym dowodem błędnej architektury.
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.
Kryteria doboru biblioteki i ograniczenia pobierania danych w efekcie omawia porównanie React Query, SWR i useEffect.
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. Debounce ogranicza częstotliwość aktualizacji. Trzeba również ustalić, które zmiany dopisują wpis do historii, a które tylko zastępują bieżący adres. Inaczej przycisk Wstecz zacznie odtwarzać każdą wpisaną literę.
Parametry URL nie wykluczają indeksowania. Dlatego techniczna wygoda zapisywania filtrów wymaga decyzji SEO: które kombinacje tworzą wartościowe strony, a które są jedynie wariantami użytkowymi. Nie należy automatycznie kierować każdego adresu kanonicznego do kategorii bazowej, jeśli podstrony reprezentują odrębną treść. Trzeba też ograniczyć generowanie niepotrzebnych kombinacji w linkowaniu.
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ć zmianę danych wokół operacji i jej wyniku. W Next.js Server Actions umożliwiają wykonanie zapisu na serwerze. useActionState pomaga przedstawić wynik i stan oczekiwania, a useOptimistic — pokazać przewidywany rezultat przed zakończeniem operacji.
Punkt wyjścia dla prostego formularza to pola HTML, akcja oraz walidacja serwerowa. Walidacja w przeglądarce skraca drogę do poprawienia błędu, lecz nie zastępuje kontroli na serwerze. Każda operacja zapisu musi ponownie sprawdzić użytkownika i jego prawo do modyfikowanego zasobu.
Po mutacji trzeba odpowiedzieć na jeszcze jedno pytanie: skąd reszta interfejsu dowie się o zmianie? Jeśli widok korzysta z cache serwerowego i klienckiego, odświeżenie tylko jednej warstwy może pozostawić sprzeczne informacje. Sposób unieważnienia danych należy do projektu mutacji, a nie do późniejszego dopracowania UI.
Formularz z Server Action przekazaną do action, renderowany w komponencie serwerowym, może obsługiwać wysłanie bez załadowanego JavaScriptu. Nie oznacza to, że dowolny formularz z Actions ma tę właściwość. Własne zdarzenia, pola dostępne dopiero po interakcji i kliencka nawigacja między krokami mogą tworzyć zależność od JS.
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
Design system powinien określać nie tylko wygląd przycisku, lecz także jego zachowanie podczas oczekiwania, sposób komunikowania błędu i obsługę klawiatury. To kontrakt między częściami aplikacji i osobami, które je rozwijają.
W architekturze RSC kontrakt obejmuje także 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 zaczyna się od ustalenia, na co czeka użytkownik. Wolny serwer, duży obraz i przeciążony główny wątek przeglądarki wymagają innych działań. W pracy nad samym frontendem warto jednak przyjąć kolejność, która chroni przed dopieszczaniem zbędnego kodu.
- 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ę.
RSC nie oznacza zerowego transferu. Do przeglądarki nadal trafiają HTML, dane protokołu RSC i kod komponentów klienckich. Przekazanie tysięcy rekordów do niewielkiego widgetu potrafi zniwelować korzyść 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. Gdy jest skonfigurowany i obejmuje dany kod, automatyzuje część memoizacji. Sama obecność Reacta 19 nie dowodzi jednak, że kompilator działa w projekcie. Nie usuwa on też kosztu sieci ani nadmiarowych zależności.
W nowym kodzie warto korzystać z jego optymalizacji, a ręczną kontrolę zachować dla uzasadnionych przypadków. 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. LCP opisuje pojawienie się największego elementu treści, INP reakcję na interakcje, a CLS stabilność układu. Wynik laboratoryjny pomaga diagnozować, lecz nie zastępuje doświadczeń użytkowników.
Typy i bezpieczeństwo po stronie klienta
Typy mają uniemożliwiać sprzeczne stany
TypeScript daje najwięcej, gdy opisuje reguły, a nie tylko kształt obiektu. Unia rozróżniana dla wyniku operacji może wykluczyć sytuację, w której komponent jednocześnie pokazuje sukces i błąd. Precyzyjne warianty propsów ograniczają nieprawidłowe konfiguracje, a generyki zachowują relacje między argumentem i wynikiem.
Typ 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.
Klient nie jest miejscem na sekret ani decyzję o uprawnieniach
Kod wykonywany w przeglądarce i przesłane do niej dane są dostępne użytkownikowi. Klucz prywatnego API musi pozostać na serwerze. Dotyczy to również danych: komponent serwerowy nie ochroni sekretu, jeśli przekaże go w propsach do klienta. Do UI wysyłaj wyłącznie pola potrzebne do jego 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 utrudnia odczyt cookie przez JavaScript, 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.
Ukrycie przycisku w UI nie stanowi autoryzacji. Server Action oraz endpoint muszą sprawdzać uprawnienia przy każdym wrażliwym wywołaniu, niezależnie od tego, z jakiej strony użytkownik miał do nich trafić.
Jak to testować
Granica serwer–klient pomaga również rozdzielić testy. Funkcja ustalająca dostępne warianty produktu nie potrzebuje uruchomionej przeglądarki. Ścieżka „zmień wariant, zapisz, odśwież stronę” już wymaga sprawdzenia współpracy kilku warstw.
| 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 asynchronicznych Server Components w popularnych narzędziach jednostkowych jest ograniczone. Dokumentacja Next.js dla Jest zaleca w tym obszarze E2E. Nie oznacza to rezygnacji z testowania logiki serwerowej: można ją wydzielić i sprawdzać niezależnie.
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.


