Performance No. 306Editor's pick

Rendering Strategies skill for Claude Code

Static where you can, per-request only where you must. Claude inventories every route first: data sources, how stale each may be, whether it is per-user, traffic share and page count. It then picks the most static mode the facts allow, maps it to the framework's real config, keeps personalized bits out of shared caches, and proves the plan with build output, cache-hit headers and a cross-user leak test.

  • namerendering-strategies
  • size207 lines · ≈2,600 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Chooses SSG, ISR, SSR, CSR, streaming, partial prerendering, or islands per route from data freshness, personalization, traffic, SEO, and hosting cost, then maps the choice to Next.js, Nuxt, SvelteKit, Astro, or React Router config and verifies it with cache headers and build output. Use when deciding how a page should render, a site is slow or expensive to host, pages show stale or leaked personalized data, build times explode, or when asked "should this be static or server-rendered?"

Try asking

  • Our Vercel bill doubled — which of these routes actually need server rendering?
  • Should the product pages be static, ISR or SSR? Stock changes every few minutes
  • Builds take 25 minutes because we prerender 60k pages. Fix the rendering plan

Use it when

  • Deciding how a new route or section should render
  • Hosting costs or serverless invocations are higher than traffic justifies
  • Pages show stale content or another user's data from a cache
  • Static builds have become too slow because of page count

Skip it when

  • You already chose the mode and need Next.js caching APIs wired correctly — use nextjs-app-router
  • The problem is header semantics on a CDN rather than the rendering mode — use http-caching

How to check the result

Build output shows each route in its planned mode, a second curl to cached routes returns a cache HIT without Set-Cookie, and personalized routes return private or no-store.

The common mistake it prevents

Reading cookies in a shared layout to show a greeting, which silently turns every page under it into uncached per-request rendering.

~/.claude/skills/rendering-strategies/SKILL.md

--- name: rendering-strategies description: Chooses SSG, ISR, SSR, CSR, streaming, partial prerendering, or islands per route from data freshness, personalization, traffic, SEO, and… license: MIT ---

A route is rendered well when its mode was chosen from written facts — how fresh the data must be, whose data it is, how much traffic it gets, whether crawlers need it — and the deployed response proves the choice (cache headers, build route table, TTFB). Default to the most static mode the data allows; pay for per-request rendering only where freshness or personalization demands it.

Step 1 — inventory routes before choosing anything

Build this table from the router, analytics, and data layer. Never pick a mode for the whole app at once; different routes have different answers.

RouteData sourcesFreshness tolerancePer-user?Traffic shareCrawled?Page count
/CMS hero, pricing1 hno30%yes1
/blog/[slug]CMSminutes after publishno25%yes4,000
/products/[id]catalog + stockprice 5 min, stock liveno20%yes80,000
/dashboarduser APIliveyes15%no—
/searchsearch API + query stringliveno10%partially∞

Commands that help:

# Next.js: current route modes (○ static, ● SSG, ƒ dynamic, ◐ partial prerender)
npx next build 2>&1 | sed -n '/Route (app)/,/^$/p'
# What headers a deployed route actually sends
curl -sI https://example.com/blog/hello | grep -iE 'cache-control|age|x-vercel-cache|cf-cache-status|x-cache|vary|set-cookie'

Step 2 — decide per route

Facts about the routeModeWhy
Same for everyone, changes only on deploySSG (prerender at build)Zero compute per request, CDN-served, best TTFB
Same for everyone, changes on content edits, < ~5k pagesSSG + on-demand revalidation (webhook → revalidate tag/path)Fresh within seconds of publish, still static
Same for everyone, large page count or unpredictable paramsISR / stale-while-revalidate (render on first request, cache, refresh in background)Build time stays flat; long tail rendered lazily
Same for everyone except one small live part (stock, price, cart count)Static shell + dynamic hole: partial prerendering, server island, or client fetch for that partKeep the page cacheable; only the hole is per-request
Depends on the request (cookie, auth, geo) and must be crawlable or fast first paintSSR, streamed with Suspense around slow dataFresh HTML; stream so TTFB is not hostage to the slowest query
Behind login, highly interactive, not crawledCSR or SSR shell + client dataSEO irrelevant; avoid paying server render for app-like screens
Infinite query space (search, filters)SSR with short CDN TTL keyed on normalized query, or CSRCannot prerender; cache popular queries only
Mostly static content with a few interactive widgetsIslands (Astro, server islands)Ship JS only for the widgets

Tie-breakers:

  • Freshness tolerance ≥ 60 s and not per-user → it can be cached; choose ISR/SWR over SSR.
  • More than ~10k prerendered pages or builds over ~10 minutes → stop prerendering the long tail; prerender the top pages by traffic and ISR the rest.
  • SSR p75 TTFB above ~600 ms on a cacheable route → it should not be SSR.
  • A route that reads cookies only to show “Hi, Ana” in the header → keep the page static and fetch the greeting client-side or as a dynamic hole.

Step 3 — map to the framework

Check the installed version first (npm ls next nuxt @sveltejs/kit astro react-router); these APIs moved between majors.

Next.js (App Router)

ModeNext 16 with cacheComponents: trueNext 15 / 16 without it
SSG"use cache" on data + generateStaticParamsno dynamic APIs; generateStaticParams
ISRcacheLife("hours") + cacheTag("posts")export const revalidate = 3600 or fetch(url, { next: { revalidate, tags } })
On-demandrevalidateTag("posts", "max") / updateTag in Server ActionsrevalidateTag("posts") / revalidatePath
Dynamic holeuncached component under <Suspense> (partial prerendering by default)<Suspense> + dynamic route, or client fetch
SSRreading cookies()/headers() under <Suspense>same; any dynamic API makes the route ƒ

