Skip to content

Client performance budgets

npx warlock build reports each page’s first-visit JavaScript and CSS after the Vite client build. Existing apps only receive a report until you configure limits. New projects start with 150kb JavaScript and 50kb CSS limits.

warlock.config.ts
import { defineConfig } from "@warlock.js/core";
export default defineConfig({
build: {
budgets: {
page: { js: "150kb", css: "50kb" },
pages: {
"/admin/**": { js: "220kb" },
"/admin/reports": { js: "300kb", css: "75kb" },
},
mode: "error",
},
},
});

Sizes accept b, kb, and mb (case insensitive), with 1024 bytes per kb. Unspecified fields are unlimited; equality passes. page sets defaults. pages keys match route paths: * stays within a segment, ** spans segments, and ? matches one non-slash character. Later matching rules override earlier fields while preserving other defaults. Other glob syntax is rejected.

Configured budgets default to mode: "error", which fails the build and restores the previous dist. "warn" prints violations and completes normally. Each violation names the page, JS or CSS budget, actual compressed size and the largest contributing modules. API-only builds have an empty page report.

build.budgets.payload limits the hydration data a single rendered document embeds in its HTML: the page, layout and app data, translations, session, the other hydration slots and the page’s browser route table.

warlock.config.ts
export default defineConfig({
build: {
budgets: {
payload: {
page: "64kb",
pages: { "/admin/**": "128kb" },
mode: "warn",
},
},
},
});

page is the limit for every document, and it defaults to 64kb. pages uses the same route-path globs as the client budgets, and the later matching entry sets the limit. mode is "warn" (the default) or "error". At runtime "warn" logs a warning and "error" logs at error level; neither fails a request. At build, "error" fails warlock build and "warn" prints a warning.

A page’s payload has seven sections. warlock build can measure two of them from what the build knows, for every page, and checks their sum against the page’s limit:

SectionAt buildHow
translationsmeasuredThe page’s pruned dictionary (shared groups excluded) plus the shared-table pointer, for the largest locale. An upper bound: it prunes the whole dictionary, not only the page’s own keys.
routesmeasuredThe browser route table of the named API routes the page’s client code reaches. An upper bound: web.namedApiRoutes filters, site scoping and the guest rule only remove routes.
pageruntimeLoader data exists only in a request.
layoutruntimeSame.
appruntimeSame.
sessionruntimeResolved per request.
otherruntimeLocale, route identity and the other hydration slots.

A build failure lists each page over its limit with the per-section breakdown (translations, routes) and states which sections were not counted, because the unmeasured sections only add to the total: a page over its limit at build is over it at runtime too, up to the upper-bound caveats above. A build that passes says nothing about page data; the runtime check covers it. The build does not render pages, so there is no build-time measurement of loader data. Use page.measurePayload({ limit }) in an end-to-end test to measure a rendered page’s full payload before release.

The full measurement is a runtime one. A page’s payload depends on its loaders, which only a request runs, so warlock build --json does not report it. Documents are measured until their route is reported, once per process. When a route is over budget, Warlock logs one line for that route with the size of each section (page, layout, app, translations, routes, session, other) and the three largest data keys. The check adds one serialization of the payload to each document. Set page above what a healthy page embeds, or raise it for one route, rather than turning the warning off.

The end-to-end kit records the same measurement for a loaded document with page.measurePayload({ limit }) (see Browser end-to-end tests), which adds it to the run’s evidence under payloads.

The first visit includes hydration, the requested page, its layouts and root, setup registration modules, and the error boundary. From those roots the report walks the static imports and CSS references in Vite’s .vite/manifest.json. Each emitted file is gzipped individually and counted once per page. Shared chunks count toward every page that needs them. The report excludes HTML, fonts, images, HTTP headers and cache reuse.

Dynamic imports beyond the required page roots and their static dependencies are reported in separate JS/CSS columns, including their transitive imports. They do not count against first-visit limits. This includes other pages exposed by the same hydration registry, so deferred totals describe reachable assets, not a prediction of what a user will download.

The ten largest modules use Rollup’s chunk.modules[].renderedLength, recorded by a small build plugin. These are uncompressed rendered-byte estimates for JS chunk contributors, including zero-length stylesheet wrapper modules. They are diagnostic rankings rather than compressed per-module costs.

The table sorts by combined JS and CSS size, descending, with route names breaking ties. Deltas compare the same route and source path with the previous successful build’s build-budgets.json in build.outdir (normally dist). Keep that output directory between builds to retain the baseline. First builds and new pages display no delta. Failed builds preserve the earlier baseline.

Terminal window
npx warlock build --json > build-report.json

Stdout contains one JSON object: { success, budgets, error? }; progress is sent to stderr. budgets contains version: 1, compression: "gzip", pages and violations. Each page records its route (page), source, byte totals (js, css), dynamic totals, files, modules and optional delta. Violations contain page, type, numeric budget and actual, and modules. A budget failure returns a nonzero exit status while still printing the attempted report; an earlier build failure may have no budgets value.

The full module attribution is private build metadata in client/.vite/budget-modules.json. Production asset routes do not expose that folder. No extra dependency is required.