React Server Components: co trafia do przeglądarki
React Server Components w Next.js 16 na żywym demo: co zostaje na serwerze, co trafia do przeglądarki jako JavaScript, a co niesie HTML i payload RSC.

React Server Components (komponenty serwerowe) to komponenty, których kod działa tylko na serwerze. W App Routerze Next.js każda strona i każdy layout jest nim domyślnie, a przeglądarka dostaje to, co wyrenderowały, jako HTML i drugi raz w payloadzie RSC, nigdy ich kod. Jako JavaScript trafia do niej tylko plik zaczynający się od "use client" i wszystko, co on importuje. Takie Client Components i tak są najpierw renderowane do HTML na serwerze, a potem hydratowane w przeglądarce. Dane przechodzą z serwera do klienta tylko przez serializowalne propsy, a Server Component przekazany jako children zostaje wynikiem z serwera nawet wewnątrz Client Component. Wszystko to pokazuje jedno demo na Next.js 16.4.0 z React 19.3.0, a dziewięć testów w Playwright sprawdza zachowanie demo opisane w tym tekście. We własnej aplikacji używaj wersji z poprawkami: poprawki RCE w React i Next.js z października 2026 dotyczą tej samej warstwy.
Czym są React Server Components?
Dokumentacja Reacta opisuje Server Components (otwiera się w nowej karcie) jako komponenty, które renderują się z wyprzedzeniem, „in an environment separate from your client app or SSR server”, czyli w środowisku oddzielonym od aplikacji w przeglądarce i od serwera SSR. W Next.js tym środowiskiem jest serwer, w czasie builda albo przy żądaniu. Komponentu nie oznacza się jako serwerowego: Server Components nie mają dyrektywy (otwiera się w nowej karcie), a "use server" oznacza Server Functions. Oznaczasz drugą stronę, przez "use client".
Przewodnik Next.js po granicy między serwerem a klientem (otwiera się w nowej karcie) ujmuje różnicę w jednej tabeli. Tu z tym, co z niej wynika przy pisaniu kodu:
| Server Component | Client Component | |
|---|---|---|
| Oznaczenie | brak (domyślne w app/) | "use client" na początku pliku |
| Renderuje się na serwerze | tak | tak, do HTML |
| Działa w przeglądarce | nie | tak, tam się hydratuje |
| Jego kod i importy trafiają do JavaScriptu | nie | tak |
useState, useEffect, onClick | nie | tak |
| Może czytać sekrety, pliki, bazę danych | tak | nie: traktuj go jak kod przeglądarki |
Co trafia do przeglądarki: HTML, payload RSC i JavaScript klienta
Przy pierwszym wczytaniu strony w Next.js (otwiera się w nowej karcie) dokumentacja wymienia trzy rzeczy, z których korzysta przeglądarka: HTML do szybkiego, nieinteraktywnego podglądu, payload RSC do uzgodnienia drzew serwera i klienta oraz JavaScript do hydratacji Client Components. Payload RSC to zwarta, zserializowana postać wyrenderowanego drzewa Server Components. Zawiera ich wynik, miejsca na Client Components z odwołaniami do ich plików JavaScript i wszystkie propsy, które Server Component przekazał do Client Component.
Server Component w demo podpisuje czas żądania kluczem HMAC. Klucz i moduł crypto z Node siedzą w osobnym pliku, który zaczyna się od import "server-only":
import "server-only"; import { createHmac } from "node:crypto"; // A made-up key, public in this repo. In an app it comes from process.env. Either way it lives// only in the server module graph: import "server-only" turns an import from a "use client"// file into a build error.const SIGNING_KEY = "lab-demo-key-stays-on-the-server"; /** The first 12 hex characters of an HMAC-SHA256 of `value`, made with Node's crypto module. */export function sign(value: string): string { return createHmac("sha256", SIGNING_KEY).update(value).digest("hex").slice(0, 12);}Karta, która z niego korzysta, to funkcja asynchroniczna bez dyrektywy:
import { io } from "next/cache"; import { sign } from "./signing";import styles from "./rsc.module.css"; // Server Components: no directive, so they stay in the server module graph. The browser// receives what they render (as HTML and in the RSC payload), never this code. const renderedOn = () => (typeof window === "undefined" ? "server" : "browser"); export async function ServerCard() { // With Cache Components on, io() keeps what follows out of the build's static shell, // so the server signs a fresh time on every request. await io(); const signedAt = new Date().toISOString(); return ( <section className={styles.card} aria-labelledby="server-component"> <h2 id="server-component">Server Component</h2> <dl className={styles.rows}> <div> <dt>Rendered on</dt> <dd>{renderedOn()}</dd> </div> <div> <dt>Signed at</dt> <dd> <time dateTime={signedAt}>{signedAt.slice(11, 19)} UTC</time> </dd> </div> <div> <dt>Signature</dt> <dd> <code>{sign(signedAt)}</code> </dd> </div> </dl> </section> );} // Passed to the Client Component as children: it renders here, on the server, and the// Client Component only places its output.export function ServerNote() { return <span>{renderedOn()}, passed in as children</span>;}Testy sprawdzają na buildzie produkcyjnym, co naprawdę dociera:
- Odpowiedź HTML ma podpis w znacznikach i ten sam podpis drugi raz w skrypcie
<script>wpisanym w tę samą odpowiedź: to payload RSC, bo przy pierwszym wczytaniu payload RSC przychodzi razem z HTML (otwiera się w nowej karcie). - Podpis to HMAC-SHA256 pełnego znacznika czasu ISO, razem z datą i milisekundami, liczony z kluczem z
signing.ts. Ten znacznik stoi w atrybuciedatetimeelementu<time>, a strona pokazuje z niego tylko godzinę, minuty i sekundy. Test czyta ten atrybut i sam liczy HMAC jeszcze raz. - Gdy strona się wczytuje, hydratuje i przyjmuje trzy kliknięcia, klucza nie ma w żadnej odpowiedzi: ani w HTML, ani w payloadzie, ani w pliku JavaScript.
- Tekst z
counter.tsxjest w pliku JavaScript. Tekstów, które istnieją tylko wserver-card.tsxipage.tsx, nie ma w żadnym.
To samo sprawdzisz we własnej aplikacji. Otwórz źródło strony i wyszukaj wartość, którą wyrenderował Server Component: pojawi się w znacznikach i drugi raz w skrypcie. Potem w DevTools przeszukaj wczytane pliki JavaScript pod kątem tekstu, który jest tylko w źródle Server Component: nie powinno go tam być.
Jak sprawdzić, który kod zostaje na serwerze
Decyduje jedna zasada: kod przechodzi przez granicę przez importy (otwiera się w nowej karcie). Plik z "use client" to wejście do grafu modułów klienta, a każdy moduł, który importuje, trafia do bundla klienta, z własną dyrektywą albo bez niej. Plik, którego nie importuje żaden plik klienta, zostaje na serwerze.
Client Component w demo to licznik:
"use client"; import { useState, useSyncExternalStore, type ReactNode } from "react"; import styles from "./rsc.module.css"; const subscribe = () => () => {}; // A Client Component: "use client" puts this file and everything it imports in the browser's// JavaScript. It still renders to HTML on the server first, then hydrates in the browser.export function Counter({ children }: { children: ReactNode }) { const [count, setCount] = useState(0); // The server snapshot (false) is used on the server and during hydration; once hydrated, // React renders again with the browser's snapshot (true). const hydrated = useSyncExternalStore(subscribe, () => true, () => false); return ( <section className={styles.card} aria-labelledby="client-component"> <h2 id="client-component">Client Component</h2> <dl className={styles.rows}> <div> <dt>Rendered on</dt> <dd>{hydrated ? "browser, after hydration" : "server, not hydrated yet"}</dd> </div> <div> <dt>Count</dt> <dd> <output aria-label="Count">{count}</output> <button type="button" onClick={() => setCount((n) => n + 1)}> Add one </button> </dd> </div> <div> <dt>Children</dt> <dd>{children}</dd> </div> </dl> </section> );}Wiersz „Rendered on” pokazuje, że Client Component działa w obu miejscach. Z wyłączonym JavaScriptem przeglądarka pokazuje tylko HTML z serwera, a wiersz brzmi „server, not hydrated yet”, z licznikiem na 0. Z włączonym brzmi „browser, after hydration”. Przełącza go useSyncExternalStore (otwiera się w nowej karcie), którego getServerSnapshot działa tylko na serwerze i podczas hydratacji. Dokumentacja hydrateRoot w React (otwiera się w nowej karcie) wymienia sprawdzanie typeof window w renderze jako częstą przyczynę błędów hydratacji, bo serwer i przeglądarka wyrenderowałyby co innego.
Stronę serwera zabezpiecza import "server-only" (otwiera się w nowej karcie) w każdym module, który nigdy nie może trafić do przeglądarki. Zaimportowanie takiego modułu do Client Component kończy się wtedy błędem builda, a nie cichym wyciekiem. W Next.js nie trzeba instalować tej paczki: Next.js obsługuje ten import sam. Zmienne środowiskowe działają podobnie: do bundla klienta trafiają tylko te z prefiksem NEXT_PUBLIC_, a resztę Next.js zastępuje pustym napisem.
Server Components w Client Components: wzorzec z children
Strona wkłada do licznika drugi Server Component, ServerNote, jako children:
import { Suspense } from "react"; import { Counter } from "./counter";import { ServerCard, ServerNote } from "./server-card";import styles from "./rsc.module.css"; // The page is a Server Component too: pages and layouts are, unless a file says "use client".export default function Page() { return ( <div className={styles.stack}> <Suspense fallback={<p className={styles.pending}>Signing on the server…</p>}> <ServerCard /> </Suspense> <Counter> <ServerNote /> </Counter> <p className={styles.hint}> Add one changes the count in the browser and leaves the signature as it is. <a href="">Reload</a> and the server signs a new time. </p> </div> );}Na zhydratowanej stronie notka nadal brzmi „server, passed in as children”, a trzy kliknięcia renderują licznik od nowa, nie zmieniając jej. Dokumentacja wyjaśnia dlaczego: wyrenderowany element Reacta da się zserializować, więc wynik Server Component przechodzi przez granicę jako dane, a Client Component tylko go umieszcza. Server Components przekazane jako children (otwiera się w nowej karcie) renderują się na serwerze z wyprzedzeniem, a ich wynik niesie payload RSC. Właścicielem ServerNote jest Page (w jego kodzie jest JSX), więc ServerNote renderuje się na serwerze; Counter jest tylko jego rodzicem w wyrenderowanym drzewie. Według zasady z dokumentacji zaimportowanie ServerNote w counter.tsx wciągnęłoby server-card.tsx do grafu modułów klienta, a za nim signing.ts, a import "server-only" zamieniłby to w błąd builda.
Tak samo używa się kontekstu Reacta: Server Components nie obsługują kontekstu (otwiera się w nowej karcie), więc provider to Client Component, który przyjmuje children, a drzewo wyrenderowane na serwerze trafia do środka.
Czy RSC to to samo co SSR?
Nie, a demo pokazuje oba naraz. HTML licznika pochodzi z serwera: to renderowanie po stronie serwera, a renderowanie po stronie serwera jest starsze niż RSC (otwiera się w nowej karcie), jak zauważa przewodnik po granicy. Next.js robi je w czasie builda albo przy żądaniu. Licznik pozostaje jednak Client Component, bo jego kod jest wysyłany i działa jeszcze raz w przeglądarce. „Renderowany na serwerze” mówi, jak powstał HTML. „Server Component” mówi, gdzie działa kod komponentu i czy jest wysyłany.
To, kiedy serwer renderuje, to osobna kwestia. Podpisany czas w demo powstaje przy każdym żądaniu: po przeładowaniu test widzi nowy. Lab działa z włączonymi Cache Components (otwiera się w nowej karcie), a dokumentacja await io() (otwiera się w nowej karcie) wyjaśnia, co to znaczy dla wartości takiej jak new Date(): zapisujesz ją raz przez use cache w statycznej powłoce albo trzymasz poza statyczną powłoką przez await io(), jak robi to karta w <Suspense>.
Dlaczego kliknięcie nie dociera do serwera, a przeładowanie tak
Trzy kliknięcia w Add one ustawiają licznik na 3 bez żadnego żądania sieciowego, a czas, podpis i notka zostają bez zmian. Według przewodnika po granicy Server Component może renderować się ponownie, gdy Next.js renderuje trasę od nowa, na przykład przy nawigacji, odświeżeniu albo po rewalidacji. W demo zmiana stanu w przeglądarce renderuje od nowa Client Component bez żadnego żądania do serwera i nie zmienia wyniku Server Components.
Po przeładowaniu serwer działa znowu: test widzi późniejszy czas, nowy podpis i licznik z powrotem na 0, bo pełne wczytanie zaczyna Client Component od stanu początkowego.
Jak podzielić stronę Next.js na Server i Client Components
Zacznij od Server Components
Strony i layouty w
app/są domyślnie Server Components. Tam pobieraj dane i czytaj sekrety, bez żadnej dyrektywy.Daj use client najmniejszej interaktywnej części
Dodaj
"use client"do pliku, który potrzebuje stanu, efektów, obsługi zdarzeń albo API przeglądarki, na przykład przycisku, filtra czy pola formularza, a nie do całej strony wokół. Wszystko, co ten plik importuje, też zostanie wysłane.Oznacz moduły serwerowe przez server-only
Dodaj
import "server-only"do każdego modułu, który czyta sekret, bazę danych albo system plików. Błędny import zatrzyma wtedy build.Przekazuj małe, serializowalne propsy
Wysyłaj do Client Component tylko pola, które pokazuje: napisy, liczby, zwykłe obiekty, tablice, Date, Map, Set, Promise i JSX. Nigdy nie przekazuj całego rekordu z bazy ani zwykłej funkcji: przez granicę przechodzą tylko Server Functions.
Przekazuj UI z serwera jako children
Gdy UI wyrenderowane na serwerze ma się znaleźć w Client Component, na przykład w modalu, panelu zakładki albo providerze kontekstu, przekaż je z Server Component jako
childrenalbo inny prop, zamiast je importować.Sprawdź build produkcyjny
Uruchom
next buildinext start, otwórz źródło strony i wyszukaj tekst, który ma zostać na serwerze, we wczytanym JavaScripcie, tak jak robi to test demo.
Co psuje podział na serwer i klienta
Propsy trafiają do przeglądarki
Każdy prop, który Server Component przekazuje do Client Component, jest serializowany do payloadu RSC. Przewodnik Next.js po bezpieczeństwie danych (otwiera się w nowej karcie) pokazuje typowy wyciek: strona przekazuje cały wiersz użytkownika do komponentu profilu z "use client", a do przeglądarki trafia każda kolumna tego wiersza, także te, których komponent w ogóle nie pokazuje. Zwracaj z warstwy danych oznaczonej server-only mały obiekt tylko z publicznymi polami. API taint z Reacta potrafią zablokować przekazanie konkretnego obiektu albo wartości do klienta (w Next.js za opcją experimental.taint (otwiera się w nowej karcie)), ale przewodnik traktuje je jako dodatkową warstwę, a nie zamiennik filtrowania.
Funkcje nie przechodzą
Przekazanie zwykłej funkcji jako propa z Server Component do Client Component nie przejdzie, więc onClick zdefiniowany na serwerze nie zadziała. Lista serializowalnych propsów (otwiera się w nowej karcie) w dokumentacji Reacta dopuszcza tylko Server Functions i nie obejmuje też instancji klas ani obiektów z prototypem null. Definiuj handlery w Client Component albo przekaż Server Function oznaczoną "use server", która przechodzi jako odwołanie.
„use client” za wysoko
"use client" na layoucie albo stronie przenosi do grafu klienta wszystko pod spodem: każdy import i każdy komponent, który ten plik renderuje bezpośrednio. Dokumentacja Next.js (otwiera się w nowej karcie) zaleca dawać dyrektywę interaktywnemu liściowi, na przykład polu wyszukiwania, a layout wokół zostawić jako Server Component.
Komponenty zewnętrzne bez dyrektywy
Komponent z biblioteki, który używa useState, ale nie ma "use client", wywołuje błąd, gdy renderuje go Server Component. Dokumentacja radzi, jak użyć takich komponentów zewnętrznych (otwiera się w nowej karcie): opakuj komponent we własny plik z dyrektywą, który go reeksportuje.
Podatności React Server Components: aktualizuj React i Next.js
Wydanie React 19.3.1 wymienia przy React Server Components jedną zmianę, „Improve transport protocol” (otwiera się w nowej karcie). 14 października 2026 React opublikował krytyczny komunikat Unauthenticated Remote Code Execution in React Server Components (otwiera się w nowej karcie) (zdalne wykonanie kodu bez uwierzytelnienia). Wymienia w nim paczki react-server-dom-webpack, react-server-dom-turbopack i react-server-dom-parcel w wersjach od 19.0.0 do 19.3.0, z poprawkami w 19.0.9, 19.1.10, 19.2.9 i 19.3.1. Samej paczki react nie ma na tej liście, więc sprawdź te paczki także w lockfile. Tego samego dnia Next.js 16.4.1 (otwiera się w nowej karcie) naprawił trzy własne podatności. W naszym przeglądzie nowości z 18 października 2026, podlinkowanym na początku tekstu i w dalszej lekturze, jest, czego dotyczy każda z nich i którą wersję wziąć.
Demo sprawdziliśmy na Next.js 16.4.0, czyli w zakresie podatności Next.js, z React 19.3.0. Na wersjach z poprawkami jeszcze go nie uruchomiliśmy; na nich buduj własną aplikację.
Czy React Server Components to to samo co SSR?
Nie. SSR mówi, jak powstaje HTML: przy każdym żądaniu, a nie w czasie builda. Server Component mówi, gdzie działa kod komponentu i czy trafia do przeglądarki. Client Components też są renderowane do HTML na serwerze, a potem hydratowane swoim JavaScriptem; kod Server Component nigdy nie trafia do przeglądarki.
Czy React Server Components zmniejszają bundle?
Ich własny kod i to, co importują, nie trafia do bundla klienta, więc ciężka biblioteka używana tylko w Server Component nic nie kosztuje przeglądarkę. Client Components nadal są wysyłane razem ze wszystkim, co importują, a payload RSC dokłada do strony wyrenderowany wynik jako dane. Zysk zależy od tego, jak duża część strony może zostać na serwerze.
Czy Server Components mogą używać useState albo useEffect?
Nie. Server Components nie są wysyłane do przeglądarki, więc nie mają stanu, efektów ani obsługi zdarzeń. Interaktywną część przenieś do małego Client Component z 'use client' i wyrenderuj go z Server Component.
Czy Server Components są stabilne w React 19?
Tak. Dokumentacja Reacta mówi, że Server Components w React 19 są stabilne i nie zepsują się między wersjami minor. Niskopoziomowe API, z których korzystają bundlery i frameworki, nie trzymają się semver, dlatego frameworki takie jak Next.js przypinają konkretną wersję Reacta.
Czym różnią się Server Components od Server Actions?
Server Component renderuje UI na serwerze i nie ma żadnej dyrektywy. Server Action (Server Function) to funkcja oznaczona 'use server', którą przeglądarka może wywołać, na przykład z formularza. 'use server' nigdy nie oznacza Server Component.
