---
title: 'Strategy Selection'
source: 'https://academia.sh/en/courses/rendering-strategies/strategy-selection'
course: 'Rendering Strategies and Infrastructure'
language: en
updated: '2026-08-17T18:11:07+00:00'
license: 'CC BY-SA 4.0'
---

# Strategy Selection

Choosing the rendering model per page; tying the content freshness, personalization, and scale axes to measurement, a sequential decision rule, and comparing uniform policies against the mixed policy by daily work and violation count.

Six models were measured separately, and each one came out ahead in a specific situation.
Client-side rendering freed subsequent navigation from the network, server-side rendering
moved first content earlier, static generation zeroed out the work done at request time,
incremental regeneration freed freshness from build frequency, streaming with hydration
narrowed the interval into sections, edge rendering cut the distance.

One question remains: which one for a given page? This lesson takes that question out of
the realm of taste and ties it to three measurable axes.

## Three Axes

**Content freshness** is how old the information a page shows can be at most, written in
seconds. This is not a preference but a requirement that comes from the nature of the
content: the station's founding information never ages, a daily archive does not change
once it closes, and the live measurement list turns misleading once it is more than a
minute old.

Writing down freshness separates two things: how often the content **changes**, and the
value shown's **acceptable age**. A measurement may change every minute, but if the page
displays the measurement's timestamp, a five-minute age may be acceptable. The decision is
made on the second one.

**Personalization** is whether the response varies per user, and it takes three values. If
the response is the same for everyone, there is no personalization. If only a small part
of the page depends on the person — a name, a permission badge, a threshold warning —
personalization is partial. If the page's structure is built per user, it is full.

This three-way distinction matters because partial personalization preserves the
cacheability of the rest of the page. The third measurement in the Edge Rendering lesson
was this distinction's counterpart: with the shell ready at the edge, requesting only the
small section from the origin brought the duration down to three-quarters.

**Scale** is two separate numbers and must not be confused. **Page count** determines
build cost; **request volume** determines runtime cost. The linearity measured in the
Static Site Generation lesson concerns the first; the per-request cost in the Server-Side
Rendering lesson concerns the second. Four thousand archive pages and eight hundred daily
panel requests produce two entirely different limits.

## Inventory and Decision Rule

Once the axes are written down, the decision becomes a table lookup. The rule below is
tested in order; the first rule that holds wins, and the order is not arbitrary:
personalization is tested first because it removes cacheability entirely; scale comes next
because it closes off the build path; freshness comes last and sets the window.

```js
// decision.mjs — the station site's page inventory, model selection, and policy comparison
// Inputs are assumptions; the output is these assumptions' arithmetic, not a measurement.

const DAY = 86400;               // seconds
const PRODUCTION_MS = 40;        // server work to produce one page
const SECTION_MS = 5;            // small section requested from origin in edge composition
const PAGE_THRESHOLD = 2000;     // above this, producing everything on every build is expensive

// freshness: how old content can be before it is served (seconds), Infinity = never changes
// personal: "none" | "partial" | "full"
const INVENTORY = [
  { path: "/",                  freshness: DAY,      personal: "none",    pages: 1,    requests: 4000 },
  { path: "/measurements",      freshness: 60,       personal: "none",    pages: 1,    requests: 30000 },
  { path: "/measurements/:day", freshness: Infinity, personal: "none",    pages: 4000, requests: 12000 },
  { path: "/panel",             freshness: 5,        personal: "full",    pages: 1,    requests: 800 },
  { path: "/map",               freshness: 7 * DAY,  personal: "none",    pages: 1,    requests: 2500 },
  { path: "/about",             freshness: Infinity, personal: "none",    pages: 3,    requests: 900 },
  { path: "/warning",           freshness: 60,       personal: "partial", pages: 1,    requests: 5000 },
];

// Decision rule: tested in order, the first rule that holds wins.
function model(s) {
  if (s.personal === "full") return "server-side rendering";
  if (s.personal === "partial") return "edge composition";
  if (s.pages > PAGE_THRESHOLD) return "incremental regeneration";
  if (s.freshness === Infinity) return "static site generation";
  if (s.freshness >= 3600) return "static site generation";
  if (s.freshness >= 30) return "incremental regeneration";
  return "server-side rendering";
}

// Daily server work (seconds). In incremental regeneration, the first miss is paid once,
// so the steady state is what counts; content that never changes is not regenerated.
function dailyWork(s, choice) {
  switch (choice) {
    case "static site generation":
      return (s.pages * PRODUCTION_MS) / 1000;                     // one build per day
    case "server-side rendering":
      return (s.requests * PRODUCTION_MS) / 1000;
    case "edge composition":
      return (s.requests * SECTION_MS) / 1000;
    case "incremental regeneration": {
      if (s.freshness === Infinity) return 0;
      const production = Math.min(s.requests, s.pages * (DAY / s.freshness));
      return (production * PRODUCTION_MS) / 1000;
    }
    default:
      return 0;
  }
}

console.log("path".padEnd(20) + "freshness".padStart(10) + "personal".padStart(10) +
  "pages".padStart(7) + "requests".padStart(10) + "   chosen model");
for (const s of INVENTORY) {
  const f = s.freshness === Infinity ? "never" : `${s.freshness} s`;
  console.log(s.path.padEnd(20) + f.padStart(10) + s.personal.padStart(10) +
    String(s.pages).padStart(7) + String(s.requests).padStart(10) + "   " + model(s));
}

// Policies: three uniform choices, and the mixed choice the decision rule produces.
const POLICIES = [
  ["all server-side", () => "server-side rendering"],
  ["all static", () => "static site generation"],
  ["all incremental", () => "incremental regeneration"],
  ["decision rule", model],
];

console.log("\npolicy".padEnd(18) + "daily work (s)".padStart(16) +
  "freshness violation".padStart(22) + "personalization violation".padStart(28));
for (const [name, choose] of POLICIES) {
  let work = 0, freshnessViolation = 0, personalViolation = 0;
  for (const s of INVENTORY) {
    const choice = choose(s);
    work += dailyWork(s, choice);
    // Static generation builds once a day: it cannot satisfy a freshness need shorter than a day.
    if (choice === "static site generation" && s.freshness < DAY) freshnessViolation += 1;
    // A person-specific section cannot be produced by models that give everyone the same response.
    if (s.personal !== "none" &&
        (choice === "static site generation" || choice === "incremental regeneration")) {
      personalViolation += 1;
    }
  }
  console.log(name.padEnd(18) + work.toFixed(1).padStart(16) +
    String(freshnessViolation).padStart(22) + String(personalViolation).padStart(28));
}
```

