Static vs SSR for a Content Site: How to Pick (2026)

Static and SSR look interchangeable on a Next.js or Astro project until you scale, ship, or read the bill. Here is how to pick correctly for a content site, with real 2026 pricing and config.

Static vs SSR is the decision that quietly determines your hosting bill, 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 genuinely matters.” This walks through how to actually decide, what the config looks like in current Next.js and Astro, and what each path costs in June 2026.

TL;DR

  • Default to static for anything that serves the same HTML to every anonymous visitor (articles, docs, marketing pages).
  • ISR (revalidate every N seconds) is the middle ground for “mostly static, refreshes sometimes.” It is what most people actually want when they reach for SSR.
  • SSR only where the response genuinely depends on the request: login state, geo, A/B assignment, or live data that must be fresh on every render.
  • Static assets are free and unmetered on Cloudflare, Firebase Hosting, GitHub Pages, and S3. SSR puts a Node/Worker runtime in the request path and turns those free pages into billable function invocations.

The three modes, side by side

ModeRenderedServed fromFreshnessCost shape
Static (SSG)At build timeCDN edge, no serverUntil next buildCheapest; usually free egress
ISRAt build, re-rendered after a timerCDN edge + occasional buildUp to revalidate seconds staleMostly CDN + a trickle of invocations
SSRPer requestNode/Worker functionAlways freshPay per request; cold starts

For a content site, 90%+ of routes belong in the first row.

What each mode looks like in code

Next.js 15 (App Router)

In Next.js 15, route segments are no longer cached by default — the default dynamic = 'auto' renders static when it can and dynamic when it must. For an article page you usually want to remove the guesswork and force static:

// 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 is the same shape, but content refreshes on a timer without a redeploy. The smallest revalidate among the page’s fetches wins:

export const revalidate = 300;  // re-render at most once every 5 minutes

SSR — only when the page truly depends on the request. Calling cookies() or headers() opts the segment in automatically, so set it explicitly when you mean it:

export const dynamic = 'force-dynamic';  // every request hits the function

In Next.js 15 a server fetch() is no longer cached implicitly the way it was in 14, so be deliberate: pass { cache: 'force-cache' } to cache, or { next: { revalidate: N } } for timed revalidation. Leaving it implicit is how routes silently go dynamic.

Astro 5

Astro is static by default. Note the change in Astro 5: the old output: 'hybrid' mode was removed and merged into static. You no longer flip a config flag for per-route SSR — you just add an adapter, and any route that needs on-demand rendering exports prerender = false:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'static',                    // the default; renders to plain HTML
  adapter: node({ mode: 'standalone' }), // add ONLY if some route needs SSR
});
---
// src/pages/dashboard.astro — opt this single route into SSR
export const prerender = false;
---

If no route needs a server, drop the adapter entirely and ship pure static files. (This site runs that way.)

How to tell which one a route needs

Ask one question per route: what changes between two anonymous visitors in the same minute?

  • Nothing changes (same article, same docs page) — Static.
  • Per-user or per-region (login-gated dashboard, geo offers, cart) — SSR.
  • You want CDN-cheap hosting with predictable latency — Static.
  • Live data that must be current on every render (price, inventory, score) — SSR, or ISR with a short revalidate.
  • Deploying to Cloudflare Pages, Firebase Hosting, GitHub Pages, or S3 — Static. These serve files; SSR needs a Worker/function and changes the cost and failure model.

What it actually costs (June 2026)

This is where the choice stops being academic.

HostStaticSSR / functions
CloudflareStatic assets free and unmetered on every planWorkers/Pages Functions free tier: 100K requests/day, 30ms CPU each; then Workers paid pricing
Firebase HostingSpark plan free, commercial use allowedCloud Functions require the Blaze pay-as-you-go plan
VercelHobby free but non-commercial only; 100 GB transfer, 4 hrs active CPUHobby: 1M function invocations/mo; Pro $20/seat, 1 TB transfer, then $0.15/GB
GitHub Pages / S3Free / near-free staticNo server runtime — SSR not possible

