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.

Czas czytania: 9 min czytaniaOpublikowano:

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.

Add one zmienia licznik w przeglądarce bez żadnego żądania do serwera, a czas, podpis i wiersz z children się nie zmieniają. Reload sprawia, że serwer podpisuje nowy czas. · Next.js 16.4.0Otwórz demoZobacz kod

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 ComponentClient Component
Oznaczeniebrak (domyślne w app/)"use client" na początku pliku
Renderuje się na serwerzetaktak, do HTML
Działa w przeglądarcenietak, tam się hydratuje
Jego kod i importy trafiają do JavaScriptunietak
useState, useEffect, onClicknietak
Może czytać sekrety, pliki, bazę danychtaknie: 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":

app/demos/react-server-components-explained/signing.ts
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);}
Z labu, commit b5049c6

Karta, która z niego korzysta, to funkcja asynchroniczna bez dyrektywy:

app/demos/react-server-components-explained/server-card.tsx
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>;}
Z labu, commit b5049c6

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 atrybucie datetime elementu <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.tsx jest w pliku JavaScript. Tekstów, które istnieją tylko w server-card.tsx i page.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:

app/demos/react-server-components-explained/counter.tsx
"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>  );}
Z labu, commit b5049c6

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:

app/demos/react-server-components-explained/page.tsx
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>  );}
Z labu, commit b5049c6

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

  1. Zacznij od Server Components

    Strony i layouty w app/ są domyślnie Server Components. Tam pobieraj dane i czytaj sekrety, bez żadnej dyrektywy.

  2. 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.

  3. 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.

  4. 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.

  5. 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 children albo inny prop, zamiast je importować.

  6. Sprawdź build produkcyjny

    Uruchom next build i next 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.

Dalsza lektura

Next.js: Server and Client Components (otwiera się w nowej karcie)Kiedy używać których, payload RSC, przeplatanie przez children i server-only.nextjs.org
Next.js: The Server and Client Boundary (otwiera się w nowej karcie)Gdzie działa każdy komponent, jak przechodzą kod i dane, właściciel a rodzic.nextjs.org
Next.js: Data security (otwiera się w nowej karcie)Co propsy wysyłają do przeglądarki, warstwa dostępu do danych, taint i kontrole w Server Actions.nextjs.org
React: Server Components (otwiera się w nowej karcie)Dokumentacja: czym są Server Components, komponenty asynchroniczne i stabilność w React 19.react.dev
React: 'use client' (otwiera się w nowej karcie)Jak dyrektywa oznacza moduł i jego importy oraz które propsy są serializowalne.react.dev
React: Unauthenticated Remote Code Execution in React Server Components (otwiera się w nowej karcie)Komunikat z 14 października 2026: podatne paczki i wersje 19.x z poprawką.github.com
AI dla programistówNowości web dev: RCE w React i Next.js, fast mode Opus 5.5Nowości web dev z 11–18 października 2026: łatki React i Next.js na trzy krytyczne RCE, fast mode dla Claude Opus 5.5 bez preview i Python na Vercel w GA.

Napisz, co budujemy.

Odpowiadamy w ciągu jednego dnia roboczego.