plugins/frontend-toolkit/skills/render-strategy-decision/SKILL.md
Choose the right rendering strategy per Next.js route — SSG / ISR / SSR / CSR / Streaming + Suspense — driven by data shape, not by page type. Use when adding a new route, when a route unexpectedly renders dynamically, when performance issues prompt re-evaluation, or when a code review flags inconsistency. Not for diagnosing Core Web Vitals regressions (use rendering-performance) or tuning fetch/cache policies (use api-caching-optimization).
npx skillsauth add jaykim88/claude-ai-engineering render-strategy-decisionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Make every route's rendering strategy explicit and justified by data characteristics. Document the decision so future maintainers don't have to re-derive it from cache and revalidate flags scattered in code.
Universal — the rendering-mode decision (static / incremental / server-rendered / client-only / streaming) exists in every modern meta-framework (Next.js, Nuxt, SvelteKit, Remix, Astro, Analog). Terminology and config syntax vary; the decision tree is the same.
Inventory current state
Mental model: in modern meta-frameworks, streaming + Suspense (or framework equivalent) is the substrate — static / dynamic / hybrid describe the mix within that substrate, not separate alternatives. Many frameworks now allow per-route mixing of static shell + dynamic islands.
Apply the decision tree
| Data characteristic | Strategy | |---|---| | Same for all users + changes rarely (marketing, docs) | Static (SSG) | | Same for all users + changes periodically (blog, product list) | Incremental (ISR) | | Per-user data (dashboard, profile) | Server-rendered (dynamic) | | Real-time + browser APIs (live chat, drag/drop) | Client-only island | | Mixed: shell static, slow sections async | Streaming + Suspense (or framework equivalent) |
See Implementation for the exact per-route config syntax (Next.js / Nuxt / SvelteKit / Remix / Astro).
What forces dynamic (the detection signal) — a route turns dynamic the moment it reads a request-time input: cookies, headers, the URL's search params, or an uncached / no-store fetch. This is usually implicit — one such read deep in a child component silently opts the whole route out of static. When auditing "why isn't this static?", grep for those reads.
Mostly-static + a personalized sliver (avatar, "Hi {name}", cart count) is the most common trap — don't make the whole page dynamic for it. Keep the page static and isolate the personalized part as a streamed island (PPR / Suspense) or a client-side fetch after hydration.
Between two correct options, decide by cost — static and ISR serve from cache (cheap, low TTFB); force-dynamic re-renders every request (server cost, higher TTFB). Pick the most cacheable strategy the data's freshness tolerates.
Align the data layer's caching with the route strategy
Place Suspense boundaries close to dynamic data
loading.tsx placement with the smallest fetching subtree (see async-ux-states)Set the route's strategy config explicitly
dynamic / revalidate / fetchCache in Next.js, route rules in Nuxt, prerender/ssr in SvelteKit)Document the decision per route
page.tsx: // Strategy: ISR @ 60s — product list updates hourlyVerify (validation loop)
next build, etc.); confirm each route's actual mode matches its intended strategyfetch options align with route strategy (no mismatches)dynamic, revalidate) explicitly set where it mattersdynamic / revalidate / fetchCache segment config per routepage.tsx:
// Strategy: ISR @ 60s — product list updates hourly; cache hit OK for ~5 min staleness
docs/adr/ADR-NNN-rendering-strategy.md for non-obvious choicesfind app -name 'page.tsx' -o -name 'page.ts' -o -name 'route.ts'; next build prints a per-route render-mode legend (○ Static / ● SSG / ƒ Dynamic; ISR routes show a revalidate interval) — read it to verify intent (exact glyphs vary by Next version)dynamic flag)export const revalidate = 60export const dynamic = 'force-dynamic''use client' directive, wrap minimum interactive section<Suspense> boundaries; Next.js 15+ PPR for shell + island mixingfetch(url, { cache: 'force-cache' | 'no-store' | { next: { revalidate: N } } })definePageMeta({ ssr: false, prerender: true }); useFetch + useAsyncData for data caching; Nuxt 3.10+ supports route rules per-page+page.ts prerender = true (static), ssr = false (client-only), default is dynamic SSR; data loading via load() functionsheaders() Cache-Control; no built-in ISR (use HTTP caching)export const prerender = false for SSR; islands architecture is built-in (client:load, client:visible directives)rendering-performance — when the wrong strategy is causing CWV issuesapi-caching-optimization — fetch options (cache, revalidate) must align with the strategyasync-ux-states — Suspense boundary placement is a render-strategy concernrevalidate for ISR. A single request-time read (cookies / headers / searchParams / no-store fetch) silently forces the whole route dynamic — for a mostly-static page with one personalized sliver, isolate that sliver to a streamed island rather than making the page dynamic. Place Suspense boundaries close to the dynamic data access (not at route root, and not around the LCP element) so the static shell renders instantly.development
Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).
development
Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).
development
Inventory and prioritize technical debt — TODO/FIXME/HACK, any usage, deprecated APIs, untested logic — with impact × effort matrix. Use at quarter start, before a refactoring sprint, when a new teammate joins, or when feature velocity slows. Not for actually paying down debt (use code-refactoring) or recording a migration approach (use decision-records) — this only inventories and prioritizes.
development
Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).