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.

  • Performance
  • $performance-budgets
  • 144 lines
  • 3 files
  • ~1,650 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- performance-budgets

Installs to ~/.agents/skills/performance-budgets/ for every project on this machine.

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

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.

~/.agents/skills/performance-budgets/SKILL.md

--- name: performance-budgets description: Sets performance budgets and enforces them as CI regression gates - quantity budgets (JS, CSS, image and font kilobytes, request counts), timing… license: MIT ---

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

KindExamplesMeasured byGate stability
QuantityInitial JS ≤ 170 kB gzip, CSS ≤ 50 kB, hero image ≤ 150 kB, ≤ 10 third-party requestsBuild output, Lighthouse resource summaryVery stable: deterministic
Timing (lab)LCP ≤ 2.5 s, TBT ≤ 200 ms, CLS ≤ 0.1 on a throttled profileLighthouse CI, WebPageTestNoisy: medians and tolerance
Timing (field)p75 LCP/INP/CLS in RUM, API p95RUM, APMNot a PR gate: alert and review
Rule/scoreLighthouse performance ≥ 0.9Lighthouse CINoisy, coarse
BackendEndpoint p95 ≤ 300 ms under k6 profile, benchmark time/opLoad test, benchmarksModerate: 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

  1. Start from the goal: Core Web Vitals good thresholds at p75 on mid-range mobile.
  2. 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.
  3. Measure today’s values (production build, median of runs).
  4. 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.
  5. 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 thresholds such as http_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 (benchstat for 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.yamlgenerated from this page

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.py96 lines, 3.5 KB
#!/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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem