Hoppa till innehållet
Kontakta oss

React Server Components i praktiken: laddtider, SEO och utvecklarupplevelse

Genomgång av React Server Components: bundle-storlekar, streaming, "use client"-gränser och DX-friktion i praktiken.

Den 5 december 2024 släppte React-teamet version 19 som stabil release. Det var kulmen på fyra års arbete med en idé som först presenterades i en RFC i december 2020: React Server Components. Med React 19 blev RSC en stabil del av React. I Next.js App Router, som i dag är den mest använda implementationen av RSC, hade det redan varit standard sedan version 13.4.

Artikeln går igenom vad RSC innebär när man bygger riktiga applikationer: vad som fungerar bra, vad som skaver och var gränsen går mellan löfte och verklighet.

Bakgrund: Från RFC till default

Tidslinjen är värd att ha i huvudet. I december 2020 publicerade React-teamet sin RFC för Server Components. Idén var radikal: React-komponenter som bara körs på servern och aldrig skickar JavaScript till klienten. Ingen hydration, ingen bundle-kostnad för komponentlogiken.

Det tog nästan två år innan någon kunde testa det på riktigt. I oktober 2022 lanserade Next.js 13 sin App Router i beta, den första implementationen av RSC som gick att bygga applikationer med. Under ett år levde App Router som beta, med frekventa breaking changes och en dokumentation som inte hängde med.

I maj 2023, med Next.js 13.4, blev App Router stabil. Sedan, i oktober 2023, kom Next.js 14 som stabiliserade Server Actions, möjligheten att hantera formulär och mutationer utan separata API-routes. Och drygt ett år senare kom React 19 och cementerade hela arkitekturen som Reacts officiella riktning.

Det tog fyra år från RFC till stabil release.

Vad RSC ändrar

En vanlig missuppfattning är att RSC är samma sak som SSR. Skillnaden är avgörande för att förstå vad RSC ger.

Server-Side Rendering (SSR) har funnits länge. Servern renderar komponenter till HTML och skickar det till webbläsaren. Men sedan skickas också all JavaScript: varje komponent, varje hook, varje eventhanterare. Webbläsaren laddar ner allt, parsar det, och "hydrerar" den renderade HTML:en så att den blir interaktiv. Resultatet: snabb first paint, men fortfarande stora bundles.

RSC fungerar annorlunda. En Server Component körs på servern och resultatet skickas som en serialiserad representation av komponentträdet, inte som JavaScript. Webbläsaren tar emot det och renderar det, men det finns ingen JavaScript att ladda ner och ingen hydration att göra för de delarna. Komponentkoden stannar på servern.

Det innebär att en Server Component kan importera tunga bibliotek som en markdown-parser, en syntax-highlighter eller ett bibliotek för datumformatering, och klienten ser aldrig den koden. Den betalar inte kostnaden. Servern gör jobbet, skickar resultatet, och klienten renderar det som om det vore vanlig HTML med lite metadata.

Med SSR skickas fortfarande all JavaScript, komponenterna renderas bara även på servern. Med RSC skickas JavaScript enbart för de komponenter som behöver vara interaktiva på klienten.

Konkreta vinster

Bundle size

Det här är RSC:s mest påtagliga vinst. Hur mycket klientens JavaScript minskar beror på hur stor del av sidan som är ren presentation, och för innehållstunga sidor kan skillnaden bli stor.

Ett tänkt exempel: en bloggsida som med Pages Router skickar 180 KB JavaScript till klienten. Sidan hade en markdown-renderer, syntax highlighting för kodblock, en datumformaterings-funktion och lite layoutlogik. Inget av det behöver vara interaktivt. Det är ren presentation.

Med Server Components kan samma sida landa på runt 60 KB. Markdown-parsern, syntax highlightern och datumformateringen körs på servern. Klienten får bara den renderade outputen plus det minimala React-runtime som behövs för att hantera navigering.

Det märks mest på svagare enheter. En billig mobiltelefon parsar 180 KB JavaScript betydligt långsammare än en MacBook Pro.

TTFB och streaming

Streaming med Suspense är den andra stora vinsten, och den är mer subtil. I en traditionell SSR-setup väntar servern tills hela sidan är renderad innan den skickar något till webbläsaren. Har du en databasquery som tar 200ms och ett API-anrop som tar 400ms? Webbläsaren väntar minst 400ms innan den ser en enda byte.

Med RSC och Suspense kan servern börja skicka HTML direkt. Layouten, navigeringen, statiskt innehåll, allt som inte behöver vänta på data skickas omedelbart. Sedan, när databasqueryn är klar, streamas den delen in. Och när API-anropet svarar streamas det in också.

I praktiken innebär det att TTFB (Time To First Byte) kan vara under 50ms även för sidor med tunga datahämtningar. Webbläsaren börjar rendera omedelbart. Användaren ser innehåll medan resten laddar. Det är en markant bättre upplevelse än att stirra på en vit sida i 400ms.

En viktig detalj: streaming kräver att man sätter ut Suspense-gränser i sin kod. Utan dem faller man tillbaka till det gamla beteendet, där servern väntar tills allt är klart. Det är inte automatiskt, och det är ett mönster man måste lära sig.

SEO

RSC:s SEO-vinst är enkel att förklara: servern skickar fullständig HTML i sitt första svar. Inga skelettsidor, inget JavaScript som måste köras innan innehållet syns.

Google har blivit bättre på att indexera JavaScript-renderade sidor, men "bättre" betyder inte "perfekt". Det finns fortfarande fördröjningar i indexering av JS-beroende innehåll. Med RSC är det inte längre ett problem. Crawlers ser samma sak som användare, direkt.

Det gäller också för sociala delningar. Open Graph-taggar, meta-beskrivningar, strukturerad data, allt finns i den initiala HTML-responsen. Inga extra steg, inget som kan gå fel.

Direkt dataåtkomst

En mer underskattad vinst: Server Components kan kommunicera direkt med databaser och interna tjänster. I en Pages Router-setup med getServerSideProps fanns redan server-exekvering, men logiken var separerad från komponenterna. Man hämtade data i en funktion och skickade den som props. Med RSC sker datahämtningen i själva komponenten.

Det kanske låter som en liten skillnad, men i praktiken förändrar det hur man organiserar kod. Istället för att ha en datahämtningsfil, en transformeringsfil, och en renderingsfil har man en komponent som gör allt. Hämtar data, transformerar den, renderar resultatet. Kolokering av data och presentation.

Säkerhetsvinsten är också konkret. Databas-credentials, API-nycklar och interna endpoints exponeras aldrig för klienten. Inte ens som miljövariabler som råkar hamna i bundlen (ett vanligt misstag med NEXT_PUBLIC_-prefixet i Next.js). Server Components har tillgång till process.env direkt, och den koden lämnar aldrig servern.

Samtidigt blir servern en del av attackytan. Den 3 december 2025 publicerade React-teamet en kritisk sårbarhet i React Server Components (CVE-2025-55182) som gjorde det möjligt att köra kod på servern utan inloggning. React och ramverket behöver därför hållas uppdaterade.

DX-friktionen

Nu till den andra sidan. RSC har verkliga vinster, men det har också verklig friktion. Och den friktionen sitter djupare än man kanske tror.

Den nya mentala modellen

Den största utmaningen med RSC är konceptuell. I klassisk React är alla komponenter likadana. De har state, de kan ha effekter, de renderas i webbläsaren. Man behöver inte tänka på var koden körs.

Med RSC måste man plötsligt tänka i två världar. Varje komponent har en implicit fråga: "Ska den här köras på servern eller klienten?" Svaret påverkar vad man kan göra i komponenten, vilka props den kan ta emot, och hur den interagerar med andra komponenter.

Det låter enkelt i teorin. I praktiken, mitt i en feature-implementation, är det förvånansvärt lätt att glömma. Man sitter och skriver en komponent, inser att man behöver useState, och måste backa och tänka igenom hela komponenthierarkin.

Hooks försvinner på servern

I Server Components kan man inte använda useState, useEffect, useRef, useContext eller någon annan hook som förlitar sig på klientens livscykel. Det är logiskt: servern har inget state som persisterar mellan renders, inga DOM-noder att referera till, ingen effekt-cykel.

Men det innebär att varje gång man vill lägga till interaktivitet (en dropdown, en modal, ett formulär med validering, en hover-effekt) måste man bryta ut det till en Client Component. Ibland är det trivialt. Ibland innebär det att man måste strukturera om hela sitt komponentträd.

Bibliotek och context

React Context fungerar inte i Server Components. Det är ett problem eftersom en stor del av React-ekosystemet bygger på context: tema-providers, auth-providers, state management-bibliotek, animationsbibliotek.