Deeper Next caching rules belong to nextjs-app-router.

Nuxt 3/4 — routeRules in nuxt.config.ts

export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },
    "/blog/**": { isr: 600 },           // cached on supporting hosts, refreshed every 10 min
    "/products/**": { swr: 300 },       // stale-while-revalidate via Nitro cache
    "/dashboard/**": { ssr: false },    // client-rendered SPA section
    "/api/catalog/**": { cache: { maxAge: 60 } },
  },
});

isr relies on platform support (Vercel, Netlify); swr uses Nitro’s own cache storage.

SvelteKit — page options in +page.ts / +layout.ts

export const prerender = true;    // or "auto" to prerender but keep SSR fallback
export const ssr = false;         // CSR only (dashboard)
export const csr = false;         // static HTML with zero JS (legal pages)

ISR is adapter-specific (e.g. config.isr with adapter-vercel). Use setHeaders in load for CDN caching elsewhere.

Astro 5

output: "static" (default) prerenders everything; add an adapter and export const prerender = false on pages that must render per request. Use server:defer on a component for a server island inside a static page.

React Router v7 framework mode

// react-router.config.ts
export default {
  ssr: true,
  async prerender() { return ["/", "/about", ...(await getTopPostPaths(500))]; },
} satisfies Config;

ssr: false gives SPA mode. For CDN caching of SSR routes, export headers() from the route module returning Cache-Control.

Anything else

SSR + CDN with explicit headers is a valid ISR equivalent:

Cache-Control: public, s-maxage=600, stale-while-revalidate=86400

Header design details (Vary, purge, surrogate keys) live in http-caching.

Personalization without losing the cache

Ranked from cheapest to most expensive:

  1. Static page, personalized bits fetched client-side after load (greeting, cart badge).
  2. Static shell + server-rendered hole (PPR, server islands, edge include).
  3. A small, fixed set of variants (locale, A/B arm, logged-in vs out) chosen in middleware/proxy by rewriting to a prerendered variant path; cache key = variant.
  4. Full SSR per request, Cache-Control: private, no-store.

Never cache HTML that contains user data in a shared cache. A response with Set-Cookie or a session-derived body must carry private or no-store.

Streaming rules

  • Stream SSR when one data source is slow (> ~200 ms) and the rest of the page can paint without it. Wrap only the slow component in Suspense with a same-size skeleton.
  • Status code and headers are sent with the first byte. Decide 404/redirect/auth before the first Suspense boundary flushes; a notFound() thrown inside a streamed boundary can produce a 200 with an error UI.
  • Crawlers receive the complete streamed HTML; streaming does not hurt SEO.
  • Buffering proxies (some Nginx configs, compression middleware) silently disable streaming. Check with curl -N --no-buffer and watch chunks arrive over time.

Gotchas

  • One cookies()/headers()/searchParams read in a shared layout turns every child route dynamic. Push request-dependent reads to the leaf that needs them.
  • Vary: Cookie on a CDN means one cache entry per user — effectively no cache.
  • ISR serves stale content to the first visitor after expiry. If “stale for one request” is unacceptable (prices at checkout), that data must be live, not ISR.
  • On-demand revalidation without a webhook from the CMS means content waits for the TTL. Wire the CMS publish event to a revalidate endpoint protected by a secret.
  • SSG of pages that call authenticated APIs at build time bakes one identity’s data into public HTML. Audit build-time fetches for tokens and user scope.
  • SSR does not fix slow interactivity; hydration cost (INP) depends on client JS, not on where HTML was made. Islands or server components reduce it; SSR alone does not.
  • CSR for crawlable content: Google renders JS eventually, other crawlers and link previews often do not. Marketing, docs, and product pages need HTML in the response.
  • Serverless SSR adds cold-start latency (100 ms – 1 s+) to p95. Factor it into TTFB budgets.
  • Preview/draft mode must bypass caches; test that drafts never land in the public cache.

Cost check

For each SSR route estimate: requests/month × avg render ms × price per GB-s. Compare with the same route cached at a 95% CDN hit rate. A route with > 1M monthly views and a freshness tolerance over a minute should almost never be uncached SSR.

Verification

  1. Build output: every route’s mode matches the plan (Next route table symbols, Nuxt prerendered list, Astro build log, SvelteKit .svelte-kit/output/prerendered).
  2. Deployed headers: curl -sI twice; the second response shows a cache hit (age > 0, x-vercel-cache: HIT, cf-cache-status: HIT) for cached routes and private/no-store for personalized ones. No cached response contains Set-Cookie.
  3. Freshness: publish a CMS change and time how long until the live page reflects it; compare with the tolerance in the inventory.
  4. Leak test: load a personalized route as user A, then as user B and logged out; no cross-user content appears.
  5. Speed: TTFB and LCP per route from RUM or WebPageTest before and after (see core-web-vitals and ttfb-optimization).
  6. View source (not DevTools Elements) on crawlable routes: the main content is in the HTML.

Output — rendering plan

| Route | Mode | Freshness mechanism | Cache headers | Personalized parts |
|---|---|---|---|---|
| / | SSG | redeploy + CMS webhook → revalidateTag("home") | s-maxage=31536000 | none |
| /blog/[slug] | SSG top 500 + ISR rest | cacheTag("posts"), webhook | s-maxage=600, swr=86400 | none |
| /products/[id] | static shell + dynamic stock hole | cacheLife("minutes") | page cached; stock no-store | stock badge |
| /dashboard | CSR (SSR shell) | live | private, no-store | everything |

**Expected effect:** SSR invocations −80%, p75 TTFB on /blog from 720 ms to < 100 ms.
**Verified:** route table matches; HIT on second request; no Set-Cookie on cached routes.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc