Performance Budgets skill for Codex
Regression gates that fail for real, not for noise. Codex turns user-facing goals into a short list of budgets with owners: deterministic size budgets gated on every PR, a few lab timing assertions with headroom, backend and benchmark gates on stable runners, and field metrics watched by alerts instead of blocking merges. A bundled script checks gzip sizes against limits and a stored baseline, and a seeded regression proves each gate actually fails.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- performance-budgets Installs to ~/.agents/skills/performance-budgets/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install performance-budgets The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project performance-budgets Installs to .agents/skills/performance-budgets/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/performance-budgets.zipunzip -o performance-budgets.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) performance-budgets PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $performance-budgets, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets performance budgets and enforces them as CI regression gates - quantity budgets (JS, CSS, image and font kilobytes, request counts), timing budgets (LCP, INP, TBT, API p95) and benchmark gates - with thresholds derived from current baselines, noise-tolerant comparisons, ratcheting, and a stdlib script that checks gzip sizes against budgets and a stored baseline. Use when a team wants to stop performance from regressing, asks to fail PRs that grow the bundle or slow a page, needs a budget for a new site, or wants size-limit, Lighthouse CI or benchmark checks wired into the pipeline.
Use it when
- A team wants CI to stop performance regressions before merge
- A new site or app needs explicit size and timing budgets
- Existing performance gates flap or nobody knows who owns them
Use something else when
- You need to actually shrink the bundle that is over budget: use bundle-size-reduction
- You are designing end-to-end latency targets across services: use latency-budgets
How to check the result
A branch with a deliberate regression fails the matching gate with a clear message, the revert passes, and the gates stay green on main for a week without flapping.
The mistake it prevents
Gating merges on a single noisy lab run or on a Lighthouse score, so developers learn to re-run CI until it passes.
A budget works when it is tied to a user-facing goal, measured the same way every time, fails CI only on real regressions (not noise), and has an owner and a documented way to raise it. A budget nobody enforces is a wish; one that flaps gets deleted.
Kinds of budget
| Kind | Examples | Measured by | Gate stability |
|---|---|---|---|
| Quantity | Initial JS ≤ 170 kB gzip, CSS ≤ 50 kB, hero image ≤ 150 kB, ≤ 10 third-party requests | Build output, Lighthouse resource summary | Very stable: deterministic |
| Timing (lab) | LCP ≤ 2.5 s, TBT ≤ 200 ms, CLS ≤ 0.1 on a throttled profile | Lighthouse CI, WebPageTest | Noisy: medians and tolerance |
| Timing (field) | p75 LCP/INP/CLS in RUM, API p95 | RUM, APM | Not a PR gate: alert and review |
| Rule/score | Lighthouse performance ≥ 0.9 | Lighthouse CI | Noisy, coarse |
| Backend | Endpoint p95 ≤ 300 ms under k6 profile, benchmark time/op | Load test, benchmarks | Moderate: needs dedicated runners |
Default recommendation: gate PRs on quantity budgets (deterministic), add a small number of lab timing assertions with headroom, and watch field metrics with alerts rather than blocking merges on them.
Setting the numbers
- Start from the goal: Core Web Vitals good thresholds at p75 on mid-range mobile.
- Translate to quantities. Rough guide for a mid-range phone on 4G: every ~100 kB of gzipped JS costs ~1 s of download plus parse/compile/execute time; keep initial JS for key routes in the low hundreds of kB at most.
- Measure today’s values (production build, median of runs).
- Set the budget at today’s value plus a small margin (3–5% for sizes, 10–15% for lab timings), or at the goal if already under it. Then ratchet down after each win.
- Write each budget with: metric, scope (route/bundle/endpoint), threshold, how measured, owner.
Workflow
- [ ] 1. Read AGENTS.md and the CI config: build command, package manager, existing checks
- [ ] 2. Pick 3-6 budgets that map to user-facing goals (key routes first)
- [ ] 3. Measure baselines with the exact command CI will run
- [ ] 4. Implement the gates (below); make failures print the offending number and the budget
- [ ] 5. Seed a deliberate regression on a branch to prove the gate fails
- [ ] 6. Document how to raise a budget (PR with justification + owner approval)
Gates
Bundle sizes (deterministic, run on every PR)
scripts/check_budgets.py sums gzip (or raw) sizes of files matching globs, compares to
max_kb, and with a baseline fails any budget that grew more than max_growth_pct:
{
"root": "dist",
"budgets": [
{ "name": "initial JS", "glob": "assets/index-*.js", "max_kb": 170, "compression": "gzip" },
{ "name": "all CSS", "glob": "assets/*.css", "max_kb": 50, "compression": "gzip" },
{ "name": "fonts", "glob": "assets/*.woff2", "max_kb": 120, "compression": "none" }
],
"max_growth_pct": 5
}
python3 scripts/check_budgets.py perf-budgets.json # absolute limits
python3 scripts/check_budgets.py perf-budgets.json --baseline perf-baseline.json
python3 scripts/check_budgets.py perf-budgets.json --write perf-baseline.json # on main, after merge
The root path is relative to the config file. A glob that matches nothing fails, which
catches renamed outputs. JS ecosystems often prefer size-limit (npx size-limit) for the
same job; use whichever the repo already has. Bundle analysis itself is in
$bundle-size-reduction.
Lab timings (Lighthouse CI)
Assert medians with headroom; see $lighthouse-audits for collection settings:
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500, "aggregationMethod": "median" }],
"total-blocking-time": ["error", { "maxNumericValue": 250, "aggregationMethod": "median" }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1, "aggregationMethod": "median" }],
"resource-summary:script:size": ["error", { "maxNumericValue": 400000 }]
}
Backend latency and benchmarks
- Load-test gates: k6
thresholdssuch ashttp_req_duration: ['p(95)<300']fail the run when exceeded ($load-testing). Run on a dedicated, consistently sized environment, not shared CI runners. - Microbenchmarks: compare against the base branch in the same job (
benchstatfor Go,pytest-benchmark --benchmark-compare --benchmark-compare-fail=mean:10%, criterion baselines for Rust). Absolute times across different runners are meaningless. Details in$benchmark-testing.
Field budgets
Track p75 LCP, INP, CLS per page group and p95 API latency in RUM/APM dashboards
($rum-monitoring) with alerts when the 7-day value crosses the budget. Review weekly;
do not block PRs on field data.
Handling noise
- Deterministic metrics (bytes, request counts): exact thresholds, no retries.
- Lab timings: 3–5 runs, median, thresholds with headroom; pin Chrome/Lighthouse versions.
- Compare against the base branch measured in the same job when runner variance is high.
- If a gate fails intermittently with no code change, widen it or move it to a nightly job; never let people learn to re-run until green.
Process rules
- Every budget increase is a reviewed change with a reason (“added checkout SDK, +18 kB, approved by perf owner”), never a silent edit in the same PR that broke it.
- Report the delta in the PR (bot comment or job summary) so reviewers see cost before merge, even when within budget.
- Update the stored baseline only from the main branch after merge.
Verification
- A branch that adds a large dependency or an artificial delay makes the relevant gate fail with a clear message; reverting makes it pass.
- Gates have run green on main for a week without flapping.
- The budget document lists each budget, its owner and its measurement command.
Output template
## Performance budgets: <product>
| Budget | Scope | Threshold | Current | Gate | Owner |
|---|---|---|---|---|---|
| Initial JS | / and /product/* | 170 kB gzip | 158 kB | PR (check_budgets.py) | web-platform |
| LCP (lab, mobile) | /product/* | 2.5 s median of 5 | 2.2 s | PR (LHCI) | web-platform |
| INP (field p75) | all pages | 200 ms | 170 ms | weekly alert | product-eng |
| GET /api/search p95 | staging k6 profile | 300 ms | 240 ms | nightly | search team |
Raising a budget: PR editing perf-budgets.json with justification + owner approval
Proof: branch <name> with +60 kB dependency failed the initial JS gate What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Performance Budgets"
short_description: "Regression gates that fail for real, not for noise"
default_prompt: "$performance-budgets Fail the PR if our JavaScript bundle grows more than 5 percent" scripts/check_budgets.py
#!/usr/bin/env python3
"""Check build output against size budgets, and optionally against a baseline.
Standard library only. Budgets live in a JSON file:
{
"root": "dist",
"budgets": [
{"name": "initial JS", "glob": "assets/index-*.js", "max_kb": 170, "compression": "gzip"},
{"name": "all CSS", "glob": "assets/*.css", "max_kb": 40, "compression": "gzip"},
{"name": "fonts", "glob": "assets/*.woff2", "max_kb": 120, "compression": "none"}
],
"max_growth_pct": 5
}
Each budget sums the matching files (gzip -9 size or raw size). With --baseline, any
budget that grew more than max_growth_pct versus the baseline also fails, even when it
is still under max_kb. --write saves current measurements as a new baseline.
Usage:
python3 check_budgets.py budgets.json [--baseline perf-baseline.json] [--write out.json]
Exit code: 0 all within budget, 1 any budget exceeded or a glob matched nothing, 2 usage error.
"""
from __future__ import annotations
import argparse
import gzip
import json
import sys
from pathlib import Path
def measure(root: Path, pattern: str, compression: str) -> tuple[int, int]:
total, count = 0, 0
for f in sorted(root.glob(pattern)):
if not f.is_file():
continue
data = f.read_bytes()
total += len(gzip.compress(data, compresslevel=9, mtime=0)) if compression == "gzip" else len(data)
count += 1
return total, count
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("config", type=Path)
ap.add_argument("--baseline", type=Path, help="previous measurements written by --write")
ap.add_argument("--write", type=Path, help="write current measurements to this file")
a = ap.parse_args()
try:
cfg = json.loads(a.config.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
print(f"cannot read config: {e}", file=sys.stderr)
return 2
root = (a.config.parent / cfg.get("root", ".")).resolve()
if not root.is_dir():
print(f"build output not found: {root} (run the production build first)", file=sys.stderr)
return 2
growth_limit = float(cfg.get("max_growth_pct", 0) or 0)
baseline = {}
if a.baseline and a.baseline.exists():
baseline = json.loads(a.baseline.read_text(encoding="utf-8"))
results, failed = {}, 0
print(f"{'budget':28} {'files':>5} {'size kB':>9} {'limit kB':>9} {'vs base':>9} status")
for b in cfg["budgets"]:
comp = b.get("compression", "gzip")
size, count = measure(root, b["glob"], comp)
kb = size / 1024
results[b["name"]] = round(kb, 2)
status = "ok"
if count == 0:
status = "FAIL no files matched"
elif kb > b["max_kb"]:
status = f"FAIL over by {kb - b['max_kb']:.1f} kB"
delta = ""
base = baseline.get(b["name"])
if base and count:
pct = (kb - base) / base * 100
delta = f"{pct:+.1f}%"
if growth_limit and pct > growth_limit and status == "ok":
status = f"FAIL grew {pct:.1f}% (> {growth_limit:g}%)"
if status != "ok":
failed += 1
print(f"{b['name'][:28]:28} {count:5d} {kb:9.1f} {b['max_kb']:9.1f} {delta:>9} {status} [{comp}]")
if a.write:
a.write.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
print(f"\nwrote measurements to {a.write}")
print(f"\n{len(cfg['budgets'])} budgets, {failed} failing")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.