DEV Community

A0mineTV
A0mineTV

Posted on

Implementing Incremental Static Regeneration (ISR) in Nuxt 4: The Complete Guide

Incremental Static Regeneration (ISR) is a game-changing feature that combines the best of static generation and server-side rendering. With Nuxt 4, implementing ISR has become more streamlined and powerful than ever. In this comprehensive guide, I'll show you how to build a complete ISR-enabled application with real-world examples.

What is ISR and Why Should You Care?

ISR allows you to serve statically generated pages while updating them incrementally in the background. Think of it as having your cake and eating it too:

  • ⚑ Lightning-fast performance - Pages served from cache
  • πŸ”„ Always fresh content - Background regeneration keeps data current
  • πŸ“ˆ Infinite scalability - No server overload on traffic spikes
  • 🎯 SEO excellence - Pre-rendered content for search engines

The ISR Advantage Over Traditional Approaches

Approach Performance Freshness Scalability SEO
Static (SSG) ⭐⭐⭐⭐⭐ ❌ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Server-Side (SSR) ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Client-Side (SPA) ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ❌
ISR ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

Setting Up Nuxt 4 for ISR

First, let's configure Nuxt 4 with the proper ISR setup:

// nuxt.config.ts export default defineNuxtConfig({ devtools: { enabled: true }, pages: true, // Enable Nuxt 4 features future: { compatibilityVersion: 4 }, experimental: { payloadExtraction: false }, // Nitro configuration for ISR nitro: { preset: 'node-server', experimental: { wasm: true }, // Stable cache directory storage: { cache: { driver: 'fs', base: './.data/cache' } } }, // Route-based ISR configuration routeRules: { // Homepage can be SSR or prerendered '/': {}, // Blog posts regenerate every hour '/blog': { isr: 300 }, // 5 minutes for index '/blog/**': { isr: 3600 }, // 1 hour for posts // Product catalog with frequent updates '/products': { isr: 300 }, '/products/**': { isr: 300 }, // 5 minutes for products // API routes with CORS '/api/**': { cors: true } } }) 
Enter fullscreen mode Exit fullscreen mode

Building a Real-World Example: Blog with ISR

Let's create a blog that demonstrates ISR in action. We'll build both the frontend pages and API endpoints.

1. API Endpoint for Blog Posts

// server/api/blog/index.get.ts const blogPosts = [ { id: 1, slug: 'nuxt-isr-guide', title: 'Complete Guide to ISR with Nuxt 4', excerpt: 'Learn how to implement ISR in your Nuxt applications', content: ` <h2>Introduction to ISR</h2> <p>Incremental Static Regeneration (ISR) combines the benefits of static generation with dynamic content updates.</p> <h2>Key Benefits</h2> <ul> <li>Optimal performance</li> <li>Always up-to-date content</li> <li>Reduced server load</li> </ul> <p>Generated at: ${new Date().toLocaleTimeString()}</p> `, publishedAt: '2024-01-15T10:00:00Z', author: 'Tech Writer' }, // ... more posts ] export default defineEventHandler(async (event) => { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 200)) // Cache headers for ISR setResponseHeader(event, 'cache-control', 'public, s-maxage=300, stale-while-revalidate=600') return { posts: blogPosts.map(post => ({ id: post.id, slug: post.slug, title: post.title, excerpt: post.excerpt, publishedAt: post.publishedAt, updatedAt: new Date().toISOString() })), generatedAt: new Date().toISOString() } }) 
Enter fullscreen mode Exit fullscreen mode

2. Individual Blog Post API

// server/api/blog/[slug].get.ts export default defineEventHandler(async (event) => { const slug = getRouterParam(event, 'slug') // Simulate database lookup await new Promise(resolve => setTimeout(resolve, 300)) const post = blogPosts.find(p => p.slug === slug) if (!post) { throw createError({ statusCode: 404, statusMessage: 'Post not found' }) } // ISR cache headers (1 hour with stale-while-revalidate) setResponseHeader(event, 'cache-control', 'public, s-maxage=3600, stale-while-revalidate=86400') setResponseHeader(event, 'x-cache', 'MISS') return { ...post, updatedAt: new Date().toISOString(), generatedAt: new Date().toISOString() } }) 
Enter fullscreen mode Exit fullscreen mode

