Przejdź do treści

React Server Components i Server Actions: SEO i wydajność

Sprawdź, jak RSC i Server Actions wpływają na HTML, JavaScript, Core Web Vitals, indeksowanie oraz bezpieczeństwo formularzy w Next.js.

Maciej Sala

Founder StriveLab

13 min czytaniaOpublikowano 31 marca 2026 (Aktualizacja 1 sierpnia 2026)

Dla stron marketingowych, blogów, landing page i serwisów opartych na treści liczą się trzy rzeczy: szybkość, indeksowalny HTML i formularze, które nie czekają na kliencki JavaScript.

Dlaczego server-first w Next.js stał się domyślnym podejściem?

Wiele aplikacji React przesuwało pobieranie danych do useEffect, przez co początkowa odpowiedź zawierała pustą powłokę oraz komunikat ładowania. Googlebot potrafi wykonać JavaScript, ale inne roboty mogą go ignorować, a użytkownik na słabszym urządzeniu nadal płaci za pobranie, parsowanie i wykonanie kodu.

W App Routerze punkt ciężkości wrócił na serwer. W praktyce oznacza to, że:

  • Komponenty renderują się na serwerze, jeśli nie oznaczysz ich jako klientowe

  • JavaScript trafia do przeglądarki tylko tam, gdzie jest faktycznie potrzebny (Client Components z dyrektywą 'use client')

  • Formularze i mutacje danych obsługuje serwer bezpośrednio (Server Actions)

  • Publiczna treść może znaleźć się w odpowiedzi HTML bez pobierania jej przez efekt uruchamiany po

To domyślny model komponentów w App Routerze, a nie gotowa strategia renderowania. RSC nie oznacza automatycznie statycznej strony. Server Component może zostać wyrenderowany podczas builda, z cache albo dla każdego żądania. Decydują o tym użyte dane, dynamiczne API i konfiguracja Cache Components.

React Server Components: jak działają i jak wpływają na wydajność?

Jak React Server Components działają pod maską?

W aplikacji renderowanej wyłącznie po stronie klienta komponenty i pobieranie danych należą do pakietu JavaScript. SSR potrafił wcześniej dostarczyć HTML, lecz kod komponentów nadal trafiał do przeglądarki w celu hydratacji. RSC dodają inną granicę: kod Server Componentu pozostaje na serwerze.

W modelu RSC:

  1. Serwer tworzy specjalny format RSC Payload z wynikiem Server Components i odwołaniami do komponentów klienckich.
  2. Pierwsze wejście otrzymuje również HTML, który pokazuje nieinteraktywny podgląd trasy przed hydratacją.
  3. Client Components odzyskują interaktywność przez hydratację, korzystając ze swojego kodu JavaScript.
  4. Kolejna nawigacja używa głównie payloadu RSC, prefetchu i przejścia po stronie klienta, bez pełnego przeładowania dokumentu.
Diagram
Model RSC: serwer renderuje treść do HTML, a JavaScript i hydratacja dotyczą tylko komponentów klienckich.

Wpływ React Server Components na Core Web Vitals

Core Web Vitals poprawią się wtedy, gdy nowa architektura faktycznie ograniczy pracę przeglądarki, skróci dostęp do głównej treści i ustabilizuje układ. Oceniaj 75. percentyl wizyt osobno dla urządzeń mobilnych i desktopowych.

Largest Contentful Paint (LCP)

mierzy czas do wyrenderowania największego elementu w widoku. RSC mogą pomóc, gdy element LCP i potrzebne mu dane powstają bez oczekiwania na klientowy fetch. Wolna odpowiedź serwera, nieoptymalny obraz lub umieszczenie hero za granicą Suspense zniwelują tę przewagę. Próg dobrego wyniku to maksymalnie 2,5 sekundy.

Interaction to Next Paint (INP)

mierzy opóźnienie między interakcją a następnym wyrenderowaniem. Mniejszy pakiet klienta może zostawić więcej czasu głównego wątku na obsługę zdarzeń. Ciężki Client Component, rozbudowany skrypt analityczny lub widget zewnętrzny nadal może przekroczyć próg 200 milisekund.

