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.
- name
rendering-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.
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.
| Route | Data sources | Freshness tolerance | Per-user? | Traffic share | Crawled? | Page count |
|---|---|---|---|---|---|---|
/ | CMS hero, pricing | 1 h | no | 30% | yes | 1 |
/blog/[slug] | CMS | minutes after publish | no | 25% | yes | 4,000 |
/products/[id] | catalog + stock | price 5 min, stock live | no | 20% | yes | 80,000 |
/dashboard | user API | live | yes | 15% | no | — |
/search | search API + query string | live | no | 10% | 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 route | Mode | Why |
|---|---|---|
| Same for everyone, changes only on deploy | SSG (prerender at build) | Zero compute per request, CDN-served, best TTFB |
| Same for everyone, changes on content edits, < ~5k pages | SSG + on-demand revalidation (webhook → revalidate tag/path) | Fresh within seconds of publish, still static |
| Same for everyone, large page count or unpredictable params | ISR / 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 part | Keep the page cacheable; only the hole is per-request |
| Depends on the request (cookie, auth, geo) and must be crawlable or fast first paint | SSR, streamed with Suspense around slow data | Fresh HTML; stream so TTFB is not hostage to the slowest query |
| Behind login, highly interactive, not crawled | CSR or SSR shell + client data | SEO irrelevant; avoid paying server render for app-like screens |
| Infinite query space (search, filters) | SSR with short CDN TTL keyed on normalized query, or CSR | Cannot prerender; cache popular queries only |
| Mostly static content with a few interactive widgets | Islands (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)
| Mode | Next 16 with cacheComponents: true | Next 15 / 16 without it |
|---|---|---|
| SSG | "use cache" on data + generateStaticParams | no dynamic APIs; generateStaticParams |
| ISR | cacheLife("hours") + cacheTag("posts") | export const revalidate = 3600 or fetch(url, { next: { revalidate, tags } }) |
| On-demand | revalidateTag("posts", "max") / updateTag in Server Actions | revalidateTag("posts") / revalidatePath |
| Dynamic hole | uncached component under <Suspense> (partial prerendering by default) | <Suspense> + dynamic route, or client fetch |
| SSR | reading 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:
- Static page, personalized bits fetched client-side after load (greeting, cart badge).
- Static shell + server-rendered hole (PPR, server islands, edge include).
- 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.
- 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-bufferand watch chunks arrive over time.
Gotchas
- One
cookies()/headers()/searchParamsread in a shared layout turns every child route dynamic. Push request-dependent reads to the leaf that needs them. Vary: Cookieon 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
- Build output: every route’s mode matches the plan (Next route table symbols, Nuxt
prerenderedlist, Astro build log, SvelteKit.svelte-kit/output/prerendered). - Deployed headers:
curl -sItwice; the second response shows a cache hit (age> 0,x-vercel-cache: HIT,cf-cache-status: HIT) for cached routes andprivate/no-storefor personalized ones. No cached response containsSet-Cookie. - Freshness: publish a CMS change and time how long until the live page reflects it; compare with the tolerance in the inventory.
- Leak test: load a personalized route as user A, then as user B and logged out; no cross-user content appears.
- Speed: TTFB and LCP per route from RUM or WebPageTest before and after (see
core-web-vitalsandttfb-optimization). - 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.