SSR Caching — Caching Server-Rendered Pages for Performance
In this tutorial, you will learn about SSR Caching. We cover key concepts, practical examples, and best practices to help you master this topic.
SSR caching stores rendered HTML to serve subsequent requests without re-rendering, using in-memory caches, Redis, CDN edge caching, and fragment caching strategies for optimal performance.
What You'll Learn
By the end of this tutorial, you will understand why SSR caching is essential, how to implement in-memory and Redis caching for full pages, fragment caching for reusable components, CDN edge caching with cache-control headers, and cache invalidation strategies.
Why It Matters
SSR renders every page on every request. For high-traffic pages, this means the server does the same work thousands of times per second. Without caching, a single popular page can overwhelm the server. Caching reduces server load by 90-99 percent and improves response times from hundreds of milliseconds to single milliseconds.
Real-World Use
An e-commerce site reduced server load from 10,000 requests per second to 200 by implementing Redis caching for popular product pages. Response times dropped from 400ms to 8ms. They saved 80 percent on server costs while improving user experience. The cache served 98 percent of product page requests.
SSR Caching Layers
┌──────────────────────────────────────────────────────────┐
│ SSR Caching Layers │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. CDN Edge Cache (Fastly, CloudFront, Cloudflare) │
│ • Closest to user, fastest response │
│ • Caches full HTML pages │
│ • TTL: 5-60 minutes │
│ • Invalidate via purge API │
│ │
│ 2. Reverse Proxy (Nginx, Varnish) │
│ • Caches behind the server │
│ • Configurable rules per URL pattern │
│ • TTL: 1-5 minutes │
│ │
│ 3. Application Cache (Redis, Memcached) │
│ • Caches rendered HTML in memory │
│ • Fast reads (1-5ms) │
│ • Supports fragment caching │
│ │
│ 4. In-Memory Cache (Node.js heap) │
│ • Fastest (no network call) │
│ • Limited by server memory │
│ • Lost on server restart │
│ │
└──────────────────────────────────────────────────────────┘
Think of SSR caching like a bakery with multiple display cases. The CDN is the front counter — customers grab pre-packaged items immediately. Redis is the back counter with frequently-ordered cakes ready to serve. In-memory cache is the baker's bench with the next batch cooling. Each layer reduces how often the baker must start from scratch.
Redis Page Caching
const express = require('express');
const { createClient } = require('redis');
const { renderToPipeableStream } = require('react-dom/server');
const redis = createClient();
redis.connect();
// SSR with Redis caching
async function cachedSSR(req, res, component, cacheTTL = 60) {
const cacheKey = `ssr:${req.originalUrl}`;
// 1. Check cache
try {
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache HIT:', cacheKey);
res.setHeader('X-Cache', 'HIT');
return res.send(cached);
}
} catch (err) {
console.error('Cache read error:', err);
}
// 2. Not cached — render and cache
console.log('Cache MISS:', cacheKey);
let html = '';
const { pipe } = renderToPipeableStream(component, {
onShellReady() {
res.setHeader('Content-Type', 'text/html');
res.write('<div id="root">');
pipe(res);
},
onAllReady() {
res.end('</div>');
}
});
// Capture the HTML for caching
// In practice, use a writable stream to capture output
const chunks = [];
const originalWrite = res.write.bind(res);
res.write = (chunk) => {
chunks.push(chunk);
return originalWrite(chunk);
};
res.on('finish', async () => {
const fullHtml = chunks.join('');
await redis.setEx(cacheKey, cacheTTL, fullHtml);
});
}
// Usage
app.get('/products/:id', async (req, res) => {
const ProductPage = await import('./pages/ProductPage');
cachedSSR(req, res,
React.createElement(ProductPage.default, { id: req.params.id }),
300 // Cache for 5 minutes
);
});
Fragment Caching
// Cache reusable component fragments
class FragmentCache {
constructor(redisClient) {
this.redis = redisClient;
}
async getOrRender(key, renderFn, ttl = 300) {
// Try cache
const cached = await this.redis.get(`fragment:${key}`);
if (cached) {
return cached;
}
// Render fragment
const html = await renderFn();
// Store in cache
await this.redis.setEx(`fragment:${key}`, ttl, html);
return html;
}
async invalidate(pattern) {
const keys = await this.redis.keys(`fragment:${pattern}`);
if (keys.length > 0) {
await this.redis.del(keys);
}
}
}
const fragmentCache = new FragmentCache(redis);
// Usage in SSR
app.get('/products/:id', async (req, res) => {
const product = await db.products.findById(req.params.id);
// Cache the product details fragment (same for all users)
const productHtml = await fragmentCache.getOrRender(
`product:${product.id}`,
() => renderToString(
React.createElement(ProductDetails, { product })
),
600 // 10 minutes
);
// Cache the sidebar (same for all users)
const sidebarHtml = await fragmentCache.getOrRender(
'sidebar:categories',
() => renderToString(React.createElement(CategorySidebar)),
300 // 5 minutes
);
// Render personalized content (not cached)
const userHeader = await renderToString(
React.createElement(UserHeader, { user: req.user })
);
// Assemble the page
res.send(assemblePage({ productHtml, sidebarHtml, userHeader }));
});
Common Mistakes
- Caching personalized content. Never cache pages with user-specific data (username, cart items, recommendations). These should bypass the cache or use ESI (Edge Side Includes).
- Not invalidating cache on content update. When content changes, the cache must be invalidated. Use Webhooks, database triggers, or TTL-based expiration.
- Caching too long. Long cache TTLs serve stale content. Balance freshness with performance. Use stale-while-revalidate for serving stale content while fetching fresh data.
- No cache key for query parameters. URLs with different query parameters should have different cache keys. Otherwise, /search?q=react and /search?q=vue serve the same cached page.
- Cache stampede. When a popular cache key expires, multiple requests may all try to re-render simultaneously. Use mutex locks or stale-while-revalidate to prevent this.
Practice Questions
- What are the four layers of SSR caching?
- How does Redis page caching work for SSR?
- What is fragment caching and when should you use it?
- How do you invalidate cache when content changes?
- What is cache stampede and how do you prevent it?
Challenge: Implement a multi-layer caching system for an SSR application: Redis page caching for public product pages with 5-minute TTL, fragment caching for navigation and sidebar, CDN cache headers set via response headers, cache invalidation via Webhook when content is updated, and cache stampede protection with mutex locks.
FAQ
Mini Project
Implement SSR caching for a blog application: Redis full-page caching for the home page and blog posts with 5-minute TTL, fragment caching for the author bio and related posts sections, cache invalidation triggered by webhook when a new post is published, Vary: Cookie header for admin preview, and cache hit ratio monitoring.
What's Next
You understand SSR caching. Now explore Redis Caching for advanced caching with Redis.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro