There’s a moment in every Next.js project where you stare at a page and ask yourself: should this be rendered on every request, or can I get away with showing a cached version? That’s the ISR versus SSR question, and it’s one of those architectural decisions that can make a massive difference in how your application feels to users.
Server-Side Rendering (SSR) fetches data on every request and sends a fully populated HTML page to the browser. Incremental Static Regeneration (ISR) pre-renders a page at build time, or on the first request, and then caches it, revalidating the cache in the background when it gets stale. They solve the same core problem—delivering dynamic content—but they sit at opposite ends of the freshness-versus-speed spectrum.
The terms get thrown around a lot, often interchangeably, which doesn't help anyone. Let's actually dig into what each one does, where they shine, where they'll let you down, and how to stop second-guessing yourself every time you create a new route.
SSR has been around since before Next.js existed, but Next.js made it feel almost effortless. You export an async function called getServerSideProps (in the Pages Router) or you just write a server component that fetches data directly (in the App Router), and Next.js handles the rest.
// App Router: server component with direct data fetching
export default async function Dashboard() {
const revenue = await fetch("https://api.example.com/revenue").then((r) =>
r.json(),
);
const users = await fetch("https://api.example.com/users").then((r) =>
r.json(),
);
return (
<div>
<h1>Dashboard</h1>
<RevenueChart data={revenue} />
<UserList users={users} />
</div>
);
}
On every request, the server runs that code, waits for the data, and sends back fully rendered HTML. The user never sees a loading spinner. The page arrives complete.
The obvious benefit is that the data is always fresh. If a user’s revenue numbers update every few seconds, they’ll see the latest value on every page load. There’s no cache to invalidate, no stale content to worry about.
The tradeoff is latency. The server has to fetch data, wait for databases, and then render React components—all before the first byte hits the browser. If your API calls take 200ms each and you need three of them, that’s at least 600ms before the server even starts sending HTML. For complex dashboards, this can add up quickly. If you’ve read about why modern websites feel slower, you already know how much perceived performance matters. Users don’t care if the delay is network, database, or rendering—they just know the page is taking too long.
ISR flips the script. Instead of rendering on every request, it renders once and caches the result. When a new request comes in, Next.js serves the cached HTML immediately, then checks if the cache is stale. If it is, it triggers a background re-render and updates the cache for the next request.
In the App Router, you use fetch with the next.revalidate option, or export a revalidate constant from your layout or page.
// App Router: ISR with revalidate
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }, // Revalidate every hour
}).then((r) => r.json());
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
The first request that hits the page triggers the initial render, which is then cached. Every subsequent request for the next hour gets that cached version instantly. When the hour is up, the next request still gets the cached version (stale-while-revalidate), but the server kicks off a regeneration in the background. The user who triggered the revalidation sees the stale page, but the next user sees the fresh one.
This is incredibly powerful for content that doesn't change moment-to-moment. Blogs, marketing pages, product descriptions, documentation—these don't need per-request rendering. They need speed.
If you've set up Next.js API routes to serve data, ISR works seamlessly with them. You can fetch from your own API routes and have them statically regenerated just like any external API.
The core tension between SSR and ISR isn't technical complexity—it's how much you care about the data being absolutely up-to-date versus how much you care about response time.
SSR guarantees freshness. Every user sees the latest data, always. But the server is doing work on every request, which costs compute, increases latency, and means your page can't be served from a CDN edge cache.
ISR guarantees speed. The cached HTML can be served from a CDN edge node, often in under 50ms. But users might see slightly stale data. If you set a revalidation time of 60 seconds, a user could see content that's up to a minute old. For a blog post, that's fine. For a stock ticker, it's useless.
There's also a hybrid approach: on-demand revalidation. You can keep a page cached indefinitely and only revalidate it when the underlying data changes—for example, when a CMS webhook fires. This gives you the speed of ISR with the freshness of SSR, provided you have a reliable way to trigger revalidation.
// On-demand revalidation from an API route
export async function POST(request) {
const { slug } = await request.json();
await revalidatePath(`/posts/${slug}`);
return Response.json({ revalidated: true });
}
This pattern is the best of both worlds. The page stays cached until a change happens, then it's instantly updated. No one ever sees stale content, and the server isn't re-rendering on a timer regardless of whether anything changed.
I worked on a content-heavy marketing site a while back that was built entirely with SSR. Every page—the blog, the pricing page, the documentation—fetched data from a headless CMS on every request. The site had decent traffic, maybe 50,000 page views a day, and it was running on Vercel's Pro plan.
The performance was okay. Not terrible, but not great. Pages took 400–600ms to load, mostly because the CMS API had inconsistent response times. During traffic spikes—like when a blog post hit the front page of Hacker News—the serverless functions would cold start and response times would balloon to over a second. Users noticed. Bounce rates went up.
We switched the blog and documentation pages to ISR with a 10-minute revalidation interval. The pricing page stayed SSR because it had user-specific A/B testing logic, but everything else went static. The change took about a day of work—mostly just adding next: { revalidate: 600 } to existing fetch calls.
The result was dramatic. Page loads dropped to under 100ms. The CDN was serving cached HTML from edge nodes, so the origin server saw almost no traffic for those pages. During the next traffic spike, the site didn't flinch. Latency stayed flat. We actually downgraded our Vercel plan and saved money because we weren't burning through serverless function executions.
The lesson wasn't that SSR is bad. It was that SSR is a tool for a specific job—pages that truly need per-request rendering. For everything else, ISR delivered a better user experience with less compute cost.
SSR isn't going anywhere, and it shouldn't. Some pages genuinely need per-request rendering.
Dashboards with real-time data are the classic case. If a logistics dashboard shows the current location of delivery trucks, caching that data for even 30 seconds is unacceptable. The user needs the freshest possible information.
Pages with user-specific content—account settings, order history, personalized recommendations—also need SSR (or client-side fetching, but that's a different discussion). You can't statically cache a page that shows different content for every user.
Pages that depend on request context also need SSR. If you're reading cookies, headers, or geolocation data to customize the response, ISR can't help you because the cache would serve the same content to everyone.
Finally, pages with rapidly changing data where a delay of even a few seconds matters—stock prices, sports scores, auction bids—need the freshness guarantee that SSR provides.
If you're building these kinds of pages and struggling with performance, you might want to look into Redis caching patterns to speed up the data fetching side without sacrificing freshness.
ISR shines for content that changes occasionally but not constantly. Blogs, marketing pages, product listings, documentation, help centers—these are perfect candidates. The content is important enough to keep fresh, but not so time-sensitive that a few minutes of staleness matters.
E-commerce product pages are the sweet spot. Product descriptions and images rarely change. Inventory levels and prices change more frequently, but most customers won't notice if the cache is a few minutes old. You can set a short revalidation window—say, 60 seconds—and get near-SSR freshness with static-level performance.
Content from a headless CMS almost always benefits from ISR. If you're using one of the best CMS options for Next.js, like Sanity or Strapi, they all support webhooks that can trigger on-demand revalidation. The CMS sends a webhook when content changes, your API route calls revalidatePath, and the cache updates instantly. Users always see fresh content, and the server only does work when something actually changes.
If you're building a site with React Server Components in Next.js, you'll find that ISR integrates naturally. Server components fetch data, and the revalidate option controls how long that data is cached. The mental model is simple: every page is statically generated by default, and you opt into revalidation where needed.
Next.js doesn't force you to pick one strategy for your entire application. You can mix SSR and ISR on a per-route basis. The blog is ISR. The dashboard is SSR. The checkout page is SSR. The marketing pages are ISR.
This per-route granularity is one of Next.js's biggest strengths. You get to decide the right rendering strategy for each page based on its specific needs, not some architectural mandate that applies to everything.
In the App Router, this is even more fine-grained. You can have a layout that uses ISR and a child page that uses SSR. Or you can use ISR for the outer shell of a page and fetch dynamic data client-side for the user-specific bits.
// Layout with ISR
export const revalidate = 3600;
export default function BlogLayout({ children }) {
return (
<div>
<Navbar />
{children}
<Footer />
</div>
);
}
// Child page with dynamic data fetched client-side
'use client';
export default function PersonalizedWidget() {
const { data } = useSWR('/api/personalized-recommendations');
return <Recommendations data={data} />;
}
The layout caches for an hour, serving the same navigation and footer to everyone. The personalized widget fetches user-specific data client-side after the page loads. This pattern gives you the speed of static caching for the common parts and the flexibility of dynamic data for the personalized parts.
Timed revalidation is fine, but it's imprecise. You're guessing how often your content changes and setting a revalidation interval accordingly. Too short, and you're re-rendering more often than necessary. Too long, and users see stale content.
On-demand revalidation solves this by tying cache invalidation to data changes. Your CMS fires a webhook when a post is updated. Your e-commerce platform fires a webhook when inventory changes. Your API route catches the webhook and calls revalidatePath or revalidateTag.
import { revalidateTag } from "next/cache";
export async function POST(request) {
const { tag } = await request.json();
revalidateTag(tag);
return Response.json({ revalidated: true });
}
This is precise. The cache is only invalidated when something actually changes. No wasted renders. No stale content. It's the pattern that makes ISR practical for content that changes unpredictably.
SSR and ISR aren't competing philosophies. They're different tools for different jobs, and the best Next.js applications use both.
SSR is for pages that need per-request freshness—dashboards, user-specific content, real-time data. It guarantees up-to-date information at the cost of higher latency and more server load.
ISR is for pages that can tolerate slight staleness—blogs, marketing pages, product listings, documentation. It delivers static-level performance with the ability to refresh content when needed.
The decision comes down to a simple question: can a user tolerate seeing data that's a few minutes old? If yes, ISR will make your site faster and cheaper to run. If no, SSR (or on-demand ISR) is the right call.
And if you're unsure, start with ISR. It's easier to add SSR to a page that needs it than to optimize an SSR page that's burning through compute without good reason.
Trying to optimize your Next.js application's performance without overcomplicating things? Red Surge Technology helps teams pick the right rendering strategies for real-world apps. Get in touch to discuss your project.