What you'll learn: By the end of this lesson you will be able to choose a rendering strategy for each page of a product with five questions instead of a favourite, express "how fresh must it be" as a freshness budget and map it to a mechanism, split one page into a shared shell plus personal and live parts, defend the choice for seven common page types, and recognise the early signs that a page is on the wrong strategy.
An airport has three kinds of information. The printed brochure rarely changes. The departures board changes every few minutes and everyone reads the same one. Your boarding pass is yours alone. Nobody argues about which medium the whole airport should use: each kind of information gets the medium that fits how it changes and who reads it.
Web pages are the same. Rendering Strategies: CSR, SSR, SSG, ISR and Streaming defines each strategy and gives a first flowchart for a store. This lesson goes one step further: a repeatable way to decide, page by page and part by part, and to notice when you decided wrongly. Its running example is Gigline, a fictional event-ticketing site (an invented product, so all numbers are illustrative) with a landing page, a help centre, event pages, search results, "my tickets", an organiser dashboard and a checkout.
Framework names move. The framework-specific behaviour below comes from the Next.js documentation for the 16.x line as read on 2026-11-08 (16.4 was the latest release in VERSIONS.md); other frameworks use other names for the same ideas.
The core idea
Choose a strategy for the shell of each page and for each hole in it, not for the site. One guideline does most of the work: pick the most static option that still meets the page's freshness budget and its personalisation needs. Static output is typically the fastest and most resilient to serve, so move away from it only when a constraint forces you to.
Figure 1. One page, four lifetimes
How to read this: the top two parts can be produced ahead of time and served as files, the bottom two depend on the visitor or the moment. Cutting a page along these lines is what lets most of it stay static.
The five questions
Ask them in this order, because the early ones are hard constraints and the later ones are tuning.
- Must the first HTML contain the content? Google says it queues pages for rendering with a headless Chromium and that server-side or pre-rendering "is still a great idea", partly because not every bot runs JavaScript. Next.js detects HTML-limited bots and, with Cache Components, renders the page dynamically for them so the metadata is in the document head. If the page is public and should be found or previewed, yes. If it sits behind a login, no.
- Is it the same for every visitor? web.dev names personalisation as the type of request that does not work well with static rendering. A shared cache can only hold what everyone may see.
- What is the freshness budget? How old may the content be before someone is harmed, expressed as a number the business agrees to.
- How many URLs, and can you list them? web.dev notes that static rendering needs an HTML file per URL, which can be challenging or even infeasible when you cannot predict the URLs.
- What should happen when the data source is down? A pre-rendered page keeps working. A request-time render can fail along with its source unless you handle the error.
Figure 2. The decision, in constraint order
not indexed?"} L -->|"yes"| C["CSR, or SSR if first paint matters"] L -->|"no"| V{"Same for
every visitor?"} V -->|"no"| H["Split: shared shell +
personal hole (stream or fetch)"] V -->|"yes"| B{"Freshness
budget?"} H --> B B -->|"a deploy or longer"| G["SSG"] B -->|"minutes to hours"| U{"URLs listable?"} U -->|"yes"| I["ISR or cached fragment,
on-demand invalidation"] U -->|"no"| D["Generate on first request,
then cache (ISR on demand)"] B -->|"seconds"| R["Request-time render, or a
small live fetch into a static page"]
How to read this: login and personalisation come first because they rule options out entirely. The freshness budget comes next and chooses among what remains.
The freshness budget
Write the budget as a sentence with a number: "an event's description may be up to 10 minutes old; seat availability up to 5 seconds." Then map it (illustrative values):
| Budget | Typical mechanism | Who pays |
|---|---|---|
| Until the next deploy | SSG: files built once | Build time grows with page count |
| Minutes to hours, known change events | ISR with time-based plus on-demand revalidation | A regeneration per page per window, and a way to call invalidation |
| Seconds | Request-time render, or a tiny uncached fetch | Origin load per visit |
| Per user | Never in a shared cache | Origin or client work per visitor |
The Next.js ISR guide documents the mechanism: after the revalidate window the next request still receives the stale page, regeneration starts in the background, and the request after it gets the new page. It recommends a high revalidate time (one hour rather than one second) and on-demand revalidation (revalidatePath, revalidateTag) when you need precision, and real-time data should use dynamic rendering. So a page can be older than its revalidate value, which the budget must allow for.
Figure 3. Stale-while-revalidate for one page
How to read this: 60 seconds is illustrative. The visitor who triggers regeneration is not the one who sees the result, so budget for one extra stale response.
Splitting a page into a shell and holes
Most pages fail a single-strategy test because one part is personal or live. Cut the page instead. In the Next.js 16.x docs, Cache Components does this: static content and results cached with use cache become part of a prerendered static shell, while reads of cookies(), headers(), searchParams or uncached data go behind <Suspense> and stream at request time. The docs call this approach Partial Prerendering and note that reading cookies() no longer opts the whole route into dynamic rendering, "the way the previous rendering model did". Check which model your app uses.
Three rules follow, each from the docs:
- Push dynamic access down. If a layout awaits
cookies()orparamsat the top, everything under it becomes request-time. Resolve them in the small component that needs them. - Keep the LCP element out of boundaries. An element inside a Suspense boundary cannot paint until its content is swapped in.
- Decide status codes before streaming. The
200is committed with the first chunk, so a latenotFound()cannot become a 404 (Next.js addsnoindexinstead). Check existence before the boundary.
Worked examples: Gigline
Illustration 1, the minimal example: two pages, five questions
A refund-policy article: public and indexed, identical for everyone, a budget of "a day", 2,000 articles that you can list, and source data that is a file. Answers: SSG. The "my tickets" page: behind login, per user, budget of seconds, not indexed. Answer: CSR, or an SSR shell if first paint matters. Same questions, opposite results.
The seven pages
| Page | Shared? | Budget | URLs | Choice and reason |
|---|---|---|---|---|
| Landing page | yes | a deploy | 1 | SSG. A greeting is a streamed or client hole. |
| Help article | yes | a day | 2,000, listable | SSG, rebuilt on content changes. |
| Event detail | shell yes | description minutes, seats seconds | 60,000, changing | ISR for the shell, seats as a live hole. |
| Search results | yes per query | minutes | unbounded | Request-time, cached per query, not pre-rendered. |
| My tickets | no | seconds | one route | CSR behind login, or SSR with no shared cache. |
| Organiser dashboard | no | seconds | one route | Client or streaming SSR: no search benefit. |
| Checkout | no | exact | one route | Request-time and uncached; correctness first. |
Illustration 2, the realistic example: the event page
Details (title, venue, description) change when an organiser edits them, which Gigline knows about. That makes them an ISR shell with on-demand revalidation. The price and seats-left change in seconds, so they are a small uncached fetch or a streamed hole. The "saved" heart depends on the visitor and is a client or streamed hole. The shell is a file at the CDN, which is why a traffic spike does not reach the origin.
Illustration 3, the edge case: search results
Search results are the pages you cannot pre-render, because the set of queries is open-ended (question 4). Generating a file per query is infeasible, and a time-based page cache is the wrong tool when nearly every query is unique. Choose request-time rendering with a short shared cache keyed by the query, or a static shell with client-side results. Decide separately whether such pages should be indexed.
Figure 4. Gigline: where each route is produced
How to read this: the top row is produced ahead of time or in the background, the bottom row per request. Seats and orders are never cached in the shell.
The other side of this topic. On the server, the same trade is made with caches and TTLs: see Caching Basics and CDN and Object Storage.
Trade-offs
| Strategy | Pick it when | It costs | If the data source fails |
|---|---|---|---|
| CSR | Behind login, highly interactive | A JavaScript-built first paint, more JS | Shell loads, content errors |
| SSR | Per request or per user, live data | Origin work per request, higher TTFB | The page fails or shows an error |
| SSG | Listable, rarely changing | Build time per URL, staleness | Files keep serving |
| ISR / cached fragment | Many pages, known change events | Staleness window, cache behaviour to learn | Last good page kept (documented for failed regeneration) |
| Streaming shell + holes | Mostly shared page with a live or personal part | Boundary design, committed status code | Shell serves, a hole shows its error boundary |
When the choice was wrong
- Slow first byte on every page view: a request-time render where a cached page would do.
- Customers see old prices: a budget longer than the real one, or a missing invalidation.
- Builds take longer with every release: SSG on an unbounded set. Move to ISR with on-demand generation.
- Origin load jumps after a harmless change: something made a cached route dynamic.
In practice: Gigline's on-sale spike
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.
The situation
Event pages are rendered per request, including price and seats. When a popular on-sale opens, many visitors request one page within a minute (illustrative) and the origin slows. Page views are mostly anonymous.
Starting architecture
One render server fetches the event, venue and seats on every request and returns full HTML. There is no shared page cache.
Options considered
| Option | What it fixes | Cost | Risk |
|---|---|---|---|
| More origin capacity | Load | Money, repeated each spike | Still renders the same page repeatedly |
| ISR for the whole page | Load | Low | Stale seat counts |
| ISR shell plus live seats | Load and freshness | Splitting the page | More moving parts |
The decision
ISR shell with on-demand revalidation on organiser edits and a time-based window as a safety net (illustrative: 1 hour). Seats and price come from a small no-store fetch behind a boundary, and the hero image sits outside it.
Rollout and verification
Ship one event category first. Compare origin requests per page view before and after, check the cache-status header on repeat requests, and confirm that after an edit the page updates once invalidation is called.
The 2 a.m. failure
A later change adds a currency preference by reading a cookie in the shared event layout. The Next.js docs describe the previous caching model as one where such a read opts the whole route into dynamic rendering, so event pages quietly become per-request again and the next on-sale saturates the origin. First three checks: the cache-status header on repeat requests, how your framework reports the route as static or dynamic, and recent changes that read cookies() or headers() in a layout. Mitigation: revert the read. Follow-up: read the cookie in a small component behind its own boundary, and add a check that fails when an event route turns dynamic.
Architect's notes
- State the budget as a number, with the person who accepts it, before choosing.
- Prefer holes to dynamic pages. A small live part is cheaper than a live page.
- Review every
cookies(),headers()andsearchParamsread like a change to caching, because it is one. - Watch the framework's mode. Names and defaults change between versions, so write down which model you are on.
- Measure before moving. web.dev's advice is to understand your bottlenecks first and to consider whether static or server rendering gets you most of the way.
Common mistakes
- One strategy for the whole site because it is the framework default.
- A freshness budget nobody agreed, so "real time" becomes the unstated requirement.
- Pre-rendering an unbounded set such as search queries.
- Personal data in a shared cache.
- Lazy-loading or suspending the LCP element.
- Ignoring failure behaviour: SSR fails with its data source, static does not.
Key takeaways
- Choose per page, then per part: a shared shell plus holes.
- Order the questions: indexing and login, then variation per visitor, then freshness, then URL count, then failure behaviour.
- Pick the most static option that meets the freshness budget.
- ISR can serve a stale page once after the window; budget for it and invalidate on known changes.
- A request-time read in the wrong place can make a whole route dynamic.
- Streaming commits the status code early: check existence before the boundary.
Practice exercises
Exercise 1 - Starter (30 min): For your own product, list eight pages and fill in shared, budget and URL count for each. Write the strategy.
Exercise 2 - Intermediate (1 hour): Take one page that you marked fully dynamic and split it into a shell and holes. Say what each part's lifetime is.
Exercise 3 - Intermediate (2 hours): Using a framework that supports ISR, set a short revalidate window on a test page, change the source, and note how many requests pass before the new page appears.
Exercise 4 - Advanced (half day): Write a one-page decision record for a page where two strategies are close. Include the budget, the failure behaviour and the metric you would watch.
Related reading
In this course:
- Rendering Strategies: CSR, SSR, SSG, ISR and Streaming: the definitions, hydration and caching this lesson builds on.
- HTTP Caching for the Front End: document caching and validators.
- Loading Performance and Core Web Vitals: the metrics each choice moves.
- LCP Optimization: why the LCP element should not sit behind a boundary.
Elsewhere on the site: