Back to Blog
Next.js DevelopmentWeb Development Best Practices

Why Next.js RSC Performance Suffers with CDNs in Highload Projects (And How to Fix It)

Dharmendra
Dharmendra
9 min read
Why Next.js RSC Performance Suffers with CDNs in Highload Projects (And How to Fix It)

Why Next.js RSC Performance Suffers with CDNs in Highload Projects (And How to Fix It)

If you've deployed a Next.js application with React Server Components (RSC) behind a CDN and noticed underwhelming cache hit rates under high traffic, you're not alone. This friction point has been documented by developers building large-scale applications, particularly in e-commerce scenarios where the same product data appears across hundreds of different pages.

This article unpacks why Next.js RSC CDN performance becomes problematic at scale, what causes these issues at a technical level, and—most importantly—how to architect your application to work harmoniously with edge caching.

The Problem: Perceived Inefficiency of RSC with CDNs in Highload Next.js Projects

Consider a high-throughput e-commerce site with 100,000+ products. A single product might appear across numerous product listing pages:

  • /mens/
  • /mens/trainers
  • /mens/trainers/brand
  • /mens/trainers/brand?facet-price=%3A168

Each of these URLs generates client-side navigation requests with RSC payloads. Here's where the problem emerges: identical product data generates different RSC request signatures.

As documented in GitHub Issue #65335, developers have observed requests like:

/product/299336/?_rsc=1vl30
/product/299336/?_rsc=qe3go
/product/299336/?_rsc=1vg99
/product/299336/?_rsc=1stsw

Despite returning identical data, each request carries a unique _rsc parameter derived from the navigation context. For a CDN, these are four distinct cache keys—leading to cache misses when there should be cache hits.

The Vary header compounds this problem:

Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url

These headers tell the CDN to create separate cache entries based on navigation state, prefetch status, and origin URL—factors that don't actually affect the response data.

Understanding RSC and CDN Fundamentals: How They Should Work Together

React Server Components fundamentally change the rendering paradigm. Instead of sending JavaScript bundles that render on the client, RSC sends a serialized component tree (the RSC payload) that React can hydrate efficiently.

The theoretical CDN synergy is compelling:

  • RSC payloads are typically smaller than equivalent client-side bundles
  • Server-rendered content can be cached at the edge
  • Subsequent navigations fetch lightweight payloads instead of full page loads

Next.js implements four caching layers that should enable excellent Next.js RSC CDN performance:

MechanismWhatWherePurpose
Request MemoizationFunction return valuesServerRe-use data within request
Data CacheFetched dataServerPersist across requests
Full Route CacheHTML and RSC payloadServerReduce rendering cost
Router CacheRSC payloadClientReduce navigation requests

When configured correctly, the Full Route Cache stores pre-rendered RSC payloads that a CDN can serve directly from edge locations. The problem arises when the cache key generation doesn't align with actual data variance.

Why This Happens: Unpacking the Interaction Challenges and Performance Bottlenecks

The _rsc parameter and Vary header behavior exist for legitimate technical reasons:

1. Navigation State Dependency

RSC needs to know the previous router state to compute minimal updates. The Next-Router-State-Tree header encodes this state, ensuring React can perform targeted reconciliation rather than full re-renders.

2. Prefetch Differentiation

Prefetched navigations (Next-Router-Prefetch) may receive abbreviated payloads optimized for instant transitions, while full navigations receive complete component trees.

3. URL Context for Layouts

The Next-Url header helps determine which layout segments remain stable during navigation, enabling partial rendering optimizations.

The architectural tension is clear: these optimizations assume a direct server connection where per-request computation is cheap. When a CDN sits between client and server, each unique header combination fragments the cache.

For highload applications, this creates a difficult choice:

  • Cache all RSC requests: Duplicates identical data across thousands of cache entries
  • Bypass caching entirely: Every request hits the origin, negating CDN benefits
  • Accept cache misses: Accept degraded performance during cold-cache scenarios

Identifying Misconfigurations: Common Pitfalls and Incorrect Usage Patterns

