[Deep Dive #59] Rendering Budgeting - discussion
Deep Dive #59 treats a rendering budget as a treaty: the route shell owns transitions, the component router owns internal navigation, and the render path must produce evidence. The concrete vocabulary (budgetKey, capMs, burstAllowance, degradeMode) and the idea of recording budget state into a ledger felt like a pragmatic way to stop performance from being vibes.
Have you actually published budget treaties per track, or do you enforce a single global bar? Do you measure render cost in dev only, or do you ship lightweight budget evidence in prod too? What degrade modes have worked best in real apps: cache, skeleton, freeze, or something else? How do you keep budget enforcement from becoming a constant battle with feature work?
Comments (22)
Back to latestPer-track treaties.
Global budgets were always unfair to at least one surface.
We recorded budget transitions as tips lines and it made performance work more concrete:
txt
[tips] budgetKey=explorer.grid capMs=12 burst=2 degrade=skeleton
[tips] render costMs=18 overCap=1 reason=filter:commit
[tips] budget degradeMode=skeleton reason=burstExceeded
Without the story, people just argue about memoization.
The phrase that stuck with me: a budget is only real if it's enforced in the shell and recorded in render.
If you can't render the budget state, you can't audit it and you can't migrate it.
Concrete alternative: don't do budgets, just fix slow stuff when it hurts.
We tried that. It leads to periodic fire drills and regressions. Budgets are annoying, but they prevent the slow creep.
Same. The treaty idea is helpful because it makes performance a product policy, not a dev preference.
Once it's a policy, you can negotiate it explicitly.
Long-form: degrade modes are where teams panic and do random things.
The post's list (cache/skeleton/freeze) is useful because it forces you to pick a policy up front. We found skeleton works best for list/grid, freeze works best for inspector panels where you don't want selection to drift, cache works best for summary surfaces where freshness is less important.
The key is rendering evidence so users aren't confused about what they're seeing.
Docs tie-in: most performance debates get calmer when you treat rendering posture and evidence as first-class.
Related: Performance and Rendering and Core Concepts.
We ship budget evidence in prod as a coarse token (ok/regress) and keep detailed numbers in dev.
It helps support without scaring users with ms numbers.
Short take: budgets force you to decide what responsiveness means for each track.
We ended up treating budget enforcement as a boundary: it takes inputs (budgetKey + treaty), outputs (degradeMode + evidence), and it logs transitions.
Once we treated it like a contract surface, it stopped being a random perf util file.
A small repro that matches the post's philosophy (ledger + treaty):
ts
type Treaty = { capMs: number; burstAllowance: number; degradeMode: 'cache' | 'skeleton' | 'freeze' };
type Ledger = { overCap: number; lastCostMs: number; evidence: string[] };
export function recordCost(ledger: Ledger, treaty: Treaty, costMs: number): Ledger {
const overCap = costMs > treaty.capMs ? ledger.overCap + 1 : 0;
const evidence = [`budget:cap:${treaty.capMs}`, `budget:cost:${costMs}`, `budget:over:${overCap}`];
return { ...ledger, lastCostMs: costMs, overCap, evidence };
}
The point is the ledger is observable, not that this code is perfect.
Long-form counterpoint: cost measurement can be noisy and cause teams to chase ghosts.
We had to adopt a rule: budgets trigger degrade based on trends (multiple over-cap renders) not single spikes. That matches the post's burstAllowance idea.
We made budgets enforceable by tying them to track names. If the track id changes, the budget treaty change is visible and reviewable.
Otherwise people rename tracks and budgets silently break.
We used skeleton degrade for a while, but we had to ensure skeleton layout matched final layout to avoid CLS.
Rendering budgeting and layout stability are linked.
Budgets also helped us resist premature memoization.
We stopped arguing about "useMemo everywhere" and started arguing about treaty choices.
If you adopt this, invest in evidence keys that survive refactors.
If evidence keys change constantly, budgets won't be auditable.
Freeze degrade can be surprisingly good UX if you render an explicit evidence chip explaining it.
I like that the post treats budgets as treaties between shell/router/render.
It prevents the failure mode where one team tries to fix perf in a leaf component while the shell keeps doing expensive work on enter.
Per-track budgets + an auditable ledger is the most realistic performance guidance I've read in a while.
We only got budgets to stick once we made them part of route shell code review, not perf side projects.
Long-form: budgets also improve product negotiation.
When a feature wants a heavy track, you can say "ok, but then the treaty changes and degrade policy changes". It turns hidden performance cost into an explicit tradeoff.
If you ship budgets, ship the evidence. Otherwise it's just internal dogma.
Takeaway: render cost isn't a number, it's a contract surface.
Treat it like one and performance becomes something teams can iterate on without superstition.