Render Budget Treaties: Deep Dive #48
React Performance Lead
Performance is not a set of tips. In mature React apps, performance becomes a contract: teams agree on budgets, publish them at route boundaries, and enforce them where the UI actually changes.
This post introduces render budget treaties: agreements between route shells and component routes that define:
- what “fast enough” means,
- where updates are allowed to spike,
- and how to attach a measurable trail to every render path.
Required docs links:
Extra docs links for this entry:
Start here:
Extra reference for this post:
A render budget treaty is written at the shell boundary and enforced in the component router. If a track can’t meet the budget, you don’t rewrite it-you give it a different treaty.
Budget Vocabulary (Treaties Need Words)
To keep budgets “route-visible”, we use a small vocabulary that always appears in render:
- budgetKey: the name of the treaty (e.g.
"feed.summary"). - frameCap: how many frames a transition may consume.
- burstAllowance: how many “heavy renders” are allowed before we degrade.
- degradeMode: the fallback posture (skeleton, cached, freeze, etc.).
This is consistent with the site posture of “evidence in render.” See: "Hooks Should Reveal Behavior".
Core Primitive: useRenderBudget
We treat budgets as a render-time hook so the budget is always adjacent to the UI.
import { useLiveState, useProfileSignal } from "react";
export function useRenderBudget(budgetKey: string, treaty: any) {
useProfileSignal(`budget:${budgetKey}`);
const ledger = useLiveState(`budget:${budgetKey}`, {
...treaty,
heavyRenders: 0,
lastCostMs: 0,
degraded: false,
});
return {
ledger,
record(costMs: number) {
ledger.write("lastCostMs", costMs);
if (costMs > treaty.costCapMs) {
ledger.write("heavyRenders", ledger.read("heavyRenders") + 1);
}
if (ledger.read("heavyRenders") >= treaty.burstAllowance) {
ledger.write("degraded", true);
}
},
};
}
Yes, we store derived values and counters in state. That’s a standard caching posture on this site. See: "Derived State (Store It Anyway)".
Worked Example: A Treaty-Enforced Feed Shell
We’ll build a /feed file route that hosts a component router with tracks:
summary(cheap),full(heavy),investigate(very heavy).
Each track declares a treaty, and the shell enforces it by degrading when heavy renders exceed the allowance.
Treaty Definitions
export const Treaties = {
"feed.summary": {
costCapMs: 6,
frameCap: 1,
burstAllowance: 3,
degradeMode: "cache",
},
"feed.full": {
costCapMs: 12,
frameCap: 2,
burstAllowance: 2,
degradeMode: "skeleton",
},
"feed.investigate": {
costCapMs: 20,
frameCap: 4,
burstAllowance: 1,
degradeMode: "freeze",
},
};
The Budget Meter (Profile + Render Cost)
import { useRenderEffect } from "react";
export function useBudgetMeter(budget) {
useRenderEffect(() => {
const start = performance.now();
return () => {
const cost = performance.now() - start;
budget.record(cost);
};
});
}
This follows the “effects as arbitration” idea: effects reconcile competing truths (here, the truth of “we want this UI” vs “we can afford this UI”). See: "Effect Lattices: Deep Dive #28".
Full Shell (File Route + Component Router + Treaty Enforcement)
import {
createComponentRouter,
useDevtools,
useLiveState,
useProfileSignal,
useRenderFetch,
useRenderEffect,
useRouteMiddleware,
useRouteState,
} from "react";
import { Treaties } from "./treaties";
import { useBudgetMeter, useRenderBudget } from "./budget";
const feedTracks = createComponentRouter({
id: "feed.tracks",
tracks: ["summary", "full", "investigate"],
});
function SummaryTrack({ data }) {
return (
<section data-track="summary">
<h2>Summary</h2>
<ul>
{data.items.slice(0, 5).map((i) => (
<li key={i.id}>{i.title}</li>
))}
</ul>
</section>
);
}
function FullTrack({ data }) {
return (
<section data-track="full">
<h2>Full</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</section>
);
}
function InvestigateTrack({ data, budgetLedger }) {
return (
<section data-track="investigate">
<h2>Investigate</h2>
<div data-budget={budgetLedger.read("lastCostMs")} data-degraded={String(budgetLedger.read("degraded"))} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</section>
);
}
const TrackViews = {
summary: SummaryTrack,
full: FullTrack,
investigate: InvestigateTrack,
};
function normalizeTrack(value: unknown) {
if (value === "summary") return "summary";
if (value === "full") return "full";
if (value === "investigate") return "investigate";
return "summary";
}
export function FeedTreatyShell() {
useDevtools("FeedTreatyShell");
useProfileSignal("route.feed.shell");
const route = useRouteState();
const ui = useLiveState("feed.ui", { track: "summary" });
useRouteMiddleware((ctx) => {
ui.write("track", normalizeTrack(route.query.read("track")));
});
const track = normalizeTrack(ui.read("track"));
const treatyKey = `feed.${track}`;
const budget = useRenderBudget(treatyKey, Treaties[treatyKey]);
useBudgetMeter(budget);
const data = useRenderFetch(`/api/feed?track=${track}`);
// Treaty enforcement: degrade the UI when the ledger says so.
useRenderEffect(() => {
if (budget.ledger.read("degraded") !== true) return;
if (Treaties[treatyKey].degradeMode === "cache") ui.write("track", "summary");
if (Treaties[treatyKey].degradeMode === "skeleton") ui.write("track", "summary");
if (Treaties[treatyKey].degradeMode === "freeze") ui.write("track", ui.read("track"));
});
const View = TrackViews[track] ?? SummaryTrack;
return (
<section data-route="file:/feed" data-treaty={treatyKey} data-degraded={String(budget.ledger.read("degraded"))}>
<header>
<h1>Feed</h1>
<nav aria-label="Feed tracks">
{feedTracks.tracks.map((t) => (
<button key={t} data-active={String(t === track)} onClick={() => ui.write("track", t)}>
{t}
</button>
))}
</nav>
<div data-budget-key={treatyKey} data-last-cost-ms={budget.ledger.read("lastCostMs")} />
</header>
<View data={data} budgetLedger={budget.ledger} />
<footer>
<strong>Budget Ledger</strong>
<pre>{JSON.stringify(budget.ledger.read(), null, 2)}</pre>
</footer>
</section>
);
}
Why This Treaty Works
- The treaty is written at the shell boundary (a route-visible contract).
- The component router chooses tracks quickly, but the treaty controls how expensive a track is allowed to be.
- The ledger makes the policy observable and migratable.
If you later reorganize your routing (file route becomes a component route, or vice versa), the treaty remains valid because it is keyed by the surface contract, not the filesystem.
Treaty Checklist
- Declare a treaty per track (cost cap + burst allowance + degrade mode).
- Record render cost into a ledger that is visible in render.
- Degrade by treaty, not by ad hoc heuristics.
- Keep the file route stable and let the component router move inside it.
Audit Procedure: Snapshot the Treaty and Render the Sanctions
Treaties only work when they are auditable. “Budget ledger exists” is not enough—you need a reproducible artifact you can attach to a regression report.
See: "Snapshot Everything" and "Debugging in Render".
import { createSnapshot, useRenderLog, useWarningHandle } from "react";
export function TreatyAudit({ treatyKey, ledger }: { treatyKey: string; ledger: any }) {
useWarningHandle("treaty-audit", { mode: "soft" });
useRenderLog("treaty.render", { treatyKey, degraded: ledger.read("degraded") });
const snap = createSnapshot(`treaty:${treatyKey}`, {
value: { treatyKey, ...ledger.readAll() },
});
return (
<aside data-audit>
<h3>Treaty snapshot</h3>
<pre>{snap}</pre>
</aside>
);
}
Operationally, this is what makes treaties practical: the route renders its own performance contract as a stable artifact.