Two things bite people: Vercel’s free Hobby plan is non-commercial only, so a content site running ads or affiliate links technically needs Pro at $20/seat/month (Vercel pricing). And on Cloudflare, requests to static assets are free and unlimited, but the moment a request hits a Function (SSR) it counts against the 100K/day free quota (Cloudflare Pages pricing). Going dynamic by accident is a real bill, not a hypothetical.

Why static wins on Core Web Vitals

Google’s “good” thresholds (75th percentile of real CrUX field data, as of June 2026) are LCP under 2.5 s, INP under 200 ms, CLS under 0.1. Prebuilt HTML served from a CDN edge gives you a near-zero TTFB, which is the single biggest lever on LCP. SSR adds a runtime hop and cold-start variance to every uncached request, which is exactly the wrong place to spend your LCP budget. INP is now the most commonly failed metric — roughly 43% of sites miss the 200 ms mark — and that is a client-side JavaScript problem, not a rendering-mode one, so going SSR does not help it.

Step by step: audit your routes

  1. List every route. Mark each: static, ISR (with a revalidate window), SSR, or client-rendered.
  2. For each SSR mark, ask the “two anonymous visitors in the same minute” question. If nothing changes, it should be static.
  3. For each ISR mark, pick a revalidate window. 60 s is fine for most blogs; a pricing page might want 300 s; almost nothing needs 1 s.
  4. In Next.js, set dynamic = 'force-static' where you need a guarantee, so a stray cookies() call in a layout can’t silently flip the subtree to dynamic.
  5. For each server fetch(), set cache / next.revalidate explicitly instead of relying on a default that changed between Next.js 14 and 15.
  6. Build and read the output. Next.js labels each route Static, SSG, or Dynamic; audit the list and chase down anything Dynamic that shouldn’t be.
  7. Run Lighthouse against the deployed pages. Clean TTFB and LCP confirm you chose right.

Common pitfalls

  • Accidentally going dynamic. One cookies() or headers() call in a shared layout marks the whole subtree dynamic in Next.js.
  • ISR revalidate set too aggressively. A 10 s window on daily-changing content burns function invocations for no benefit.
  • Picking SSR “just in case.” Premature flexibility is premature optimization — you pay for it now in cold starts and bills.
  • Forgetting dynamic = 'force-static' on routes that must stay CDN-cacheable.
  • Trying to SSR on a host that serves files only (GitHub Pages, plain S3, Cloudflare Pages with no Function) and not understanding why it fails.
  • Leaving Astro on an adapter you don’t need — that ships a server runtime for a site that could be pure static.

Who this is for

Indie content sites — blogs, docs, niche directories, course sites — where 95%+ of traffic is the same HTML for everyone.

When SSR is the right call

Apps with login walls, personalized feeds, carts, or content tied to the requester (auth, geo, A/B). Those genuinely need SSR or client rendering, and forcing them static will break them.

FAQ

  • Is ISR just cached SSR?: Effectively, yes. Next.js rebuilds the page on the first request after the revalidate window expires, then serves the cached HTML to everyone until the next expiry. You get fresh-ish content with CDN economics.
  • Does static break if I use a CMS?: No. Build-time fetches from Contentful, Sanity, or Notion are still static. Trigger a rebuild on publish via webhook and you keep both the freshness and the CDN cost.
  • Did Astro remove hybrid mode?: Yes. As of Astro 5, output: 'hybrid' is gone and folded into output: 'static'. Add an adapter and set prerender = false on the specific routes that need a server; everything else stays static.
  • What about search?: A client-side search over a static JSON index handles most content sites up to several thousand posts. Only reach for server-side search when you need filtering at scale.
  • Can I mix static and SSR in one project?: Yes — that is the whole point of both Next.js App Router and Astro adapters. Each route picks its own strategy.

Tags: #Indie dev #Next.js #Website planning #Comparison #Core Web Vitals