Det innebär att man inte kan wrappa en Server Component i en context provider och förvänta sig att det fungerar. Man måste tänka om. Antingen lyfter man context-providers till Client Components högre upp i trädet och låter Server Components vara barn som inte konsumerar context, eller så hittar man alternativa mönster.

Ekosystemet håller på att anpassa sig. Komponentbibliotek lägger till RSC-stöd steg för steg. Men under tiden stöter man regelbundet på bibliotek som helt enkelt inte fungerar i Server Components. Det beror på att hela den mentala modellen har ändrats, och ekosystemet behöver tid att hinna ikapp.

Felmeddelanden och debugging

Felmeddelanden vid "use client"-överträdelser är ibland kryptiska. Man får ett error som säger att en viss funktion inte kan serialiseras, men det är inte uppenbart vilken komponent som orsakar problemet eller varför.

Debugging är också svårare. Server Components visas i React DevTools, men deras state och kod går inte att inspektera på klienten. Fel får man felsöka i serverloggen, och den feedback-loopen är långsammare och mindre intuitiv än att inspektera komponenter direkt i webbläsaren.

Det här förbättras successivt. React-teamet arbetar på bättre felmeddelanden och verktyg. Men i dagsläget är det en reell kostnad i utvecklingstid, särskilt för nya team som inte har byggt upp en intuition för var problem brukar uppstå.

"use client"-gränsen

"use client"-direktivet styr var gränsen mellan server- och klientkod går.

I App Router är alla komponenter Server Components som standard. Man behöver inte göra något aktivt för att en komponent ska köras på servern, det är default. Det är först när man lägger till "use client" högst upp i en fil som den komponenten blir en Client Component.

Men här är haken: "use client" markerar inte bara den enskilda komponenten. Det markerar en gräns i modulernas beroendeträd. Allt som den filen importerar blir också klientkod. Om komponenten importerar tio underkomponenter blir alla tio Client Components, oavsett om de behöver vara det eller inte. Server Components som skickas in som children påverkas däremot inte.

Det skapar ett mönster som är avgörande att förstå: gränsen mellan server och klient bör vara så djupt ner i komponentträdet som möjligt. Interaktivitet (knappar, formulär, dropdowns, animationer) ska leva i löv-komponenter nära botten av trädet. Datahämtning, layout och presentation ska leva i Server Components högre upp.

I praktiken ser det ut så här: en sida har en Server Component som hämtar data från databasen, en Server Component som renderar layouten, och små Client Components för interaktiva element: en "Läs mer"-knapp som togglar text, en mobilmeny som öppnar och stänger sig, ett formulär med klientvalidering.

Det bästa mönstret är att tänka på "use client" som en kostnad. Varje gång man sätter dit direktivet betalar man med JavaScript som skickas till klienten. Målet är att minimera den kostnaden genom att hålla gränsen så nära interaktiviteten som möjligt.

Vanliga misstag

Några misstag är vanliga.

Överanvändning av "use client"

Det vanligaste misstaget. Man stöter på ett fel ("useState is not allowed in Server Components") och lägger reflexmässigt till "use client" i filen. Problemet löst, vidare till nästa.

Men gör man det tillräckligt många gånger har man i praktiken byggt en vanlig klient-renderad React-app med extra steg. Hela poängen med RSC (reducerade bundles, ingen onödig hydration) försvinner.

Lösningen är att stanna upp och fråga: "Behöver hela den här komponenten vara en Client Component, eller kan jag bryta ut den interaktiva delen?" Ofta kan man extrahera en liten Client Component, kanske bara en knapp med en onClick-handler, och låta resten vara en Server Component.

Prop-serialisering

Allt som passeras som props från en Server Component till en Client Component måste vara serialiserbart. Det innebär: strängar, nummer, booleans, arrayer, objekt. Inga funktioner, inga klasser. (Date-objekt, Map och Set stöds däremot nativt i React 19:s RSC-serialiseringsprotokoll.)

Det här biter folk regelbundet. Man skickar en callback-funktion som prop till en Client Component och får ett kryptiskt serialiseringsfel. Lösningen är att flytta logiken: antingen definiera funktionen i Client Component, eller använd Server Actions för server-logik som triggas från klienten.

Server Actions löser mycket av det här. Istället för att skicka en funktion som prop kan man definiera en Server Action (en async-funktion markerad med "use server") och anropa den direkt från Client Components. Formulär kan ha en Server Action som action-prop, och den exekveras på servern utan att man behöver bygga ett API.

Waterfall-requests i nästade Server Components

