Skip to main content

Command Palette

Search for a command to run...

RSC in 2026: The End of the SPA Era and What Comes Next

Updated
21 min read

TL;DR: React Server Components are the default in Next.js 15+ and React 19. They can slash JavaScript bundles by 40–75% and cut LCP by up to 67% — but only if you architect them correctly. 67% of developers who use RSC report negative sentiment. Not because the technology is bad. Because the mental model hasn't clicked yet. This post fixes that.

📅 Published: May 2026 | ⏱ Reading Time: 18 min | 🎯 Level: Intermediate–Advanced React Developers


React Server Components — the shift from client-first to server-first rendering

Your component tree now spans two environments. Only one of them ships JavaScript.


The Number That Should Alarm Every React Developer

The State of React 2025 survey (3,760 developers, Nov 2025–Jan 2026) dropped this bombshell:

45% of new React projects use Server Components. Of those developers, 67% report negative sentiment.

The survey authors called this "troubling for a set of new APIs that was supposed to pave the way towards React's next big evolution."

But here's the interpretation the headlines missed: this is an education crisis, not a technology crisis.

Shopify's Hydrogen 2 cut per-page JavaScript from 340KB to 89KB with RSC. Meta reported 78% bundle reduction for data-display components. DoorDash saw 65% LCP improvement. These aren't cherry-picked demos — they're production migrations at scale.

The 67% dissatisfaction comes from developers who tried RSC without the right mental model, got confused by Context incompatibility, accidentally marked a root layout 'use client', and watched their 2.1MB bundle stay at 2.1MB.

This post gives you the mental model. The real numbers. The code that works. And the honest assessment of when RSC is the wrong answer.


A Decade of SPA: What We Built and What It Cost Us

The SPA era gave us extraordinary things: instant navigation, rich interactivity, component-driven development. But it came with a structural tax that kept compounding:

The median React app in 2024 shipped 500KB+ of gzipped JavaScript. Users on mid-range devices in India, Southeast Asia, or anywhere with slower connections experienced multi-second blank screens — for content that was mostly static.

The irony: we were shipping the code for rendering product names and blog posts to every device in the world, then waiting for that code to execute before showing the content.

RSC asks the obvious question: why does a product listing component need to run in the browser at all?


What RSC Actually Is (The Distinction That Changes Everything)

Let's kill the most dangerous misconception first:

RSC is NOT the same as Server-Side Rendering (SSR).

This confusion is responsible for most of the frustration in the survey data.

The critical difference: with SSR, your component code still ships to the client for hydration. React renders on the server, sends HTML, then re-runs the same code in the browser to attach event handlers.

With RSC, server components never reach the client. They render on the server, produce an RSC Payload (a compact binary representation of the component tree), and that's it. No hydration. No JavaScript shipped for those components. Ever.

This is why RSC can eliminate 40–75% of your JavaScript bundle — but only if you're intentional about what gets the 'use client' label.


The Three-Layer Architecture of 2026

This three-layer model is what Partial Pre-rendering (PPR) — stable in Next.js 15, default in Next.js 16 — formalizes. A single route can contain all three layers simultaneously, each optimized independently.


The Real Numbers: Production Case Studies

Real migrations. Not benchmarks.

Bundle Size Reductions

Company / Project Before After Reduction
Shopify Hydrogen 2 340KB 89KB 74%
Meta (Facebook) data-display tree 78%
Vercel Commerce Template 210KB 67KB 68%
OpenMyPro (healthcare, 150K users) 180KB 45KB 75%
E-commerce (50K DAU, 4G mobile) 800KB 320KB 60%
Ecosire.com (~1,500 pages, Next.js 16) 340KB 210KB 38%
HTTP Archive 2025 (App vs Pages Router) 390KB 180KB 54%

Core Web Vitals

Metric CSR Baseline RSC + Streaming RSC + PPR
TTFB 450ms ~50ms ~30ms
LCP 4.1s 1.28s ~1.1s
TTI 5.8s 2.1s 1.8s
FID/INP 180ms 45ms 45ms

Business Impact (Real Data)

  • DoorDash: 65% LCP reduction on home and store pages; Poor URLs (LCP > 4s) dropped 95%

  • Preply: INP 250ms → 175ms, estimated $200K/year in additional conversions

  • OpenMyPro: Lighthouse 78 → 94, contributed to 150K+ users and six-figure ARR

  • E-commerce case study: Bounce rate -23%, conversion rate +8%, session duration +12%

  • Meta Quest Store (React Compiler 1.0): Initial loads 12% faster, interactions 2.5× faster

💡 Critical caveat: These results came from architectural rewrites, not surface-level migrations. The common thread: restructure data fetching to be server-side AND keep client components minimal. RSC alone, without restructured data fetching, shows negligible improvement.


The Mental Model That Makes RSC Click

One rule unlocks everything:

"Server Component by default. Add 'use client' only when you need the browser."

You need 'use client' for exactly three things:

  1. StateuseState, useReducer

  2. EffectsuseEffect, useLayoutEffect, useRef

  3. Browser APIsonClick, onChange, window, localStorage

Everything else — data fetching, layout, static content, heavy libraries — belongs on the server.

The target architecture looks like this:

App (Server)
├── Layout (Server)             ← static structure, zero JS
├── Header (Server)             ← reads DB for nav items
│   └── MobileMenuButton (Client) ← needs onClick
├── ProductPage (Server)        ← async, queries DB directly
│   ├── ProductImages (Client ← needs state for zoom/gallery
│   ├── ProductDetails (Server)   ← static markup, zero JS
│   └── AddToCartButton (Client)  ← needs onClick + state
└── Footer (Server)               ← static markup, zero JS

Result: Only 3 components ship JavaScript. Everything else = zero bytes.

Code: The Patterns That Work

❌ The Wrong Pattern — Everything as a Client Component

// ❌ BAD: Entire blog post as a client component
'use client'

import { useState, useEffect } from 'react'
import { marked } from 'marked'                    // 45KB — now in your bundle
import { Prism as SyntaxHighlighter } from         // 120KB — now in your bundle
  'react-syntax-highlighter'

export default function BlogPost({ id }: { id: string }) {
  const [post, setPost] = useState(null)
  const [loading, setLoading] = useState(true)

  // Waterfall: JS loads → mounts → fetch fires → data arrives → renders
  useEffect(() => {
    fetch(`/api/posts/${id}`)
      .then(r => r.json())
      .then(data => { setPost(data); setLoading(false) })
  }, [id])

  if (loading) return <div>Loading...</div>

  return (
    <article>
      <h1>{post.title}</h1>
      {/* marked + SyntaxHighlighter both shipped to every user */}
      <div dangerouslySetInnerHTML={{ __html: marked(post.content) }} />
    </article>
  )
}

Cost of this approach: 165KB+ of libraries shipped to every user. Full round-trip waterfall before any content shows. No streaming.


✅ The Right Pattern — Server Component + Minimal Client Island

// ✅ GOOD: BlogPost is a Server Component (no 'use client')
// app/blog/[id]/page.tsx

import { marked } from 'marked'                    // stays on server — zero bytes shipped
import { Prism as SyntaxHighlighter } from         // stays on server — zero bytes shipped
  'react-syntax-highlighter'
import { LikeButton } from './LikeButton'          // only interactive part is client
import { db } from '@/lib/database'                // direct DB access — no API route

export default async function BlogPost({
  params
}: {
  params: { id: string }
}) {
  // Runs on the server — no useEffect, no loading state, no waterfall
  const post = await db.posts.findUnique({ where: { id: params.id } })
  if (!post) notFound()

  // marked runs on the server — zero bytes shipped to client
  const htmlContent = marked(post.content)

  return (
    <article className="prose max-w-3xl mx-auto">
      <h1>{post.title}</h1>
      <p className="text-gray-500">
        By {post.author} · {new Date(post.date).toLocaleDateString()}
      </p>
      {/* Pure HTML — no markdown library in browser bundle */}
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
      {/* Only the interactive button needs 'use client' */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  )
}
// app/blog/[id]/LikeButton.tsx — the ONLY client component on this page
'use client'

import { useState } from 'react'

export function LikeButton({
  postId,
  initialLikes
}: {
  postId: string
  initialLikes: number
}) {
  const [likes, setLikes] = useState(initialLikes)
  const [liked, setLiked] = useState(false)

  const handleLike = async () => {
    if (liked) return
    setLiked(true)
    setLikes(prev => prev + 1)
    await fetch(`/api/posts/${postId}/like`, { method: 'POST' })
  }

  return (
    <button
      onClick={handleLike}
      disabled={liked}
      className={`flex items-center gap-2 px-4 py-2 rounded-full border transition-colors
        ${liked ? 'text-red-500 border-red-200 bg-red-50' : 'text-gray-500 border-gray-200'}`}
    >
      ❤️ {likes.toLocaleString()}
    </button>
  )
}

What changed: marked + SyntaxHighlighter (165KB+) never reach the browser. No fetch waterfall. Only LikeButton (~2KB) ships JavaScript.


Parallel Data Fetching: Eliminating the Waterfall

According to SitePoint's 2026 report, most teams only unlock ~30% of RSC's performance potential because of untreated data waterfalls. Here's how to fix it:

// ❌ BAD: Sequential awaits — each blocks the next
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)         // 200ms
  const reviews = await getReviews(params.id)         // 150ms (waits for product)
  const related = await getRelatedProducts(params.id) // 180ms (waits for reviews)
  // Total: 530ms before ANY content renders
}
// ✅ GOOD: Parallel with Promise.all
export default async function ProductPage({ params }: { params: { id: string } }) {
  const [product, reviews, related] = await Promise.all([
    getProduct(params.id),         // 200ms
    getReviews(params.id),         // 150ms  ← all run simultaneously
    getRelatedProducts(params.id)  // 180ms
  ])
  // Total: 200ms (the slowest only)
}
// ✅ EVEN BETTER: Independent Suspense boundaries — each streams as it resolves
export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div>
      <Breadcrumb /> {/* renders immediately — no data needed */}

      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetail id={params.id} />    {/* streams at ~200ms */}
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={params.id} />   {/* streams at ~150ms — independently */}
      </Suspense>

      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts id={params.id} />  {/* streams at ~180ms — independently */}
      </Suspense>
    </div>
  )
}