Cumulative Layout Shift (CLS)

CLS mierzy niestabilność układu. Render serwerowy nie zna automatycznie wymiarów obrazów, reklam i osadzonych widgetów. Rezerwuj miejsce w HTML i projektuj fallback Suspense o docelowych wymiarach. Dobry wynik nie przekracza 0,1.

Przykład React Server Components w sekcji blogowej

Rozważmy stronę blogową z listą artykułów i panelem kategorii. W wariancie renderowanym po stronie klienta:

Code
// Wariant klientowy: cała strona jest Client Component
'use client'
 
import { useState, useEffect } from 'react'
 
export default function BlogPage() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
 
  useEffect(() => {
    fetch('https://api.example.com/posts')
      .then((res) => res.json())
      .then((data) => {
        setPosts(data)
        setLoading(false)
      })
  }, [])
 
  if (loading) return <div>Ładowanie...</div> // Brak artykułów w początkowym HTML-u
 
  return (
    <div>
      {posts.map((post) => (
        <ArticleCard key={post.id} post={post} />
      ))}
    </div>
  )
}

Początkowy HTML tego wariantu nie zawiera artykułów. Użytkownik czeka na hydratację i kolejne żądanie, a robot bez obsługi JavaScriptu nie odczyta listy. Googlebot może ją zobaczyć po renderowaniu, lecz strona tworzy dodatkową zależność oraz później odkrywa treść i linki.

W podejściu server-first z RSC:

Code
// Wariant server-first: domyślny Server Component
import { getAllPosts } from '@/lib/posts'
import { ArticleCard } from '@/components/ArticleCard'
import { NewsletterForm } from '@/components/NewsletterForm'
 
export default async function BlogPage() {
  const posts = await getAllPosts()
 
  return (
    <main>
      <h1>Blog</h1>
      <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
        <section className="col-span-2">
          {posts.map((post) => (
            <ArticleCard key={post.slug} post={post} />
          ))}
        </section>
        <aside>
          {/* Client Component tylko ten fragment ma JS */}
          <NewsletterForm />
        </aside>
      </div>
    </main>
  )
}
Code
// components/ArticleCard.tsx, Server Component bez 'use client'
interface Post {
  slug: string
  title: string
  excerpt: string
  date: string
  readingTime: number
}
 
export function ArticleCard({ post }: { post: Post }) {
  return (
    <article>
      <h2>
        <a href={'/blog/' + post.slug}>{post.title}</a>
      </h2>
      <p>{post.excerpt}</p>
      <time dateTime={post.date}>
        {new Date(post.date).toLocaleDateString('pl-PL')}
      </time>
      <span>{post.readingTime} min czytania</span>
    </article>
  )
}
Code
// components/NewsletterForm.tsx, interaktywny Client Component
'use client'
 
import { useActionState } from 'react'
import { subscribeNewsletter } from '@/app/actions'
 
// Akcja przyjmuje (prevState, formData), czyli sygnaturę wymaganą
// przez useActionState
export function NewsletterForm() {
  const [state, formAction, pending] = useActionState(
    subscribeNewsletter,
    null,
    '/blog',
  )
 
  return (
    <form action={formAction}>
      <h3>Newsletter</h3>
      <label htmlFor="newsletter-email">E-mail</label>
      <input id="newsletter-email" type="email" name="email" required />
      <button type="submit" disabled={pending}>
        {pending ? 'Zapisuję…' : 'Zapisz się'}
      </button>
      {state?.success && <p>Zapisano!</p>}
      {state?.success === false && <p>Spróbuj ponownie za chwilę.</p>}
    </form>
  )
}

Pole nie potrzebuje useState, bo formularz przekazuje jego wartość przez FormData. useActionState dostarcza wynik i stan oczekiwania. Trzeci argument, czyli permalink, wskazuje adres używany przed załadowaniem JavaScriptu. Strona docelowa musi wyrenderować ten sam formularz, tę samą akcję i ten sam permalink, aby React przeniósł stan odpowiedzi. Bez permalinka Client Component kolejkuje wysłanie przed hydratacją i odtwarza je po załadowaniu kodu.

