I Wanted Static Speed Without Serving Yesterday’s Data. ISR Was the Answer - Pt 3
Series: Part 3 of Rendering with Intent
Breaking down how I learned to choose rendering strategies intentionally rather than relying on defaults.
Static rendering works brilliantly until a page needs to stay fresh without waiting for a full redeploy. That was when I realised I needed something between fully frozen static pages and fully dynamic pages that compute on every request.
Incremental Static Regeneration (ISR) lets you cache a page like static content but regenerate it in the background on a timed schedule. It gives you the best of both worlds. Static speed for users and periodic updates without manual rebuilds.
How ISR works
Instead of choosing between rebuild-every-time or rebuild-never, ISR lets you define how long a page should stay valid before it gets refreshed. This regeneration happens in the background after the page has expired.
You control how often this happens:
export const revalidate = 300 // every 5 minutes
When ISR makes sense
| ✅ Use ISR when | ❌ Avoid ISR when |
|---|---|
| Data changes occasionally | Content changes on each request |
| Slightly stale is acceptable | Accuracy must be real-time |
| You want speed from cache | Personalised content per user |
| SEO matters | Auth or session data is required |
ISR is a time-based compromise. It assumes that showing data from a few minutes ago is usually acceptable if the trade-off is instant performance.
ISR in WonderBook - Explore page
In WonderBook, the Explore page lists public stories along with their like counts. The rankings change over time, but not rapidly enough to justify re-rendering every single request.
Originally, Explore fetched data client-side each time a user loaded the page. That meant unnecessary database calls, slower content visibility and pages that didn’t feel as responsive as they should have.
Switching to ISR with a 5-minute revalidation window meant:
| Before | After (with ISR) |
|---|---|
| Client-side fetch on every page load | HTML served instantly from cache |
| Database queried on each request | Queries reduced by ~83 percent |
| Slower content visibility | First paint felt instant |
| Required loading states and hydration | Static content visible immediately |
ISR was the perfect middle ground. It didn’t matter if two users saw slightly different rankings within a few minutes. The perceived speed boost was worth it.
Why this worked
Explore didn’t need real-time accuracy for every view. A cached version refreshed every five minutes was good enough. It improved load speed, reduced operational cost and created a smoother browsing experience.
When ISR would fail
ISR breaks down when:
- You need to reflect user-specific data such as notifications or account balances
- Content must refresh immediately after a user action
- Showing outdated content could cause confusion or mistrust
- Real-time competition is involved, such as bidding or live scoring
My rule for ISR
Static rendering gave me a freeze-until-rebuild model. ISR gave me a freeze-until-expiry model.
So I treat ISR as a timed extension of static rendering. If a page is mostly stable but needs occasional refreshing without a deploy, ISR is a strong candidate.
What ISR taught me
ISR made me think in terms of acceptable staleness. It forced me to ask whether a page really needed to execute code every time or whether I was wasting compute for marginal gains.
But there were pages where a five-minute delay would already feel too late. Some data needed to reflect the current user immediately. That’s when I moved fully into server-side rendering.
In case you missed the other posts, here they are:
The Rendering with Intent Series
- Part 1: Rendering Blindly Cost Me Speed, Money, and Sanity. Here’s How I Fixed It
- Part 2: Static Rendering. Doing the Work Before Anyone Asks For It
- Part 3: I Wanted Static Speed Without Serving Yesterday’s Data. ISR Was the Answer [You are here]
- Part 4: Switched to SSR When “Good Enough” Started Causing Real Problems
- Part 5: When Static Pages Weren’t Enough, Client Components Took Over
- Part 6: I Made Data Feel Faster Without Actually Speeding It Up