Lesson 07 / 19
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.
Contents
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.
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.