Partial Pre-rendering (PPR): The Best of Both Worlds

PPR, stable in Next.js 15 and default in Next.js 16, is the culmination of the RSC vision. A single route delivers:

  • Static shell → prerendered at build time, served from CDN in ~30ms

  • Dynamic sections → streamed at request time via Suspense boundaries

PPR Request Flow:
───────────────────────────────────────
  t=0ms    CDN serves static shell (Header + Nav + Sidebar) instantly
  t=~30ms  Server starts streaming LiveMetrics (parallel)
  t=~50ms  Server starts streaming PersonalizedFeed (parallel)
  t=~80ms  Server starts streaming UserNotifications (parallel)
  t=~200ms All dynamic content fully rendered

  vs. traditional SSR: t=0 → ~450ms before ANY content
───────────────────────────────────────
// app/dashboard/page.tsx — a PPR page
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'

// Static content — prerendered at build time, served from CDN
function DashboardHeader() {
  return <header><h1>Dashboard</h1></header>
}

// Cached dynamic content — included in static shell
async function CachedStats() {
  'use cache'
  cacheLife('hours')
  cacheTag('dashboard-stats')
  const stats = await db.analytics.getStats()
  return <StatsGrid stats={stats} />
}

// Runtime dynamic — streams at request time (user-specific)
async function PersonalizedFeed() {
  const user = await getCurrentUser()
  const feed = await getFeedForUser(user.id)
  return <FeedList items={feed} />
}

export default function DashboardPage() {
  return (
    <div className="dashboard-layout">
      {/* ✅ Static shell — served from CDN instantly */}
      <DashboardHeader />

      {/* ✅ Cached — included in static shell */}
      <CachedStats />

      {/* ✅ Dynamic — streams at request time */}
      <Suspense fallback={<FeedSkeleton />}>
        <PersonalizedFeed />
      </Suspense>
    </div>
  )
}
// next.config.js — enable PPR
const nextConfig = {
  experimental: {
    ppr: 'incremental', // Route-by-route adoption (Next.js 15)
    // ppr: true        // Global (Next.js 16 default)
  },
}

⚠️ PPR Gotcha #1: Calling cookies() or getServerSession() outside a Suspense boundary forces the entire route to dynamic rendering, silently defeating PPR. Push auth into a Suspense boundary.

⚠️ PPR Gotcha #2: Next.js 15 changed fetch() to uncached by default. Audit every fetch in your static shell — add explicit { cache: 'force-cache' } where you want caching.


The 'use cache' Directive: Next.js 16's Caching Revolution

Stable in Next.js 16 (experimental in 15), 'use cache' replaces unstable_cache and caches any async function — not just fetch().

// Cache a database query with granular control
import { cacheLife, cacheTag } from 'next/cache'

async function getProducts(categoryId: string) {
  'use cache'
  cacheLife('hours')                              // 5min fresh, 1hr revalidate, 1 day expire
  cacheTag('products', `category-${categoryId}`) // tags for on-demand invalidation

  return db.products.findMany({
    where: { categoryId, active: true },
    orderBy: { updatedAt: 'desc' }
  })
}
// Invalidate on mutation (Server Action)
'use server'
import { revalidateTag } from 'next/cache'

export async function updateProduct(id: string, data: ProductData) {
  await db.products.update({ where: { id }, data })
  revalidateTag(`product-${id}`, 'max') // Stale + background regen (SWR pattern)
  revalidateTag('products', 'max')
}
// Custom cache profiles in next.config.ts
const nextConfig = {
  cacheComponents: true,
  cacheLife: {
    'product-catalog': { stale: 300, revalidate: 900, expire: 3600 },
    'blog-posts':      { stale: 3600, revalidate: 86400, expire: 604800 },
    'live-data':       { stale: 0, revalidate: 30, expire: 120 },
  },
}

Server Actions: Mutations Without API Routes

Server Actions replace fetch('/api/...') for mutations. They run on the server, can be called directly from Client Components, and work with useActionState for progressive enhancement.

// app/actions/cart.ts
'use server'

import { z } from 'zod'
import { revalidateTag } from 'next/cache'

// ✅ Always validate — Server Actions are public API endpoints
const AddToCartSchema = z.object({
  productId: z.string().cuid(),
  quantity: z.number().int().min(1).max(99),
})

export async function addToCart(formData: FormData) {
  const result = AddToCartSchema.safeParse({
    productId: formData.get('productId'),
    quantity: Number(formData.get('quantity')),
  })

  if (!result.success) {
    return { error: 'Invalid input', details: result.error.flatten() }
  }

  // Verify auth — always check on the server
  const session = await getServerSession()
  if (!session?.user) {
    return { error: 'Authentication required' }
  }

  await db.cartItems.upsert({
    where: { userId_productId: { userId: session.user.id, productId: result.data.productId } },
    update: { quantity: { increment: result.data.quantity } },
    create: { userId: session.user.id, ...result.data },
  })

  revalidateTag(`cart-${session.user.id}`, 'max')
  return { success: true }
}
// app/components/AddToCartButton.tsx
'use client'

import { useActionState } from 'react' // React 19 — replaces useFormState
import { addToCart } from '@/app/actions/cart'

export function AddToCartButton({ productId }: { productId: string }) {
  const [state, formAction, isPending] = useActionState(addToCart, null)

  return (
    <form action={formAction}>
      <input type="hidden" name="productId" value={productId} />
      <input type="hidden" name="quantity" value="1" />
      <button type="submit" disabled={isPending} className="btn-primary">
        {isPending ? 'Adding...' : 'Add to Cart'}
      </button>
      {state?.error && <p className="text-red-500 text-sm mt-1">{state.error}</p>}
    </form>
  )
}

⚠️ Security Alert: CVE-2025-55182 — CVSS 10.0 Critical

In December 2025, a critical remote code execution vulnerability was discovered in React Server Components.

Severity: CVSS 10.0 (maximum possible score) Type: Pre-authentication Remote Code Execution Affected: React 19.0.0–19.2.0, Next.js 14.3.0-canary.77 through 15.x/16.x (pre-patch) Fixed in: React 19.0.1+, Next.js 15.0.5+, 16.0.7+