Before implementing advanced solutions, audit your application for these common misconfigurations:

Pitfall 1: Unintentional Dynamic Rendering

Server Components become dynamic when they access request-time APIs. Check for these patterns:

// ❌ Forces dynamic rendering - breaks CDN caching
export default async function ProductPage() {
  const headers = await headers(); // Request-time API
  const cookies = await cookies(); // Request-time API
  
  return <Product />;
}
 
// ✅ Static rendering - CDN cacheable
export default async function ProductPage({ 
  params 
}: { 
  params: Promise<{ id: string }> 
}) {
  const { id } = await params;
  const product = await getProduct(id); // Cached data fetch
  
  return <Product data={product} />;
}

Pitfall 2: Missing Static Generation Configuration

For pages with known parameter sets, explicit static generation dramatically improves cache efficiency:

// app/product/[id]/page.tsx
export async function generateStaticParams() {
  const products = await getTopProducts(1000);
  
  return products.map((product) => ({
    id: product.id.toString(),
  }));
}
 
// Optional: Control dynamic parameter behavior
export const dynamicParams = true; // Allow dynamic fallback
export const revalidate = 3600; // Revalidate hourly

Pitfall 3: Incorrect Fetch Caching

Next.js extends fetch with caching options, but defaults changed significantly:

// Default behavior (no caching without explicit opt-in in Next.js 15+)
const data = await fetch('https://api.example.com/products');
 
// ✅ Explicit caching with revalidation
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 } // Cache for 1 hour
});
 
// ✅ Force cache for truly static data
const data = await fetch('https://api.example.com/static-config', {
  cache: 'force-cache'
});

Optimizing RSC for Highload: Best Practices for CDN Caching and Edge Strategies

To achieve optimal Next.js RSC CDN performance, implement these proven strategies:

Strategy 1: Normalize Cache Keys

Configure your CDN to strip or normalize RSC-specific parameters. For Cloudflare:

// Cloudflare Worker - Cache key normalization
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});
 
async function handleRequest(request) {
  const url = new URL(request.url);
  
  // Create normalized cache key
  const cacheKey = new Request(url.pathname + url.search.replace(/_rsc=[^&]+&?/, ''), {
    method: request.method,
    headers: new Headers({
      // Exclude navigation-specific headers from cache key
      'Accept': request.headers.get('Accept') || '*/*',
      'RSC': request.headers.get('RSC') || '',
    }),
  });
  
  const cache = caches.default;
  let response = await cache.match(cacheKey);
  
  if (!response) {
    response = await fetch(request);
    if (response.ok) {
      const cloned = response.clone();
      // Cache with normalized key
      event.waitUntil(cache.put(cacheKey, cloned));
    }
  }
  
  return response;
}

Strategy 2: Implement Route-Level Caching Policies

Use route segment configuration to establish clear caching boundaries:

// app/products/[category]/layout.tsx
export const revalidate = 300; // 5-minute baseline
 
// app/products/[category]/[id]/page.tsx  
export const revalidate = 3600; // Product pages cache longer
 
// app/cart/page.tsx
export const dynamic = 'force-dynamic'; // Never cache user-specific data

Strategy 3: Separate Static from Dynamic Content

Architect your pages to isolate dynamic elements:

// app/product/[id]/page.tsx
import { Suspense } from 'react';
import { ProductDetails } from './ProductDetails';
import { PersonalizedRecommendations } from './PersonalizedRecommendations';
 
