Manual contentsTestingBrowse 140 chapters
Manual 8 min read

Page Budget

A page budget checks the document and the assets it fetches. The test helper uses the application's in-process HTTP path. It needs no browser, port or network. The console command reads a HAR capture or fetches a live URL. All three modes use PageBudget and the same evaluator. Budgets measure the application's own resources: the document and same-origin assets. Third-party hosts are listed and governed by the configured allowed host list because the application controls neither their bytes nor their headers. HAR entries from other origins do not count toward transfer, asset, raster, font, cache or cookie rules.

Test a page

Pass the application's router and middleware to TestClient. Use an absolute URL when the document contains absolute asset URLs. Relative paths use http://localhost.

use suprnova::page_budget::PageBudget;
use suprnova::testing::TestClient;
use suprnova::{HttpResponse, MiddlewareRegistry, Request, Router};

#[tokio::test]
async fn public_page_stays_within_budget() -> Result<(), suprnova::FrameworkError> {
    let router = Router::new().get("/", |_req: Request| async {
        Ok(HttpResponse::html("<h1>Hello</h1>"))
    });
    let client = TestClient::new(router, MiddlewareRegistry::new());
    client.audit_page("/", &PageBudget::default()).await?.ensure_passes()
}

The helper fetches stylesheets, scripts, images, source sets, icons, manifests and preloads. It follows video and audio sources, video posters and CSS URLs in style attributes and stylesheets, including @font-face fonts, imports and string candidates in image-set(). It fetches manifest icons only when the document references the manifest. It deduplicates URLs and records external hosts without requesting them. All srcset candidates count; the helper does not choose a viewport. Data URL commas do not hide later candidates. Every asset request carries no cookie. The client's login and cookie jar do not affect the audit. A refused or redirected asset fails the audit.

React, Vue, Svelte and Live kits ship tests/page_budget.rs over their five public entry pages. The API kit has no public pages and ships no page test.

Set limits

The defaults use decimal bytes. Total transferred response bodies are limited to 1,000,000 bytes. An asset is limited to 250,000 bytes. Fonts together are limited to 100,000 bytes. A raster above 100,000 decoded bytes needs a WebP or AVIF alternative and a fallback. A retained PNG above that threshold must be within 10 percent of oxipng's fast preset. Manifest icons follow the same raster rule. A PNG fallback needs another entry in the same manifest with type image/webp or image/avif and the same sizes. The PNG compression rule still applies to that fallback.

use suprnova::page_budget::PageBudget;

let budget = PageBudget {
    total_transfer_bytes: 800_000,
    asset_bytes: 200_000,
    font_bytes: 80_000,
    ..PageBudget::default()
};

These limits expose the captured 1.4 MB page, its large PNG and its icon font. They leave room for a starter page. Set limits for the application's content and measure changes in review. PNG recompression is lossless and preserves the media pipeline's colour and metadata policy. Lossy media output keeps the framework's default quality of 85.

The configured Vite build manifest identifies build outputs in each entry's file, css and assets. A listed file receives immutable caching only when its filename has a hyphen or dot followed by at least eight base64url characters (A-Z, a-z, 0-9, _ or -) before the extension. This includes app-deadbeef.js; mutable names such as index.html and app.js retain the revalidating policy. Empty outputs, absolute URLs or paths, and outputs containing .. or a backslash are ignored. The static handler, publisher and all audit modes share this classification and read the installed InertiaConfig and its manifest_path. For files outside that manifest, fingerprint detection uses a configurable regular expression against the file name without its extension. The fallback matches Vite's eight-character hash when it contains a digit, uppercase letter, underscore or hyphen, in names such as main-BjmpJyAU.js. An all-lowercase hash outside the manifest is not recognized by the fallback; configure the pattern for it. Fingerprinted assets require public, max-age=31536000, immutable. Other static files default to public, max-age=3600, or the handler's configured cache policy. Static file responses skip session persistence and cookie issuance only when the handler changes no session state, issues no CSRF token and queues no security work. Automatic flash aging still marks the request copy dirty for CSRF and other readers. It does not advance the separate framework mutation marker used by the asset gate. Aging alone does not force asset persistence. When the gate skips finalization, stored flash survives for the following page. Token issuance finalizes normally. A shared-cacheable response that exposes the token becomes private, no-store. Remember credential rotation, regeneration, identity changes and queued sign-in rebinds also finalize normally and deliver any required cookies. Responses carrying those cookies become private. On an ordinary shared-cacheable page, automatic aging keeps loaded flash dirty. CSRF's usual XSRF-cookie rules see that dirty state. If CSRF attaches the cookie, session finalization sends Cache-Control: private, no-store.

Select another naming convention on both the audit and the static handler. The expression matches the filename without its extension. An invalid handler pattern returns an error on its first request.

use suprnova::{StaticFiles, page_budget::PageBudget};

let pattern = r"\.v[0-9]+$";
let files = StaticFiles::public().fingerprint(pattern).handler();
let budget = PageBudget { fingerprint_pattern: pattern.into(), ..PageBudget::default() };

Audit a capture or URL