Root cause: Unsafe deserialization of HTTP request payloads to Server Function endpoints. The requireModule function performed prototype chain lookup instead of checking own properties, allowing attackers to shadow hasOwnProperty with a malicious reference — giving access to constructor, __proto__, and RCE gadgets like child_process.execSync.

Attack vector: A single maliciously crafted HTTP POST request to any Server Function endpoint. No credentials required. No user interaction needed.

Real-world impact: ~145 in-the-wild proof-of-concept exploits identified. Listed in CISA KEV database. EPSS score: 84% probability of exploitation.

// ❌ VULNERABLE: Passing unsanitized input to server operations
'use server'

export async function searchProducts(query: string) {
  // If query contains injection payload, this is exploitable
  const results = await db.raw(`SELECT * FROM products WHERE name LIKE '%${query}%'`)
  return results
}
// ✅ SAFE: Always validate and sanitize with Zod + parameterized queries
'use server'

import { z } from 'zod'

const SearchSchema = z.object({
  query: z.string().max(100).regex(/^[\w\s]+$/)
})

export async function searchProducts(formData: FormData) {
  const { query } = SearchSchema.parse({ query: formData.get('query') })

  // Parameterized query — never string interpolation
  return db.products.findMany({
    where: { name: { contains: query } }
  })
}

🔐 The rule: Treat every Server Action like a public API endpoint. Validate all inputs with Zod. Use parameterized queries. Never trust data from the client — even in Server Actions. Upgrade to React 19.0.1+ and Next.js 15.0.5+ immediately if you haven't.


The 'use client' Propagation Trap (The 2.1MB Bug)

This is the single most common RSC mistake. It's responsible for teams seeing zero improvement after "migrating to RSC":

// ❌ CATASTROPHIC: 'use client' on the root layout
// app/layout.tsx
'use client' // ← Added because ThemeProvider uses useState

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html><body><ThemeProvider>{children}</ThemeProvider></body></html>
  )
}

What happens: 'use client' propagates downward through the entire component tree. Every page, every component, every import becomes a Client Component. The RSC runtime is active but never used. Bundle stays at 2.1MB.

The fix — isolate the client-only piece:

// app/providers/ThemeProvider.tsx — isolated client wrapper
'use client'

import { createContext, useState } from 'react'

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// app/layout.tsx — NO 'use client' here
import { ThemeProvider } from './providers/ThemeProvider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* ThemeProvider is 'use client', but layout itself is NOT */}
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Result: Bundle drops from 2.1MB to 680KB. LCP improves from 3.2s to 1.4s. Same functionality.


'use client' vs 'use server' — Complete Reference


React Compiler + RSC: A Synergistic Combination

The React Compiler (v1.0, October 2025) and RSC work together, not in competition:

  • RSC reduces the volume of code running on the client — entire component subtrees eliminated from the bundle

  • React Compiler reduces the frequency of re-renders within the Client Components that remain — automatic memoization without useMemo/useCallback

Per React team benchmarks from React Conf 2024: enabling the Compiler on a production app that was already well-memoized by hand reduced re-renders by an additional 22%. Manual memoization, even by experienced engineers, leaves significant optimization unrealized.

A well-structured RSC app: 60–70% Server Components (zero re-render cost) + 30–40% Client Components (Compiler-optimized).

You can stop writing useMemo and useCallback. You can stop writing data-fetching useEffect. RSC + Compiler eliminates two of the biggest sources of React complexity simultaneously.


TanStack Start vs Next.js: The RSC Landscape in 2026

The RSC ecosystem is no longer a Next.js monopoly.

Aspect Next.js 16 TanStack Start
RSC support ✅ Full production 🔄 Experimental (April 2026)
Build tool Turbopack Vite
Dev startup 10–12s 2–3s
HMR speed ~836ms ~335ms
Bundle (hello world) 80–95KB 45–60KB
Deployment Optimized for Vercel Any host (Nitro runtime)
Type safety Partial End-to-end (TanStack Router)
Ecosystem Massive Growing
Memory behavior Linear growth in K8s Standard Node.js