Formularz serwerowy daje prostsze progressive enhancement. Dodawaj Client Component dopiero wtedy, gdy potrzebujesz stanu oczekiwania lub reakcji bez przeładowania. Więcej wzorców opisuję w artykule o React 19 Actions.

Efekt: przeglądarka może otrzymać kompletny HTML z listą artykułów bez czekania na pobieranie danych i renderowanie po stronie klienta. JavaScript ogranicza się do formularza newslettera. Taka architektura daje dobre warunki do poprawy LCP i ograniczenia ryzyka CLS, ale wynik trzeba potwierdzić pomiarem na realnej stronie: obraz LCP, fonty, CSS, odpowiedź API i hosting nadal mają znaczenie.

Server Actions w Next.js: mniej endpointów dla formularzy

Server Actions to funkcje serwerowe oznaczone dyrektywą 'use server', które możesz wywołać z komponentów React. Nie kasują całego API z projektu, ale zdejmują sporo prostych endpointów: zapis formularza, aktualizacja danych, wysyłka maila, zapis leadu.

Jak Server Actions zmieniają strony marketingowe?

Typowa strona marketingowa ma kilka formularzy: newsletter, kontakt, lead magnet, webinar, kalkulator z zapisem wyniku. W tradycyjnym podejściu każdy z nich łatwo kończył jako osobny endpoint API, walidacja po dwóch stronach i dodatkowy stan ładowania.

Z Server Actions ten sam formularz kontaktowy wygląda tak:

Code
// app/actions.ts
'use server'
 
import { z } from 'zod'
 
const ContactSchema = z.object({
  name: z.string().trim().min(2).max(80),
  email: z.string().trim().email().max(254),
  message: z.string().trim().min(10).max(5_000),
  consent: z.literal('on'),
  website: z.literal(''), // Pole pułapka powinno pozostać puste.
  source: z.string().trim().max(100).optional(),
})
 
export async function submitContact(formData: FormData) {
  const parsed = ContactSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
    consent: formData.get('consent'),
    website: formData.get('website'),
    source: formData.get('source') || undefined,
  })
 
  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.flatten().fieldErrors,
    }
  }
 
  // Implementacja limitu zależy od zaufanego proxy, sesji i infrastruktury.
  const accepted = await rateLimitContactForm()
  if (!accepted) return { success: false, retryAfter: 60 }
 
  await saveLead(parsed.data)
 
  return { success: true }
}

To fragment pokazujący granicę walidacji, a nie kompletny system antyspamowy. Treść zgody musi odpowiadać rzeczywistemu celowi i podstawie przetwarzania. source jest niezaufaną informacją analityczną, więc nie może sterować ceną, uprawnieniem ani routingiem leada. saveLead powinien używać unikalnego tokenu próby i indeksu w bazie, aby ponowienie po timeoutcie nie stworzyło drugiego rekordu. Powiadomienie e-mail lepiej wysłać przez kolejkę lub wzorzec outbox po udanym zapisie.

Co dostajesz:

  • Formularz działa jako natywny mechanizm dzięki progressive enhancement w Server Component.
  • Przepływ wymaga mniejszej liczby warstw, gdy mutację wywołuje wyłącznie interfejs React.
  • Pomiar serwerowy nadal wymaga zgody, prawidłowej deduplikacji i zgodności z regułami platformy analitycznej.
  • Logika zapisu pozostaje poza klientem, więc sekrety i połączenie z bazą nie trafiają do bundle'a.

Server Action przyjmuje bezpośrednie żądania POST z pominięciem widocznego formularza. Bezpieczne identyfikatory akcji, eliminacja martwego kodu i kontrola zgodności nagłówków Origin oraz Host tworzą dodatkową osłonę Next.js, lecz nie zastępują walidacji i autoryzacji w funkcji. Dla danych użytkownika sprawdź sesję i prawo do konkretnego rekordu. Publiczny formularz potrzebuje limitowania, ochrony antyspamowej oraz idempotencji. Wzorce kontroli dostępu opisuję w artykule o RBAC w Next.js.

Jeśli reverse proxy zmienia host widoczny dla aplikacji, skonfiguruj serverActions.allowedOrigins jako krótką listę zaufanych domen. Nie dodawaj szerokiego wildcardu tylko po to, aby uciszyć błąd Origin, bo rozszerza on powierzchnię żądań akceptowanych przez aplikację.

Server Actions projektuj wyłącznie dla mutacji wywoływanych przez aplikację React. useActionState kolejkuje powiązane wywołania, bo następna akcja otrzymuje poprzedni stan. Dane odczytuj w Server Components, gdzie możesz jawnie ustalić równoległość i cache. Webhook, endpoint dla aplikacji mobilnej oraz publiczne API pozostają Route Handlerem albo osobną usługą.

Progressive Enhancement: formularz działa bez JavaScriptu

Server Actions w Next.js działają z natywnym HTML <form action>, więc formularz może działać nawet bez JavaScriptu klienta:

Code
// Ten formularz DZIAŁA bez JavaScript
export function ContactForm() {
  return (
    <form action={submitContact}>
      <label htmlFor="contact-name">Imię</label>
      <input id="contact-name" type="text" name="name" required />
      <label htmlFor="contact-email">E-mail</label>
      <input id="contact-email" type="email" name="email" required />
      <label htmlFor="contact-message">Wiadomość</label>
      <textarea id="contact-message" name="message" required />
      <input
        className="hidden"
        type="text"
        name="website"
        tabIndex={-1}
        autoComplete="off"
        aria-hidden="true"
      />
      <label>
        <input type="checkbox" name="consent" required /> Akceptuję opisaną
        zgodę na przetwarzanie danych.
      </label>
      <button type="submit">Wyślij</button>
    </form>
  )
}

Użytkownik na wolnym łączu mobilnym wysyła formularz, zanim załaduje się JavaScript. Każda sekunda opóźnienia formularza to utracone leady.

progressive enhancement

działa najprościej, gdy formularz w Server Component otrzymuje bezpośrednio Server Action. Client Component kolejkuje wysłanie do zakończenia hydratacji, a useActionState może użyć permalinka do nawigacji przed załadowaniem kodu. Własny onSubmit z preventDefault odbiera przeglądarce natywną ścieżkę i uzależnia formularz od JavaScriptu.

RSC nie naprawiają waterfalli: pobieraj dane równolegle i świadomie je cache'uj

Server Component eliminuje klientowy useEffect, ale nie skraca automatycznie czasu odpowiedzi serwera. Jeśli jedna funkcja czeka na drugą, użytkownik nadal czeka na sumę obu opóźnień. Niezależne dane uruchamiaj równolegle, a zależne pobieraj dopiero po otrzymaniu potrzebnego identyfikatora:

Code
export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
 
  const [product, relatedProducts] = await Promise.all([
    getProduct(slug),
    getRelatedProducts(slug),
  ])
 
  return <ProductDetails product={product} relatedProducts={relatedProducts} />
}

Druga decyzja to cache. Dane publiczne, które zmieniają się rzadko, warto cache'ować i unieważniać po mutacji. Dane zależne od sesji, koszyka czy uprawnień muszą pozostać per żądanie. W Next.js 16 Cache Components trzeba włączyć przez cacheComponents: true; dyrektywa 'use cache' może objąć funkcję pobierającą dane albo cały komponent. Nie dodawaj cache'u na ślepo: błędny klucz lub zbyt długie życie wpisu potrafi pokazać cenę, dostępność albo dane użytkownika w nieaktualnej wersji.

W praktyce optymalizację oceniaj w tej kolejności: najpierw usuń sekwencyjne oczekiwanie, potem wybierz cache dla danych publicznych, a wolne fragmenty odizoluj przez <Suspense>. RSC są fundamentem tego układu, nie zamiennikiem dla dobrej strategii danych.

Streaming i Suspense w Next.js: szybszy feedback dla użytkownika

