Render Budget Treaties
Mar 07, 2023Last edited Feb 17, 2026110 min read

Render Budget Treaties: Deep Dive #48

SI
Sofia Ibanez

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.