The Next.js App Router supports four rendering modes per route: fully static (SSG), incremental static regeneration (ISR), server-side rendering on every request (SSR), and edge runtime. Most teams pick one and use it everywhere. That’s the wrong move — different routes have different freshness, traffic, and personalization needs, and the rendering choice should match.
What each mode actually does
- SSG: HTML is generated at build time and served as a static file from the CDN. Fastest possible delivery, zero per-request server cost, but the content is frozen until the next build.
- ISR: SSG plus a `revalidate` window. The page is served from the static cache until it’s N seconds old, then the next request triggers a background regeneration. Stale-while-revalidate semantics — fast and reasonably fresh.
- SSR: HTML is generated on every request. Always fresh, always pays a server round-trip per visit, scales with your traffic.
- Edge: SSR but the function runs at a Cloudflare/Vercel edge node near the user instead of in a central region. Lower latency, fewer features (no Node APIs, smaller package size limit).
Default: SSG for marketing, ISR for blog, SSR for authenticated routes
That’s the FH default and it covers 80% of cases without thinking. Your homepage, your services pages, your locations pages — all SSG. They change rarely, traffic loves the CDN-hit, and the build cost is paid once. Your blog index and post pages — ISR with a 60-minute or 24-hour `revalidate`, so new posts and edits propagate without a full deploy. Anything behind auth (admin, dashboard, account) — SSR, because the content is per-user and shouldn’t be cached at the edge.
How to set each mode
Route-segment config in the file itself. Export `revalidate` (number of seconds) for ISR, `dynamic = 'force-static'` for SSG, `dynamic = 'force-dynamic'` for SSR, `runtime = 'edge'` for edge.
// app/page.tsx — SSG (default behavior with no data fetches)
export const dynamic = "force-static";
// app/blog/page.tsx — ISR, 1 hour
export const revalidate = 3600;
// app/admin/page.tsx — SSR
export const dynamic = "force-dynamic";
// app/api/proxy/route.ts — Edge
export const runtime = "edge";When ISR earns its keep
ISR is the sweet spot for content that changes occasionally but reads constantly. Blog posts are the canonical case — once published, they’re read thousands of times but maybe edited once a month. A 24-hour `revalidate` window means a single regeneration per day per page, and every other request hits the cache. We use ISR on every FH client blog.
It also works for category pages, listing pages, and any “data view” where the source updates a few times a day. Pair ISR with `revalidatePath()` or `revalidateTag()` in server actions, and you can trigger a regeneration on demand when content changes — best of both worlds.
When SSR is actually necessary
Three signals: (1) the content is per-user (a dashboard, an account page); (2) the content depends on request-time data like a cookie or auth header; (3) the content needs to be fresh down to the second (a stock ticker, a sports score). Anything else is over-rendering — you’re paying per-request server cost for content that could be cached.
Edge runtime: low-latency reads, but fewer features
Edge functions run on Cloudflare’s network and start in single-digit milliseconds. Latency from a remote user is a fraction of what a centralized Node function would be. The trade-off: no Node-only APIs (no `fs`, no `child_process`, no most-of-`node:` modules), smaller bundle size limit, fewer NPM packages work. We use edge for routes that are read-heavy and don’t touch the database directly — geolocation-based redirects, A/B test variant selection, Turnstile token verification.
Mixed-mode in the same app
Different routes can have different modes. The same Next.js app can serve a SSG homepage, ISR blog posts, SSR admin dashboard, and an edge API route. Next handles the routing and the build correctly. You don’t need separate deploys or separate hostnames.
Common mistakes we see in audits
- Marketing pages set to SSR by default because someone enabled `dynamic = 'force-dynamic'` at the layout level. Every page now hits the server on every visit. Bandwidth bill goes up, page-load time goes up.
- Blog post pages set to SSG with no ISR, so new posts don’t appear until the next deploy. Editorial team has to redeploy to publish. Friction kills cadence.
- Admin pages set to SSG with auth, so the static page caches one user’s session and serves it to everyone. Real production incident we’ve cleaned up twice.
- Edge runtime set on a route that imports a Node-only library, so the build silently falls back to Node and you don’t get the latency win.
Picking per-route in 30 seconds
- Is the content per-user? → SSR.
- Does it need to be fresh down to the second? → SSR.
- Does it change a few times a day or week? → ISR.
- Does it change with each deploy? → SSG.
- Is it a read-heavy proxy with no DB? → Edge.
Measuring the impact
Set up a simple TTFB monitor — we use Cloudflare’s built-in analytics on every FH client site — and compare TTFB across rendering modes on the same site. An SSG page should serve in under 80ms TTFB from the CDN. An ISR cache-hit should be the same. An ISR cache-miss should be under 600ms. SSR should be under 800ms. Anything above those is a signal something’s wrong — usually a misconfigured cache or an unexpected database call on a route that should be static.
How this maps to FH client work
Across the client book, every site has the same shape: SSG for marketing pages, ISR for blog content, SSR for any authenticated area. The split isn’t exotic — it’s the boring default that performs well, scales cheaply, and stays maintainable. If your site is rendering the same mode for everything, book an audit — we can usually identify three or four routes that should change mode in the first hour.