Next.js z RSC wspiera streaming HTML. Serwer wysyła gotowe fragmenty strony w miarę ich renderowania, zamiast czekać aż cała strona będzie gotowa. W połączeniu z React Suspense pozwala to na natychmiastowe wyświetlenie szkieletu strony z progresywnym dociąganiem treści:

Code
import { Suspense } from 'react'
 
export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
 
  return (
    <main>
      {/* Główna sekcja poza granicą Suspense */}
      <ProductHero slug={slug} />
 
      {/* Sekcja asynchroniczna wysyłana później */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews slug={slug} />
      </Suspense>
 
      <Suspense fallback={<RecommendationsSkeleton />}>
        <RelatedProducts slug={slug} />
      </Suspense>
    </main>
  )
}

Streaming pozwala wcześniej wysłać gotową część dokumentu, ale nie gwarantuje niskiego LCP. ProductHero w przykładzie może zablokować początek odpowiedzi, jeśli sam długo pobiera dane. Element LCP umieszczony w późno rozwiązywanym Suspense pojawi się dopiero wraz z odpowiednim fragmentem strumienia. Mierz TTFB, czas dotarcia elementu LCP i kompletność odpowiedzi po zakończeniu streamingu.

Bot, który odczytuje pełną odpowiedź, może zobaczyć fragmenty wysłane później. Next.js w przypadku botów wymagających metadanych w <head> wyłącza streaming metadanych i czeka na generateMetadata. Listę takich botów można zmienić przez htmlLimitedBots, ale nie konfiguruj jej bez testu wpływu na czas odpowiedzi.

Wariantem tego mechanizmu jest plik loading.tsx, czyli granica Suspense na poziomie segmentu trasy. Przy nawigacji Next.js może wcześniej pokazać stan ładowania, a właściwa strona dociera strumieniem. Ręczny <Suspense> daje precyzyjniejszą kontrolę nad sekcjami. Fallback powinien rezerwować docelowe wymiary. Szkielet o innej wysokości niż treść wygeneruje przesunięcie układu i pogorszy CLS.

Partial Prerendering w Next.js: hybryda statycznego i dynamicznego renderowania

Partial Prerendering (PPR) łączy statyczny HTML z dynamicznymi komponentami w jednym żądaniu HTTP. Statyczna powłoka strony, na przykład nawigacja, stopka i układ, może zostać wysłana wcześniej. Personalizacja oraz bieżące dane docierają przez streaming.

To bardzo naturalny kierunek dla stron marketingowych:

  • Katalog łączy powłokę z aktualną dostępnością oraz ceną pobieraną dynamicznie.
  • Landing zachowuje statyczną główną treść, a personalizację umieszcza w osobnym fragmencie.
  • Artykuł pozostaje w statycznej powłoce, natomiast komentarze docierają przez streaming.

Server Component czy Client Component: strategia wyboru

Najpierw zdecyduj, co zostaje Server Componentem (domyślne), a co trafia do Client Componentu (oznaczonego 'use client'). Zasada ogólna:

Server Component stosuj domyślnie do:

  • Treści statycznych (nagłówki, akapity, listy, karty artykułów)

  • Pobierania danych (fetch z bazy, CMS, API)
  • Renderowania markdown/MDX
  • Komponentów, które nie wymagają interaktywności
  • SEO-critical content (meta tagi, structured data, treść indeksowalna)

Client Component wydzielaj wyłącznie do:

  • Interaktywnych formularzy z walidacją podczas wpisywania

  • Komponentów ze stanem, takich jak karty, akordeony, modale i listy rozwijane

  • Komponentów korzystających z Browser API (localStorage, geolocation, clipboard)

  • Animacji reagujących na interakcje użytkownika
  • Zewnętrznych widgetów, takich jak mapy, czaty i osadzenia

Interakcja wyznacza granicę kodu klienta. Jeśli komponent nie potrzebuje stanu, efektu, własnego hooka ani API przeglądarki, pozostaw go po stronie serwera. Sama animacja CSS, link lub natywny formularz nie wymagają dyrektywy 'use client'.

