ISR (Caching)
ISR (Incremental Static Regeneration)
Stale-while-revalidate caching for dynamic data with fresh reads.
Table of Contents
Overview
ISR provides automatic caching with background revalidation. Cached data is served immediately while fresh data is fetched in the background, ensuring fast responses without stale content.
# Fetch posts with 60-second cache
posts = await withRevalidate('posts:all', 60, -> { Post.all })
How it works:
- Fresh (within TTL): Return cached data immediately
- Stale (past TTL): Return cached data, fetch fresh in background
- Missing: Fetch fresh data, cache it, return
API
withRevalidate(key, ttlSeconds, fetcher)
Cache data with automatic background revalidation.
import { withRevalidate } from '../lib/isr.js'
# Basic usage
posts = await withRevalidate('posts:all', 60, -> { Post.all })
# With parameters in the key
post = await withRevalidate("post:#{slug}", 300, -> { Post.findBy(slug: slug) })
# Longer TTL for expensive queries
stats = await withRevalidate('dashboard:stats', 3600, -> { compute_stats() })
Parameters:
| Parameter | Type | Description |
|---|---|---|
key |
String | Unique cache key |
ttlSeconds |
Number | Time-to-live in seconds |
fetcher |
Function | Async function that returns fresh data |
invalidate(key)
Remove a specific key from the cache, forcing fresh fetch on next request.
import { invalidate } from '../lib/isr.js'
# Invalidate after mutation
post.save().then do
invalidate('posts:all')
invalidate("post:#{post.slug}")
end
invalidateAll()
Clear the entire cache. Useful for logout or major data changes.
import { invalidateAll } from '../lib/isr.js'
def handleLogout()
invalidateAll()
navigate('/login')
end
Pragma Syntax (Planned)
For page-level caching, use the revalidate pragma:
# Pragma: revalidate 60
@posts = Post.published.order(date: :desc)
__END__
<Layout>
{posts.map { |p| <Card post={p} /> }}
</Layout>
This is equivalent to wrapping the page data in withRevalidate.
Per-Target Implementation
ISR adapts to each deployment target:
| Target | Cache Layer | Implementation |
|---|---|---|
| Browser | In-memory | JavaScript Map with TTL |
| Node.js | In-memory | JavaScript Map with TTL |
| Vercel | Native ISR | revalidate export |
| Cloudflare | Cache API | Edge caching with stale-while-revalidate |
Browser / Node.js
In-memory cache using JavaScript Map:
// lib/isr.js
const cache = new Map();
export async function withRevalidate(key, ttlSeconds, fetcher) {
const cached = cache.get(key);
const now = Date.now();
if (cached && now < cached.staleAt) {
return cached.data; // Fresh
}
if (cached) {
// Stale - return cached, revalidate in background
fetcher().then(data => {
cache.set(key, { data, staleAt: now + ttlSeconds * 1000 });
});
return cached.data;
}
// Missing - fetch fresh
const data = await fetcher();
cache.set(key, { data, staleAt: now + ttlSeconds * 1000 });
return data;
}
Vercel
Native ISR via revalidate export:
export const revalidate = 60; // seconds
export async function getStaticProps() {
const posts = await Post.all();
return { props: { posts } };
}
Cloudflare
Cache API with stale-while-revalidate:
const cache = caches.default;
async function withRevalidate(key, ttl, fetcher) {
const cacheKey = new Request(`https://cache/${key}`);
const cached = await cache.match(cacheKey);
if (cached) {
// Return cached, revalidate in background
ctx.waitUntil(revalidate(cacheKey, ttl, fetcher));
return cached.json();
}
const data = await fetcher();
await cache.put(cacheKey, new Response(JSON.stringify(data), {
headers: { 'Cache-Control': `max-age=${ttl}` }
}));
return data;
}
Usage Patterns
List with Create/Update/Delete
Invalidate on mutations to ensure list stays current:
import { withRevalidate, invalidate } from '../lib/isr.js'
def PostList()
posts, setPosts = useState([])
loadPosts = -> {
withRevalidate('posts:all', 60, -> { Post.all }).then { |data| setPosts(data) }
}
# Listen for changes
useEffect -> {
handler = -> { invalidate('posts:all'); loadPosts.() }
window.addEventListener('post-created', handler)
window.addEventListener('post-updated', handler)
window.addEventListener('post-deleted', handler)
-> {
window.removeEventListener('post-created', handler)
window.removeEventListener('post-updated', handler)
window.removeEventListener('post-deleted', handler)
}
}, []
# ...
end
Related Queries
Invalidate related caches together:
def invalidate_post_caches(post)
invalidate('posts:all')
invalidate("post:#{post.slug}")
invalidate("category:#{post.category_id}:posts")
end
Cache Key Conventions
Use consistent key patterns:
| Pattern | Example | Use Case |
|---|---|---|
resource:all |
posts:all |
Full collection |
resource:id |
post:123 |
Single record by ID |
resource:slug |
post:getting-started |
Single record by slug |
parent:id:children |
user:5:posts |
Scoped collection |
page:path |
page:/about |
Page-level cache |
Working Example
The Astro Blog demo demonstrates ISR with:
- PostList using
withRevalidatefor 60-second caching - Cache invalidation on create/update/delete events
- Custom events for cross-component communication
Future Enhancements
| Target | Current | Future |
|---|---|---|
| Browser | In-memory | Service Worker + Cache API |
| Node.js | In-memory | Redis adapter |
| Edge | Cache API | Durable Objects for coordination |
The architecture supports these upgrades without changing application code.