export default async function ProductPage({ 
  params 
}: { 
  params: Promise<{ id: string }> 
}) {
  const { id } = await params;
  
  return (
    <main>
      {/* Static - fully cacheable */}
      <ProductDetails id={id} />
      
      {/* Dynamic - streams after initial render */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations productId={id} />
      </Suspense>
    </main>
  );
}

Advanced Techniques: Streaming, Stale-While-Revalidate, and Edge Runtime Considerations

Streaming for Perceived Performance

RSC streaming allows the CDN to cache and serve the initial HTML while dynamic content streams incrementally:

// Components that return immediately get cached
// Suspended components stream when ready
export default function Page() {
  return (
    <main>
      <Header /> {/* Instant */}
      <Suspense fallback={<LoadingProducts />}>
        <ProductGrid /> {/* Streams when data ready */}
      </Suspense>
      <Footer /> {/* Instant */}
    </main>
  );
}

This pattern improves Next.js RSC CDN performance by allowing edge servers to respond immediately with cacheable shell content.

Stale-While-Revalidate Pattern

Configure your CDN to serve stale content while fetching fresh data:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/products/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=300, stale-while-revalidate=600',
          },
        ],
      },
    ];
  },
};

This serves cached content for 5 minutes, then serves stale content for up to 10 additional minutes while revalidating in the background.

Edge Runtime Considerations

The Edge Runtime (export const runtime = 'edge') moves execution closer to users but has trade-offs:

// app/api/lightweight/route.ts
export const runtime = 'edge';
 
export async function GET(request: Request) {
  // Limited Node.js APIs available
  // Faster cold starts, lower latency
  return Response.json({ timestamp: Date.now() });
}

When to use Edge Runtime:

  • Simple API routes needing minimal latency
  • Personalization based on headers/cookies
  • A/B testing logic

When to avoid Edge Runtime:

  • Complex database operations
  • Heavy computation
  • Node.js-specific dependencies

Future Outlook: Improvements in Next.js and Vercel for RSC/CDN Synergy

The Next.js team acknowledges these challenges. Several improvements are in progress:

Route Handler Caching Enhancements

Future versions aim to provide more granular control over RSC payload caching, potentially allowing developers to specify which header combinations should actually affect cache variance.

Experimental staleTimes Configuration

Next.js 14.2+ introduced experimental client-side cache duration controls:

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,  // 30 seconds for dynamic routes
      static: 180,  // 3 minutes for static routes
    },
  },
};

This provides finer control over the client-side Router Cache, reducing unnecessary server requests.

Improved Prefetch Behavior

Recent versions have refined prefetch handling to reduce payload variance between prefetch and full navigation requests, improving CDN cache hit rates.

Platform-Level Solutions

Vercel's infrastructure implements RSC-aware caching that understands payload semantics rather than treating RSC requests as opaque HTTP traffic. For self-hosted deployments, this intelligence must be manually implemented via CDN configuration.


Key Takeaways

  • RSC navigation parameters create cache fragmentation: The _rsc parameter and Vary headers cause CDNs to create separate cache entries for identical data, degrading cache hit rates under high traffic.

  • Explicit static generation is essential: Use generateStaticParams and route segment configuration to pre-render high-traffic pages and establish predictable caching behavior.

  • Isolate dynamic content with Suspense: Separate cacheable static content from personalized elements using Suspense boundaries, enabling CDNs to serve cached shells while streaming dynamic content.

  • CDN configuration requires RSC awareness: Implement cache key normalization and stale-while-revalidate patterns at the CDN level to work around RSC-specific caching challenges.

  • Monitor and iterate: Use your CDN's analytics to identify cache hit rates by route and optimize your highest-traffic pages first.

Next Steps

  1. Audit your current cache behavior: Review your CDN analytics to identify routes with low cache hit rates despite static content.

  2. Implement generateStaticParams: Start with your top 100 most-visited dynamic routes and add explicit static generation.

  3. Configure CDN cache key normalization: Work with your CDN provider to implement RSC-aware cache key handling.

  4. Add Suspense boundaries strategically: Identify pages mixing static and dynamic content, then refactor to enable partial caching.

  5. Monitor the GitHub issue: Follow Issue #65335 for updates on native RSC/CDN performance improvements.

Tags

#Tutorial
Share:
Dharmendra

Dharmendra

Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.

Premium Templates

Build Your Next Project Faster

Save hours of development time with our premium Next.js templates. Built with Next.js 16, React 19, and Tailwind CSS 4.

Subscribe to our newsletter

Get the latest articles, tutorials, and product updates delivered to your inbox.

Related Articles