Granica server/client w praktyce: kompozycja i serializacja

Dyrektywa 'use client' działa na zasadzie propagacji, czyli każdy komponent importowany przez komponent kliencki automatycznie staje się częścią bundle'a przesyłanego do przeglądarki. Nawet jeśli importujesz wyłącznie statyczną treść, całość poddrzewa importów traci status serwerowy. Nierozważne umieszczenie tej dyrektywy w layoutcie może przez to nieumyślnie przekształcić architekturę server-first w klasyczne SPA.

Rozwiązaniem jest kompozycja. Zamiast importować treść serwerową bezpośrednio do komponentu klienckiego, przekazuj ją jako children:

Code
// Import wciąga ArticleContent do grafu modułów klienta.
'use client'
import { useState } from 'react'
import { ArticleContent } from './ArticleContent'
 
export function Accordion() {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Szczegóły</button>
      {open && <ArticleContent />}
    </div>
  )
}
Code
// Treść przychodzi z serwera jako children. JS obsługuje tylko accordion.
'use client'
import { useState } from 'react'
 
export function Accordion({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Szczegóły</button>
      {open && children}
    </div>
  )
}
Code
// Strona (Server Component) składa jedno z drugim
<Accordion>
  <ArticleContent /> {/* renderuje się na serwerze */}
</Accordion>

Interaktywne moduły trzymaj przy liściach drzewa, czyli w małych obszarach stanu, do których serwer przekazuje gotową treść. 'use client' wyznacza granicę grafu modułów. Komponent serwerowy przekazany jako children nie staje się z tego powodu modułem klienta, ponieważ rodzic serwerowy wyrenderował go wcześniej.

Propsy muszą pozostać serializowalne przez React. Nie przekazuj do Client Componentu całego rekordu użytkownika tylko dlatego, że mechanizm serializacji go zaakceptuje. Utwórz obiekt DTO zawierający wyłącznie pola potrzebne w interfejsie. Moduły z bazą, tokenami i logiką uprawnień oznacz przez server-only. React Taint API może stanowić dodatkową barierę przed przypadkowym przekazaniem wartości, lecz podstawą nadal jest świadoma selekcja danych.

Funkcji i instancji klas nie przekazuj przez zwykłe propsy. Server Actions są obsługiwanym wyjątkiem, bo React serializuje referencję potrzebną do wywołania funkcji na serwerze. Wpływ granicy na wagę kodu sprawdzisz analizą bundle'a, opisaną w artykule o analizie paczki JS w Next.js.

Wpływ React Server Components i Server Actions na SEO

Server-first daje robotowi dobry punkt wejścia, jeśli publiczna treść rzeczywiście znajduje się w odpowiedzi HTML. Googlebot i tak kieruje strony z kodem 200 do kolejki renderowania, nawet gdy odpowiedź zawiera gotową treść. RSC ograniczają zależność od wykonania klientowego fetcha, ale nie omijają procesu renderowania Google ani nie stanowią osobnego sygnału rankingowego.

Core Web Vitals

RSC mogą ograniczyć pracę głównego wątku, ponieważ kod Server Components nie wchodzi do paczki klienta. Ten zysk poprawia warunki dla INP. LCP zależy również od TTFB, priorytetu obrazu i położenia granic Suspense, a CLS od zarezerwowania miejsca. Porównuj dane terenowe przed migracją i po niej, zamiast opierać ocenę na samym wyniku Lighthouse z jednej sesji.

Odczyt treści i linków

Google wykonuje JavaScript, lecz część robotów społecznościowych, wyszukiwarek i narzędzi AI odczytuje przede wszystkim odpowiedź HTTP. Publiczny tekst, linki z href, dane strukturalne i główne metadane powinny być dostępne bez interakcji. Nie ukrywaj podstawowej treści za kliknięciem obsługiwanym wyłącznie w Client Component ani za klientowym zapytaniem uruchamianym po montażu.

Metadane i kody odpowiedzi

Metadata API działa w Server Components i pozwala generować tytuł, opis, canonical oraz Open Graph z tego samego rekordu co strona. Dla dynamicznych tras Next.js może streamować metadane, ale boty wymagające znaczników w <head> otrzymują wariant blokujący. Nadal musisz wywołać notFound() dla brakującego rekordu, ustawić przekierowanie po zmianie adresu i nie zwracać pustej strony z kodem 200.

Audyt SEO po migracji na RSC

  • Pobierz URL przez curl i sprawdź status, canonical, robots, nagłówek H1, główną treść oraz linki bez uruchamiania JavaScriptu.

  • Porównaj zwykłą odpowiedź z wyrenderowanym HTML-em w narzędziu do inspekcji URL. Różnica nie powinna zmieniać sensu strony ani linkowania.

  • Sprawdź JSON-LD po serializacji i nie przekazuj do skryptu surowego tekstu, który może zamknąć znacznik przez sekwencję HTML.

  • Zmierz LCP, INP i CLS w danych terenowych na poziomie typu strony oraz urządzenia. Wynik laboratoryjny służy do diagnozy, a nie kończy audytu.

  • Zweryfikuj 404, 301, błędy backendu i timeouty. Render serwerowy może szybciej dostarczyć również błędną odpowiedź, jeśli obsługa statusów kuleje.

Migracja z Pages Router na App Router przy server-first Next.js

Jeśli masz istniejący projekt Next.js na Pages Router, migracja na App Router (i tym samym na RSC) jest procesem przyrostowym:

  1. Zacznij od nowych stron. Nowe sekcje buduj w App Router (app/ directory), istniejące zostaw w Pages Router (pages/)
  2. Najpierw migruj proste trasy treściowe. Blog, strona o firmie i dokumenty bez interakcji łatwo podzielić na Server Components.
  3. Wydziel małe Client Components. Przenieś do nich stan, efekty i obsługę zdarzeń, pozostawiając treść w rodzicu serwerowym.
  4. Przenieś mutacje interfejsu do Server Actions. Formularze kontaktowe i zmiany konta pasują do akcji, jeśli nie potrzebują publicznego kontraktu API.
  5. Przenieś pobieranie danych na serwer. Zastąp getStaticProps i getServerSideProps asynchronicznymi komponentami oraz jawną strategią cache.

Next.js wspiera oba routery jednocześnie, więc migracja może być rozłożona w czasie bez ryzyka regresji.

Po każdym etapie porównaj HTML, paczkę JavaScript, statusy, metadane i dane terenowe. Oba routery mogą działać w jednym projekcie, lecz granica nawigacji między nimi może powodować pełne przeładowanie strony. Sprawdź też współdzielone style, analitykę i stan sesji.

Jeśli rozwijasz ten temat dalej, zobacz App Router czy Pages Router, co wybrać? i React 19 Actions.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czym są React Server Components?

React Server Components (RSC) to komponenty renderowane na serwerze, a ich kod nie trafia do pakietu JavaScript przeglądarki. Przy pierwszym załadowaniu Next.js używa wyniku RSC oraz Client Components do utworzenia HTML-a. Kolejne nawigacje korzystają głównie z RSC Payload. Nie oznacza to, że cała aplikacja działa bez kodu klienckiego.

Jak Server Components wpływają na SEO?

Publiczna treść może trafić do odpowiedzi HTML bez dodatkowego pobierania w useEffect. Ułatwia to odczyt strony robotom, które nie wykonują JavaScriptu. Googlebot potrafi renderować JavaScript, więc RSC nie są warunkiem indeksacji ani czynnikiem rankingowym. Trzeba nadal sprawdzać status HTTP, metadane, linki, HTML i wyrenderowany wynik.

Czym są Server Actions w Next.js?

Server Actions to asynchroniczne funkcje serwerowe oznaczone dyrektywą use server. Możesz użyć ich do formularzy, mutacji danych i wysyłki maili bez budowania osobnego endpointu API dla każdej prostej akcji. Z natywnym HTML form formularz może działać nawet wtedy, gdy JavaScript klienta jeszcze się nie załadował.

Kiedy używać Server Component, a kiedy Client Component?

Jeśli komponent nie potrzebuje useState, useEffect ani obsługi zdarzeń przeglądarki, zostaw go jako Server Component. Client Component (use client) dawaj tam, gdzie masz realną interakcję: stan, Browser API, animacje po kliknięciu, modale, widgety zewnętrzne. Im mniej niepotrzebnego use client, tym lżejsza strona.

Czy mogę migrować istniejący projekt na Server Components?

Tak, przyrostowo. Next.js wspiera Pages Router i App Router jednocześnie. Zacznij od prostej trasy, wydziel małe Client Components i przenieś pobieranie danych do asynchronicznych Server Components. Server Actions stosuj do mutacji wywoływanych przez interfejs React. Webhooki, publiczne API i integracje zewnętrzne nadal wymagają Route Handlers lub osobnego API.

Czy Server Actions nadają się do pobierania danych?

To mechanizm przeznaczony do mutacji. Wywołanie korzysta z POST, a akcje uruchamiane przez useActionState mogą być kolejkowane sekwencyjnie. Odczyt wykonuj w Server Components przez warstwę danych lub fetch, gdzie można jawnie ustalić równoległość, cache i streaming.

Czy Server Action jest bezpieczna sama z siebie?

Nie. Eksportowana Server Action jest osiągalna przez bezpośrednie żądanie POST i można ją wywołać bez klikania w formularz. Walidację danych wejściowych i sprawdzenie uprawnień rób wewnątrz akcji, tak samo jak w klasycznym endpoincie API. Ukrycie przycisku w UI niczego nie chroni.

Dlaczego moje propsy nie przechodzą do Client Componentu?

Propsy przekraczające granicę serwer i klient muszą być serializowalne: obiekty, tablice, stringi, liczby, daty. Funkcje, instancje klas czy klienty bazy danych nie przejdą, a jedynym wyjątkiem są Server Actions, które można przekazywać jako propsy. Jeśli komponent kliencki potrzebuje logiki z serwera, przekaż wynik jej działania albo gotowy JSX przez children, nie samą funkcję.

Ile JavaScriptu realnie oszczędzam z RSC?

Zależy od architektury i granic między komponentami serwerowymi a klienckimi. W serwisach opartych o treść oszczędność bywa znacząca, bo logika renderowania całej treści nie trafia do klienta. Konkretny wynik zawsze potwierdź bundle analyzem i pomiarem Core Web Vitals.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Kiedy Next.js daje przewagę w SEO nad React SPA

Next.js to framework React, który ułatwia uniknięcie głównego ograniczenia aplikacji SPA renderowanych wyłącznie w przeglądarce: kluczowa treść może znaleźć się już w początkowym HTML. Google nie musi wtedy czekać na wykonanie JavaScriptu, żeby ją odkryć. Framework dodaje też Metadata API, optymalizację obrazów i podział kodu, ale korzyść dla Core Web Vitals zależy od sposobu implementacji, ilości kodu klienckiego i infrastruktury.

Maciej Sala

Maciej Sala

Founder StriveLab

Astro vs Next.js w 2026: porównanie frameworków

Astro czy Next.js? Wybór frameworka musi być dokładnie przemyślany, zanim pojawi się pierwszy commit. Jeśli stoisz przed takim właśnie wyborem, w tym artykule staram się wykazać, w jakich obszarach najlepiej sprawdza się Astro , a w jakich będzie dominował Next.js .

Maciej Sala

Maciej Sala

Founder StriveLab

Jak przyspieszyć Next.js za pomocą Streamingu i Suspense?

Jedno wolne zapytanie do bazy potrafi zatrzymać całą stronę, jeśli nie wydzielisz go odpowiednią granicą. Bez streamingu serwer czeka na dane potrzebne do pełnego HTML-u i dopiero wtedy kończy odpowiedź. Streaming pozwala wcześniej pokazać gotową część oraz fallbacki, a wolniejsze fragmenty dosłać później. Nie przyspiesza samego zapytania, ale poprawia sposób dostarczania i odczuwania strony.

Maciej Sala

Maciej Sala

Founder StriveLab