Run the application's console binary. Capture a fresh visit with response bodies. A finding returns a non-zero exit code. Each row names the asset, rule, measured value and allowed value. The output also includes the request count, transfer bytes and third-party hosts.

cargo run --bin console -- budget:audit capture.har
cargo run --bin console -- budget:audit https://example.com/

HTTP requests have a ten-second timeout. The whole collection has a thirty-second timeout, a limit of 256 resources and a 16 MiB response limit. budget:audit reads these collection limits through PageBudget::from_env(). The resource limit counts normalized HTTP and HTTPS URLs after deduplication. Data URLs and fragment-only references count zero. A separate limit of 65,536 work units meters HTML and CSS tokens, JSON keys and values, and raw reference candidates. Duplicate and ignored candidates also consume work. CSS whitespace, comments and complete function argument lists consume tokens. A token can span many bytes, so the response byte limit separately bounds its lexical size. Set max_discovery_tokens in Rust or PAGE_BUDGET_MAX_DISCOVERY_TOKENS for the command. Discovery stops HTML input on overflow, parses inline styles on their tags and processes complete CSS constructs from style blocks before later markup. Only an unfinished CSS construct is retained between character chunks; the response byte limit bounds its size. Manifest keys and values consume work, including scalars, strings and entries inside ignored extension objects and arrays. The parser checks the allowance before reading each value. Icons are decoded one at a time. Exhaustion stops before the next value is consumed, without materializing the remaining array. Each image-set() is a modern-plus-fallback group, like a picture or source set. The pending queue is bounded too. Repeating one reference does not allocate another queue entry. Every Cache-Control header line contributes to the policy; a second no-cache line fails the immutable rule. Redirects are recorded before following same-origin locations. The initial HTTP-to-HTTPS upgrade may be followed on the same host; it still fails the first-redirect rule. Other external redirects are listed.

PageBudget::default() keeps the shipped values. PageBudget::from_env() reads one variable per field and rejects invalid values with the variable's name. Boolean values use literal true or false. Host lists use commas; spaces around each host are ignored. Unset lists only report hosts. An empty list rejects every external host. An invalid fingerprint expression fails before collection.

PAGE_BUDGET_TOTAL_BYTES=800000 PAGE_BUDGET_FONT_BYTES=80000 cargo run --bin console -- budget:audit capture.har
Variable Default Type Purpose
PAGE_BUDGET_TOTAL_BYTES 1000000 u64 Total transferred response bytes.
PAGE_BUDGET_ASSET_BYTES 250000 u64 Transferred bytes per asset.
PAGE_BUDGET_RASTER_THRESHOLD_BYTES 100000 u64 Decoded bytes above which a raster needs a modern format and fallback.
PAGE_BUDGET_PNG_MARGIN_PERCENT 10 u32 Margin above the fast PNG recompression size.
PAGE_BUDGET_FONT_BYTES 100000 u64 Combined transferred font bytes.
PAGE_BUDGET_REQUIRE_IMMUTABLE true bool Check fingerprinted cache policy.
PAGE_BUDGET_FORBID_ASSET_SET_COOKIE true bool Reject asset response cookies.
PAGE_BUDGET_FORBID_ASSET_REQUEST_COOKIE true bool Reject captured asset request cookies.
PAGE_BUDGET_REQUIRE_COOKIE_INDEPENDENT true bool Require successful anonymous asset access.
PAGE_BUDGET_FORBID_FIRST_REDIRECT true bool Reject an initial document redirect.
PAGE_BUDGET_FINGERPRINT_PATTERN Strict Vite hash expression regex Match the filename stem.
PAGE_BUDGET_ALLOWED_THIRD_PARTY_HOSTS unset comma-separated hosts Restrict external hosts when set. Empty forbids all external hosts.
PAGE_BUDGET_MAX_RESOURCES 256 usize Bound discovery and queued resources.
PAGE_BUDGET_MAX_DISCOVERY_TOKENS 65536 usize Bound parser tokens and raw reference candidates across collection.
PAGE_BUDGET_MAX_RESPONSE_BYTES 16777216 usize Bound one response body.
PAGE_BUDGET_REQUEST_TIMEOUT_SECS 10 u64 Bound one HTTP request in seconds.
PAGE_BUDGET_COLLECTION_TIMEOUT_SECS 30 u64 Bound the complete collection in seconds.

HAR response bodySize measures transferred bytes; content.size measures decoded bytes. When bodySize is unknown, the audit uses content.size. A capture without PNG bytes cannot prove compression and reports that missing evidence. Captured request cookies fail the request-cookie rule; they show that cookies were sent, not that the server required them. The live and test runners prove cookie independence by requesting assets without cookies. Cookie independence also fails when a capture carries cookies, even when the separate request-cookie privacy rule is disabled. A bodyless HAR cannot prove document fallback relationships for a large raster. Export response bodies when those rules matter.

Why Suprnova diverges

Laravel's HTTP tests inspect individual responses. Suprnova follows the page's assets so a test catches transfer size, image format, font size and cache policy together. The same rules run against a deployment capture. The audit does not execute JavaScript. Assets created only by client-side code appear in a HAR audit when the capture records them.

Next

Read HTTP Tests, Testing, Starter Kits and Images.