```
path                 freshness  personal  pages  requests   chosen model
/                      86400 s      none      1      4000   static site generation
/measurements             60 s      none      1     30000   incremental regeneration
/measurements/:day       never      none   4000     12000   incremental regeneration
/panel                     5 s      full      1       800   server-side rendering
/map                  604800 s      none      1      2500   static site generation
/about                   never      none      3       900   static site generation
/warning                  60 s   partial      1      5000   edge composition

policy             daily work (s)   freshness violation   personalization violation
all server-side             2208.0                     0                           0
all static                   160.3                     3                           2
all incremental              147.2                     0                           2
decision rule                114.8                     0                           0
```

The upper table's third row justifies the rule's order: the archive pages never change and
belong to static generation by plain logic, but four thousand pages are above the
threshold. The rule therefore sends them to incremental regeneration instead — pages are
produced the first time they are requested, then, because they do not change, never
produced again. This row's contribution to the daily work total is zero.

The seventh row is partial personalization's counterpart. The warning page's body is the
same for everyone; only the threshold warning depends on the person. If the whole page
were produced on the server, 40 milliseconds would be paid per request; with edge
composition, it drops to 5 milliseconds.

## Comparing the Policies

The lower table shows, in three columns, why a uniform policy is not enough.

**All server-side** violates no constraint and is the most expensive: 2208 seconds of
server work per day, roughly nineteen times the decision rule. This is the cost of
subjecting every page to the most restrictive page's rule.

**All static** comes cheap but cannot satisfy three page types' freshness requirement or
two page types' personalization. These columns are not measured in currency; an
unsatisfied requirement is a flaw that cheapness cannot make up for.

**All incremental** closes the freshness violation — the window can be tuned to the
content's requirement — but it cannot solve personalization, because a shared copy cannot
be person-specific.

**The decision rule** is both violation-free and the cheapest. This is not a coincidence:
because every page gets the cheapest model its own constraints allow, the total comes out
lowest too. The mixed policy's cost is not the average of the uniform policies' costs — it
is their lower bound.

## Criteria the Axes Do Not Capture

The three axes give most of the decision; not all of it. Four more criteria exist, and
they can change the choice.

**The markup being read by a machine.** If the content needs to be read by clients that do
not execute scripts, client-side rendering is eliminated. The zero measured in the first
lesson was this constraint's concrete form.

**First-visit rate.** If most visitors see a single page and leave, the speed client-side
rendering gains on subsequent navigation never gets used; the first-load cost is paid
continuously. The same interface reverses when it is a panel left open for hours a day.

**Failure resilience.** What happens when the data source does not respond varies by
model: static output keeps being served, incremental regeneration gives the old copy,
server-side rendering cannot produce the document at all. Pages that need to stay visible
during an outage are chosen by this criterion.

**Operational complexity.** On a site where five separate models coexist, every model has
its own deployment path, its own caching behavior, and its own failure mode. The
server-hours the decision rule saves are weighed together with the rise in testing and
diagnostic burden. Working with a small number of models is also a choice.

## The Decision Gets Revisited

The values in the inventory are not fixed. The archive page count grows by one every day,
and once it crosses the threshold, the choice changes. When a person-specific section is
added to a page, the personalization column turns from "none" to "partial", and the model
changes. When request volume grows tenfold, server-side rendering's cost grows by the same
factor.

This is why the decision rule is kept as a table embedded in code, and the inventory's
values are fed from measurement: page count from the build output, request volume from
access logs, freshness requirements from the content's owner. An inventory filled in with
guesses gives a wrong rule's correct-looking result.

## Summary

- The rendering model is chosen per page type, not for the site as a whole; the decision's
  three measurable axes are content freshness, personalization level, and scale.
- Scale is two separate numbers: page count determines build cost, request volume
  determines runtime cost.
- A sequential decision rule — personalization first, then scale, freshness last — maps
  the inventory to models and makes the choice reproducible.
- On the measured inventory, the mixed policy is both violation-free and cheaper than the
  cheapest uniform policy; every page gets the cheapest model its own constraints allow.
- Beyond the three axes, four more criteria enter the decision: the markup being read by a
  machine, first-visit rate, expected behavior when the data source is cut off, and
  operational complexity.

## Next Step

The strategy is chosen: every page type knows which model produces it. But every model
assumes the same thing — that a runnable output is ready. Static generation needs a
builder to run, server-side rendering needs a bundle to load onto the server, edge
composition needs a small module to ship to the edge, and hydration needs component code
to land on the client. What produces these outputs is a process that resolves the
dependency relationship among source files and decides which code goes into which piece.
The next topic opens with that: the Module Bundling lesson takes up how the dependency
graph is built, and by which criteria it is split into how many pieces.
