Static vs SSR is the decision that quietly determines your hosting cost, your time-to-first-byte, your Core Web Vitals, and how often you wake up to a 500 error. For a content site the answer is almost always “static, with a sprinkle of dynamic where it matters”. This walks through how to actually decide.
Background
Modern Next.js (App Router) blurs the lines: a single route can be static, ISR-cached, dynamic SSR, or streamed. That flexibility is a feature, but it lets people accidentally opt into SSR-by-default because they forgot a generateStaticParams or used cookies() somewhere. For a content site that is a self-inflicted regression.
What “static” / “ISR” / “SSR” look like in code
Next.js App Router, forced static — what you almost always want for an article page:
// app/articles/[slug]/page.tsx
export const dynamic = 'force-static';
export const revalidate = false;
export async function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
export default async function Article({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Markdown source={post.body} />;
}
ISR — same shape but content refreshes every N seconds without redeploying:
export const revalidate = 300; // re-render at most once every 5 minutes
SSR — only when the page genuinely depends on the request:
export const dynamic = 'force-dynamic';
// implies cookies()/headers() use; every request hits the server
Astro is static by default; opt in to SSR per-route only when needed:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static', // global default
// output: 'hybrid', // per-route opt-in to SSR
// adapter: import('@astrojs/node').default(), // only if you actually need it
});
How to tell
- Content rarely changes per request — same article served to everyone? Static.
- Content is per-user or per-region — login-gated dashboard, geo offers? SSR.
- You want CDN-cheap hosting with predictable latency? Static.
- You need to fetch a freshly updated price / inventory / score on every render? SSR or ISR with short revalidate.
- You are deploying to Cloudflare / Firebase / S3 / GitHub Pages? Static (these do not run Node per request).
Quick verdict
Default to static for content. Reach for SSR only where the page genuinely depends on per-request data. ISR (revalidate every N seconds) is the middle ground for “mostly static, updates sometimes” pages and is almost always what people actually want when they reach for SSR.
Step by step
- List every route. Mark each as: static, ISR (with revalidate window), SSR, or client-rendered.
- For each SSR mark, ask “what changes between two anonymous visitors in the same minute?” If nothing — it should be static.
- For each ISR mark, pick a revalidate window. 60s is fine for most blogs. Pricing pages may want 300s. Almost nothing needs 1s.
- In Next.js App Router, make sure routes are not accidentally opting into dynamic — set
dynamic = 'force-static'where you want guarantees. - For RSC fetches, set
cache: 'force-cache'(default) ornext: { revalidate: N }deliberately rather than leaving it implicit. - Deploy and check the build output — Next.js logs which routes are
Static,SSG, orDynamic. Audit it. - Run Lighthouse against the deployed static pages. If TTFB and LCP are clean, you have made the right call.
Common pitfalls
- Accidentally going dynamic — one
cookies()orheaders()call in a layout will mark the whole subtree dynamic. - Setting ISR revalidate too aggressively (every 10s) and burning function invocations for content that changes daily.
- Picking SSR “just in case I need it later” — premature flexibility is the same as premature optimization, you pay for it now in cold starts and bills.
- Forgetting to set
dynamic = 'force-static'on routes that must be CDN-cacheable for performance commitments. - Trying to do SSR on a host that does not run Node (Cloudflare Pages without Workers, plain S3, GitHub Pages) and not understanding why it fails.
Who this is for
Indie content sites — blogs, docs, niche directories, course sites — where 95%+ of traffic is the same HTML for everyone.
When to skip this
Apps with login walls, personalized feeds, or content tied to the requester (auth, geo, A/B) — those genuinely need SSR or client-rendered.
FAQ
- Is ISR just cached SSR?: Effectively yes — Next.js builds the page on the first request after the revalidate window expires, then serves the cached HTML to everyone else until the next expiry.
- Does static break if I have a CMS?: No. Build-time fetches from Contentful, Sanity, or Notion are still static. Trigger a rebuild on publish (via webhook) and you have the best of both worlds.
- What about search?: Client-side search against a static JSON index handles most content sites up to thousands of posts. Only reach for SSR search if you need server-side filtering at scale.
- Can I mix static and SSR in one Next.js project?: Yes, that is the whole point of App Router. Each route picks its own strategy.
Related
- Next.js App Router — the Concepts You Actually Need
- When Next.js Is the Right Pick for Your Website
- Next.js Content-Site SEO: The Things That Bite
- When Next.js Is the Wrong Choice for a Content Site
Tags: #Indie dev #Next.js #Website planning #Comparison #Core Web Vitals