Performance budget — catch heavy pages at build time
Every build weighs each page. A heavy page — an unoptimized hero image, a bloated
inline script, a runaway HTML document — is the single biggest drag on load time,
and it's cheap to catch at build time instead of weeks later as a bad Core Web
Vitals score. StaticOwl runs a page-weight check on every compile and writes the
result to /perf-budget.json next to your published pages.
This is a build-time weight check from data the build already has — not a full Lighthouse run (that would need a headless browser). It measures bytes, which is what you have the most control over on a static site.
What it measures
For every HTML page the build emits, the check records:
| Field | Meaning |
|---|---|
htmlBytes |
Size of the HTML document itself. |
assetBytes |
Combined size of the local assets the page references (images, scripts, stylesheets, the generated social card). |
totalBytes |
htmlBytes + assetBytes — the page's transfer weight. |
assetCount |
How many local assets resolved to a build file. |
unresolved |
Referenced local paths that didn't resolve to a build file (a missing asset, or an off-site reference that can't be weighed). Informational. |
Asset references are pulled from src= attributes, <link> tags for
stylesheets, preloads and icons, and url(...) in inline CSS. Off-site URLs
(https://…), data: URIs, and anchors are ignored — only local files that ship
with your site are counted.
The check also flags any single image artifact that is over budget on its own, independent of which page uses it. SVGs are exempt from the image size check.
The budgets
Three thresholds, applied to every build:
| Budget | Default | Applies to |
|---|---|---|
| HTML document | 150 KB | A single .html file. |
| Page weight | 2 MB | HTML + all of a page's local assets. |
| Single image | 600 KB | Any one local image file (SVG exempt). |
A page or image that exceeds its budget produces a violation. Each violation
records the path, the kind (page_html, page_weight, or image), the measured
bytes, and the limit it broke.
The /perf-budget.json artifact
The report is emitted on every build and served at your site root, so you can fetch it the same way as any page:
curl https://<your-site>/perf-budget.json
Its shape:
{
"generatedAt": "2026-10-16T12:00:00.000Z",
"budget": { "maxPageBytes": 150000, "maxPageWeightBytes": 2000000, "maxImageBytes": 600000 },
"pagesScanned": 42,
"pages": [ { "path": "/", "htmlBytes": 48213, "assetBytes": 910044, "totalBytes": 958257, "assetCount": 11, "unresolved": 0 } ],
"worst": [ "… top 10 pages by total weight …" ],
"violations": [ { "path": "/gallery/", "kind": "page_weight", "bytes": 2411233, "limit": 2000000 } ],
"totalViolations": 1
}
pageslists every HTML page, heaviest first.worstis the top 10 by total weight — a compact summary when you don't want to read the whole list.violationsis only the pages and images that broke a budget.
How to read and act on it
- Start with
worst. Those are your heaviest pages. A highassetBytespoints at images or scripts; a highhtmlBytespoints at the document itself (often a large inlined block or a very long page). - Work the
violations. Apage_weightviolation usually comes down to one or two oversized images — compress them, serve a smaller variant, or lazy-load below-the-fold media. Animageviolation names the exact file to re-export. Apage_htmlviolation means the document itself is too large — split the page or move large inline blocks to referenced assets. - Watch
unresolved. A non-zero count usually means a page references an asset path that didn't get built — a broken image or stylesheet link worth fixing on its own.
Report-only by default — or a hard gate when you opt in
Out of the box the budget check never fails a build. When a page is over budget,
the build logs a warning with the count and a few examples, writes
/perf-budget.json, and still publishes. Treat it as a signal to act on.
If you'd rather a heavy page block the publish, turn on enforcement:
- Per site — set
perfBudgetEnforce: truein the site'spublishConfig. This is the one to reach for: it affects only that site. - Platform-wide — on a self-hosted StaticOwl, set the environment variable
STATICOWL_PERF_BUDGET_ENFORCE=1to make enforcement the default for every site. A per-sitepublishConfig.perfBudgetEnforce(true or false) always overrides the environment default, so one site can opt out of a platform-wide gate (or into one that's otherwise off).
With enforcement on, a build that has any violation fails and does not deploy —
the over-budget pages come back as build errors (visible in the build result and
in build_last / the build event), and /perf-budget.json is still written so you
can see exactly what broke. Fix the pages, or raise the budget, and rebuild.
Two things enforcement never does: it won't block a rollback or replay of a
previously-good build (those always re-publish), and it won't change what gets
measured — the report is identical whether or not you enforce. If you'd rather gate
in your own external pipeline instead, read /perf-budget.json after a build and
fail on totalViolations > 0.
See also
- Media — images, files, and the CDN — where your images live and how to serve optimized variants.
- Publishing & environments — how builds and publishing fit together.
- Accessibility — the other build-time quality check, which runs in the same pass.