Det här är ett subtilt men allvarligt prestandaproblem. Tänk dig en sidkomponent som hämtar användardata. Inuti den finns en komponent som hämtar användarens ordrar. Inuti den finns en komponent som hämtar orderdetaljer. Varje komponent awaitar sin datahämtning.

Resultatet: sekventiella requests. Ordrar kan inte hämtas förrän användardata är klar. Orderdetaljer kan inte hämtas förrän ordrar är klara. Tre requests som kunde köras parallellt körs istället i serie.

Det är inget nytt problem, waterfall-requests har alltid varit en utmaning. Men RSC gör det lättare att hamna i fällan, eftersom datahämtning sker direkt i komponenten med async/await. Det ser rent ut och känns naturligt, men utan medveten parallellisering betalar man med väntetid.

Lösningen har flera delar. Parallella data-fetches med Promise.all där det är möjligt. Suspense-gränser som låter oberoende delar streamas in separat. Och ibland: att lyfta datahämtning högre i trädet och skicka data nedåt som props.

Saknade Suspense-gränser

Streaming fungerar bara om det finns Suspense-gränser att streama kring. Utan dem renderar servern hela sidan innan den skickar något.

Man ska inte sätta Suspense runt allting, det skapar onödig komplexitet och potentiellt flimmer i UI:t. Men runt varje del av sidan som har en egen, oberoende datahämtning bör det finnas en Suspense-gräns med en relevant fallback. Det ger streaming möjlighet att leverera snabba delar direkt och långsamma delar när de är klara.

Vad som gäller framåt

RSC är inte färdigt. Det är en arkitektur i mognadsfas, och det finns fortfarande kantigheter som behöver slipas.

Ekosystemet anpassar sig gradvis. Komponentbibliotek som Radix, Headless UI och shadcn/ui har lagt till eller arbetar på RSC-kompatibilitet. State management-bibliotek utforskar nya mönster som fungerar med server-klient-gränsen. Animationsbibliotek lär sig var i trädet de behöver leva.

React-teamet fortsätter utveckla verktygen. Bättre felmeddelanden, bättre DevTools-integration, och nya primitiver som gör det enklare att arbeta med server-klient-gränsen. Det har tagit tid, men trenden pekar åt rätt håll.

En observation som är värd att nämna: RSC är i praktiken ett Next.js-fenomen just nu. Andra ramverk experimenterar (React Router har RSC-stöd i preview, och Waku är ett experimentellt ramverk byggt kring RSC), men för produktionsanvändning är det Next.js App Router som gäller. Det är både en styrka (fokuserad implementation) och en svaghet (ett enda ramverks beslut styr hela ekosystemet).

Praktisk vägledning

För den som står inför beslutet att använda RSC eller inte, här är en sammanfattning av var det landar efter de första riktiga produktionsåren:

Använd RSC när applikationen har mycket innehåll som inte behöver vara interaktivt. Bloggar, dokumentationssajter, e-handel med produktlistor, dashboards med mycket data och lite interaktion. I de fallen är bundle-reduktionen och streaming-fördelarna substantiella och konkreta.

Var försiktig med RSC när applikationen är primärt interaktiv. En real-time chat, en komplex editor, ett spelliknande UI. Då är majoriteten av komponenterna Client Components ändå, och RSC-arkitekturen skapar overhead utan proportionerlig vinst.

Oavsett situation: lär dig "use client"-gränsen ordentligt innan du börjar bygga. Det är det enskilt viktigaste konceptet. Förstår man det, faller resten på plats. Förstår man det inte, kommer man att kämpa med kryptiska fel och en kodstruktur som gradvis tappar alla RSC-fördelar.

Om du migrerar från Pages Router: gör det stegvis. Next.js stöder båda routrarna parallellt. Börja med nya sidor i App Router och migrera befintliga sidor en i taget. Mät bundle-storlek och TTFB före och efter varje migrering. Flytta inte allt på en gång, det är en säker väg till frustration och regressioner.

Om du börjar ett nytt projekt: App Router med RSC som default är rätt val för de flesta webbapplikationer 2025. Kostnaden är inlärningskurvan. Vinsten är en arkitektur som skalar bättre och levererar snabbare sidor. Det är en rimlig trade-off för de flesta team.

RSC är ingen universallösning, men det är en genomgripande förändring i hur React tänker kring rendering.

Källor

Ämnen: React Server Components, RSC, Next.js, App Router, Server Actions