3. Blog Index Page with ISR Debug Info

<!-- pages/blog/index.vue --> <template> <div class="blog-index"> <h1>Blog with ISR</h1> <p class="subtitle">Demonstrating Incremental Static Regeneration</p> <div class="posts-list"> <article v-for="post in data.posts" :key="post.id" class="post-card" > <NuxtLink :to="`/blog/${post.slug}`" class="post-link"> <h2>{{ post.title }}</h2> <p class="excerpt">{{ post.excerpt }}</p> <div class="meta"> <span>Published {{ formatDate(post.publishedAt) }}</span> <span>Updated: {{ formatDate(post.updatedAt) }}</span> </div> </NuxtLink> </article> </div> <!-- ISR Debug Information --> <div class="isr-info"> <h3>πŸš€ ISR Information</h3> <p><strong>Page generated at:</strong> {{ formatDateTime(data.generatedAt) }}</p> <p><strong>Cache duration:</strong> 5 minutes (300s)</p> <p><strong>Revalidation:</strong> 10 minutes in background</p> <button @click="refresh()" class="refresh-btn"> Refresh Page </button> </div> </div> </template> <script setup> // Fetch data with ISR const { data, refresh } = await $fetch('/api/blog') // SEO meta tags useSeoMeta({ title: 'ISR Blog - Nuxt 4', description: 'Demonstrating Incremental Static Regeneration with Nuxt 4', ogTitle: 'Blog with ISR', ogDescription: 'See ISR in action' }) function formatDate(dateString) { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) } function formatDateTime(dateString) { return new Date(dateString).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) } </script> <style scoped> .blog-index { max-width: 800px; margin: 0 auto; padding: 2rem; } .posts-list { margin-bottom: 3rem; } .post-card { border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 1.5rem; transition: transform 0.2s, box-shadow 0.2s; } .post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .post-link { display: block; padding: 1.5rem; text-decoration: none; color: inherit; } .isr-info { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 12px; text-align: center; } .refresh-btn { background: rgba(255,255,255,0.2); color: white; border: 2px solid rgba(255,255,255,0.3); padding: 0.75rem 1.5rem; border-radius: 25px; cursor: pointer; transition: all 0.3s; } .refresh-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-1px); } </style> 
Enter fullscreen mode Exit fullscreen mode

4. Individual Blog Post Page

<!-- pages/blog/[slug].vue --> <template> <div class="blog-post"> <article> <header class="post-header"> <h1>{{ post.title }}</h1> <div class="post-meta"> <p>By {{ post.author }}</p> <p>Published {{ formatDate(post.publishedAt) }}</p> <p>Updated: {{ formatDate(post.updatedAt) }}</p> </div> </header> <div class="post-content" v-html="post.content"></div> <footer class="post-footer"> <NuxtLink to="/blog" class="back-link"> ← Back to blog </NuxtLink> </footer> </article> <!-- ISR Debug Panel --> <div class="isr-debug"> <h3>πŸ”§ ISR Debug Panel</h3> <div class="debug-grid"> <div class="debug-item"> <strong>Page generated:</strong> {{ formatDateTime(post.generatedAt) }} </div> <div class="debug-item"> <strong>ISR cache:</strong> 1 hour (3600s) </div> <div class="debug-item"> <strong>Stale While Revalidate:</strong> 24 hours </div> <div class="debug-item"> <strong>Slug:</strong> {{ $route.params.slug }} </div> </div> <button @click="reloadPage" class="reload-btn"> πŸ”„ Reload Page </button> </div> </div> </template> <script setup> const route = useRoute() const slug = route.params.slug // Fetch post data with ISR const { data: post } = await $fetch(`/api/blog/${slug}`) // Handle 404 if (!post) { throw createError({ statusCode: 404, statusMessage: 'Post not found' }) } // Dynamic SEO meta tags useSeoMeta({ title: post.title, description: post.excerpt, ogTitle: post.title, ogDescription: post.excerpt, ogType: 'article' }) function formatDate(dateString) { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) } function formatDateTime(dateString) { return new Date(dateString).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) } function reloadPage() { window.location.reload() } </script> 
Enter fullscreen mode Exit fullscreen mode

Advanced ISR Patterns

1. Webhook-Based Revalidation

Set up webhooks to trigger revalidation when content changes:

// server/api/webhook/revalidate.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event) // Verify webhook signature const isValid = verifyWebhookSignature(body, getHeader(event, 'x-signature')) if (!isValid) { throw createError({ statusCode: 401, statusMessage: 'Invalid signature' }) } // Clear cache for specific routes if (body.type === 'blog.updated') { const slug = body.data.slug // Clear the specific blog post cache await clearNuxtData(`blog-${slug}`) // Trigger regeneration await fetch(`${getBaseURL()}/blog/${slug}`) return { revalidated: true, slug } } return { revalidated: false } }) 
Enter fullscreen mode Exit fullscreen mode

2. Category-Based ISR

Different cache durations for different content types:

// nuxt.config.ts routeRules: { // News articles need frequent updates '/news/**': { isr: 300 }, // 5 minutes // Documentation can be cached longer '/docs/**': { isr: 3600 }, // 1 hour // Marketing pages rarely change '/features/**': { isr: 86400 }, // 24 hours // User-generated content '/profile/**': { isr: 60 }, // 1 minute } 
Enter fullscreen mode Exit fullscreen mode

3. Performance Monitoring

Track ISR effectiveness:

// plugins/isr-analytics.client.ts export default defineNuxtPlugin(() => { // Track page generation time const performance = window.performance const navigationStart = performance.timing?.navigationStart const loadComplete = performance.timing?.loadEventEnd if (navigationStart && loadComplete) { const loadTime = loadComplete - navigationStart // Send to your analytics service analytics.track('page_load', { loadTime, fromCache: document.querySelector('meta[name="x-cache"]')?.getAttribute('content'), route: useRoute().path }) } }) 
Enter fullscreen mode Exit fullscreen mode

ISR vs Traditional Caching

Edge Cases and Considerations

  1. Cold Start Performance: First visitor after cache expiry experiences slower load
  2. Cache Invalidation: Manual invalidation requires webhook setup
  3. Memory Usage: File-based caching requires disk space management
  4. Deployment Strategy: Cache persists across deployments

Best Practices

  1. Choose appropriate cache durations:

    • News/updates: 5-15 minutes
    • Blog posts: 1-6 hours
    • Documentation: 1-24 hours
    • Marketing pages: 1-7 days
  2. Implement proper error handling:

 // Always have fallbacks const { data, error } = await $fetch('/api/posts').catch(() => ({ data: null, error: true })) if (error) { // Show cached version or error state } 
Enter fullscreen mode Exit fullscreen mode
  1. Monitor cache hit rates:
 // Log cache performance setResponseHeader(event, 'x-cache-timestamp', Date.now().toString()) setResponseHeader(event, 'x-cache-ttl', '3600') 
Enter fullscreen mode Exit fullscreen mode

Deployment Considerations

Vercel

{ "functions": { "app/**": { "maxDuration": 30 } }, "isr": { "expiration": 3600 } } 
Enter fullscreen mode Exit fullscreen mode

Netlify

[build] command = "npm run build" publish = ".output/public" [[headers]] for = "/blog/*" [headers.values] Cache-Control = "public, s-maxage=3600, stale-while-revalidate=86400" 
Enter fullscreen mode Exit fullscreen mode

Cloudflare Pages

// nuxt.config.ts nitro: { preset: 'cloudflare-pages', cloudflare: { pages: { routes: { include: ['/*'], exclude: ['/api/*'] } } } } 
Enter fullscreen mode Exit fullscreen mode

Performance Comparison

I tested the same content with different rendering strategies:

Metric SSR SSG ISR
TTFB 800ms 45ms 50ms
FCP 1200ms 200ms 220ms
LCP 1500ms 400ms 450ms
Content Freshness Real-time Build-time Near real-time
Server Load High None Low

Conclusion

ISR in Nuxt 4 provides the perfect balance between performance and freshness. It's particularly powerful for:

  • Content-heavy sites (blogs, news, documentation)
  • E-commerce catalogs with frequent stock updates
  • Marketing sites with occasional content changes
  • User dashboards with semi-static data

The configuration is straightforward, the performance benefits are significant, and the developer experience is excellent. If you're building modern web applications that need both speed and fresh content, ISR should be your go-to strategy.

Top comments (0)