Ak ste posledné dva roky nesledovali React zblízka, jedna vec sa vám zmenila pod rukami. V Next.js App Routeri je každý komponent predvolene serverový. Nie klientsky s možnosťou renderovať ho na serveri, ale serverový, kým výslovne nepoviete inak.
Väčšina článkov na túto tému začne vysvetľovaním, čo Server Components sú. Tento nie. Tento je o rozhodovaní: podľa čoho sa v konkrétnom komponente rozhodnúť pre server alebo pre klienta, čo presne sa pokazí, keď to trafíte zle, a čo z toho má firma, ktorá si web objednáva a React ju nezaujíma. Príklady sú z kódu tohto webu, takže si ich viete otvoriť a overiť.
Čo sa reálne zmenilo
Starý model bol jednoduchý a všetci sme ho poznali. Prehliadač stiahol JavaScript, React sa naštartoval, komponent sa vykreslil prázdny, v useEffect si zavolal dáta a po ich príchode sa prekreslil znova. Medzitým používateľ pozeral na spinner.
Ten model mal tri náklady, ktoré sme dlho brali ako fakt života:
- Všetok kód, ktorý dáta načítaval a spracovával, musel ísť do prehliadača. Vrátane knižníc na parsovanie, formátovanie a validáciu.
- Dáta prišli až po JavaScripte. Najprv bundle, potom request, potom obsah. Tri kroky za sebou, nie vedľa seba.
- Google aj používateľ videli najprv prázdno.
Nový mentálny model je posunutie hranice. Namiesto otázky "ako dostanem dáta do komponentu" sa pýtate "kde má tento komponent vôbec bežať". Server Component sa vykreslí na serveri, do prehliadača pošle hotový výsledok a svoj vlastný kód neposiela vôbec. Nie zmenšený, nie odložený. Vôbec.
To je celý rozdiel. Nie rýchlejší JavaScript, ale žiadny JavaScript.
Hranica sa vyhlasuje direktívou 'use client' na začiatku súboru. Všetko nad ňou beží na serveri, všetko pod ňou a všetko, čo si takýto súbor naimportuje, ide do prehliadača. Direktíva nie je vlastnosť komponentu, ale hranica stromu, a to je detail, na ktorom sa najčastejšie chybuje.
Konkrétny príklad: výpis článkov na tomto webe
Zoberme stránku, na ktorej práve ste. Výpis článkov v sekcii Z praxe číta deväť MDX súborov z disku, parsuje ich frontmatter, počíta čas čítania a vykresľuje karty s filtrom podľa kategórie.
Takto by to vyzeralo starým spôsobom:
'use client'
import { useEffect, useState } from 'react'
export function BlogIndex() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then((r) => r.json())
.then((data) => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <Spinner />
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>{post.title}</li>
))}
</ul>
)
}
Funguje to. Ale znamená to API endpoint, ktorý treba postaviť a udržiavať, stav pre načítavanie, spinner, prázdny prvý render a request navyše po naštartovaní Reactu.
Takto to na tomto webe vyzerá dnes:
// app/(sk)/blog/page.tsx
// Žiadne 'use client'. Tento súbor sa v prehliadači nikdy neocitne.
import { getAllPosts, getCategories } from '@/lib/blog'
import { BlogIndexPage } from './page-client'
export default function Page() {
const posts = getAllPosts()
// Telo článku sa zahadzuje tu. Do prehliadača ide len to,
// čo karta reálne potrebuje vykresliť.
const cards = posts.map(({ slug, title, description, publishDate, category, readTime }) => ({
slug, title, description, publishDate, category, readTime,
}))
return <BlogIndexPage posts={cards} categories={getCategories(posts)} locale="sk" />
}
Žiadny useEffect, žiadny loading, žiadny API endpoint. getAllPosts siaha priamo na súborový systém cez fs a gray-matter, čo je v prehliadači nemysliteľné, ale na serveri je to obyčajné čítanie súboru.
Podstatný je ten komentár uprostred. Deväť článkov má na disku okolo 68 kB zdrojového MDX, dokopy takmer desaťtisíc slov. Výpis z toho potrebuje len nadpis, perex, dátum, kategóriu a čas čítania. Server telá zahodí a cez hranicu pošle iba metadáta kariet. Pri klientskom načítavaní by ste buď poslali všetko a orezali to až v prehliadači, alebo postavili druhý endpoint, ktorý vracia skrátenú verziu. Tu to je jeden map.
Zvyšok, teda filter podľa kategórie a animácie, zostáva klientsky v page-client.tsx. To je celý vzor: serverový obal, klientske ostrovy. Nie "celá stránka je serverová" ani "celá stránka je klientska", ale hranica vedená čo najnižšie.
Praktické pravidlo, ktoré nám drží: 'use client' patrí na najmenší možný komponent, nie na stránku. Keď ho dáte na stránku, stiahnete pod klientsku hranicu aj všetko, čo tá stránka importuje, vrátane vecí, ktoré interaktivitu nikdy nepotrebovali.
Kedy Server Component nepoužiť
Server nie je predvolená odpoveď na všetko. Sú miesta, kde klientsky komponent vyhráva, a snaha vyhnúť sa mu je strata času.
Interaktivita so stavom. Čokoľvek s useState, useReducer alebo obsluhou udalostí. Filter kategórií na výpise článkov, otváranie a zatváranie FAQ, prepínač jazyka. Tieto veci reagujú na klik a klik sa deje v prehliadači.
Prehliadačové API. window, localStorage, matchMedia, IntersectionObserver. Na serveri neexistujú. Náš hook useMobile stojí na matchMedia, takže všetko, čo ho používa, je nutne klientske.
Animácie viazané na scroll. Celá scrollovacia architektúra tohto webu, teda Framer Motion a useScroll, je klientska a inak byť nemôže. Server nevie, kde na stránke sa nachádzate.
Efekty po vykreslení. Čokoľvek, čo musí bežať až po tom, ako je prvok v DOM.
Ako to vyzerá, keď to trafíte zle? Dva typické scenáre. Prvý je hlučný: dáte 'use client' na komponent, ktorý číta zo súborového systému, a build spadne, lebo fs v prehliadači neexistuje. To je dobrá chyba, všimnete si ju hneď.
Druhý je tichý a preto horší. Zabudnete 'use client' na komponent s onClick, alebo naopak dáte direktívu priveľmi vysoko. V prvom prípade sa stránka vykreslí, tlačidlo je vidieť a nič nerobí. V druhom sa nič nepokazí, iba vám do bundlu potichu pribudne kód, ktorý tam nemá čo robiť. Nikto sa nesťažuje, len je web pomalší a nikto nevie prečo.
Preto sa oplatí pozerať na veľkosť bundlu po každom builde, nie raz za pol roka. Na tomto webe je zdieľaný základ 115 kB a výpis článkov sa dostane na 192 kB. Keď niektoré číslo skočí, vieme to spojiť s konkrétnou zmenou, kým je čerstvá.
Čo to znamená pre klientske projekty
Toto je časť, ktorá zaujíma firmu, nie vývojára.
Rýchlejší prvý obsah. Používateľ nečaká na JavaScript, aby uvidel text. HTML príde hotové. Na mobilnom pripojení, kde stiahnutie a spracovanie bundlu trvá sekundy, je to rozdiel medzi "web sa načítal" a "web sa načítava".
Menej kódu na stiahnutie. Každá knižnica, ktorá zostane na serveri, je kód, ktorý zákazník neplatí dátami ani časom. Najlepšia ilustrácia je z nášho 3D zážitku: pre návštevníkov s vypnutými animáciami sa scéna vôbec nenačítava, čo im ušetrí 1461 kB JavaScriptu. Rovnaký princíp, iná mierka.
SEO bez kompromisov. Google síce JavaScript spracuje, ale s oneskorením a nie vždy spoľahlivo. Obsah, ktorý je v HTML od prvej odpovede, tento problém nemá. Celý tento web je staticky vygenerovaný, takže robot dostane hotovú stránku.
Merateľne to sedí. Pre Adrilex sme tento prístup priviedli k Lighthouse skóre 100 vo výkone a 94 a viac v ostatných kategóriách. Nie preto, že by sme optimalizovali obrázky do poslednej kilobajtu, ale preto, že veľká časť práce sa nikdy nedostala do prehliadača.
Za tým všetkým je jedna vec, ktorú si treba odniesť. Server Components neznamenajú, že máte prepísať web. Znamenajú, že predvolená odpoveď na otázku "kde má tento kód bežať" sa zmenila zo servera na klienta na presný opak, a že klientsky kód je teraz rozhodnutie, ktoré treba vedieť obhájiť.
Ak vás zaujíma, čo z toho má váš konkrétny web, pozrite si našu tvorbu webov alebo portfólio. A ak máte web, ktorý sa načítava pomaly a nikto vám nevie povedať prečo, ozvite sa. Pozrieme sa, koľko JavaScriptu reálne posiela a koľko z toho tam nemusí byť. Niekedy je odpoveď, že je to v poriadku a problém je inde. Aj to je užitočné vedieť.