Next.js remains the default for content-heavy apps, large teams, and Vercel deployments. TanStack Start is gaining ground for teams that value type safety, deployment flexibility, and faster development iteration.

🔥 Hot Take from Tanner Linsley: "RSC is a protocol, not an architecture. Frameworks are using RSC primitives in the way that has been revealed to them thus far — which is fine if that model covers your use cases. Ours needed more." TanStack Start treats RSC as one tool in the pipeline, not the entire pipeline.


When NOT to Use RSC

RSC is not a silver bullet. Here's the honest assessment:

Scenario RSC Value Recommendation
Content-heavy pages (blogs, docs, listings) 🟢 Very High Use RSC aggressively
Marketing / landing pages 🟢 Very High RSC + PPR
E-commerce product pages 🟢 High RSC with client islands
Data dashboards (mostly read-only) 🟡 Medium RSC for data, client for charts
Admin panels with complex filters 🟡 Low Mostly client, RSC for shell
Real-time features (chat, live feeds) 🔴 None Client + WebSocket
Drag-and-drop UIs 🔴 None Client Components
Offline-first apps 🔴 None Client Components
Authenticated SPAs (no SEO needed) 🟡 Low Consider plain SPA + API

🔥 Hot Take: If your app is 80%+ interactive — think Figma, Google Docs, or a complex real-time dashboard — RSC adds architectural complexity without proportional performance gains. A plain SPA with a separate API is sometimes the right answer. The 67% dissatisfaction rate in the survey is largely from developers applying RSC to the wrong problem.


The Ecosystem Reality Check

State of React 2025 — The Honest Numbers
───────────────────────────────────────
  React 19 daily usage:     48.4% of respondents
  SPA still dominant:       84.5% of projects
  RSC adoption:             45% of new projects
  RSC positive sentiment:   33%
  RSC negative sentiment:   67%

  Top RSC pain points:
  ├── Context API incompatibility(59 mentions — #1 complaint)
  ├── Testing complexity(24 mentions)
  ├── Debugging difficulty
  ├── Too many directives
  └── Excessive complexity

  Framework sentiment:
  ├── TanStack Query:  68% usage, 42% positive, 1% negative  
  ├── Next.js:         80% usage, 27% positive, 17% negative
  └── React Compiler:  62% excited (top upcoming feature)
────────────────────────────────────────

The community friction is real. Context API incompatibility is the #1 pain point — many state managers, UI libraries, and theme providers use React Context internally, requiring 'use client' wrapper files. Testing RSC components requires different tooling. Error messages are still improving.

But the direction is clear: Next.js 16 makes App Router the only recommended path. TanStack Start is adding RSC support. Remix/React Router v7 is adding it. The ecosystem is converging.


Migration Checklist: Pages Router → App Router

Phase 1: Low-Risk Wins (Week 1–2)
  ☐ Pick one content-heavy route (blog, product listing, landing page)
  ☐ Move it to App Router as a Server Component
  ☐ Replace useEffect data fetching with async Server Component
  ☐ Add Suspense boundaries around slow data sections
  ☐ Measure: bundle size + LCP before/after with @next/bundle-analyzer
  ☐ If no improvement → check for 'use client' leaking up the tree

Phase 2: Architectural Changes (Week 3–4)
  ☐ Identify true 'use client' candidates (state/effects/events only)
  ☐ Push 'use client' to leaf components (buttons, inputs, modals)
  ☐ Replace Context providers with isolated 'use client' wrapper files
  ☐ Replace API routes with Server Actions for mutations
  ☐ Add Zod validation to every Server Action
  ☐ Upgrade to React 19.0.1+ and Next.js 15.0.5+ (CVE-2025-55182 patch)

Phase 3: Advanced Optimization (Month 2)
  ☐ Add 'use cache' directives for expensive DB queries
  ☐ Configure cacheLife profiles for your domain
  ☐ Add cacheTag for on-demand invalidation after mutations
  ☐ Enable PPR for high-traffic routes
  ☐ Audit Suspense boundary granularity (3–5 per route max)
  ☐ Set up bundle size CI gates with bundlesize

The 5 RSC Rules to Memorize


Further Reading