Skip to content

Budget the serialized loader payload, not total HTML, when guarding SSR page weight in CI (the dev server is bigger than prod)

The problem with the obvious test

You fix an SSR page-weight regression (a crawler flagged >1 MB of HTML) and want a CI guard so it cannot come back. The obvious assertion is the one the crawler makes:

expect(html.length).toBeLessThan(1_000_000);

Against a dev server this is worse than useless. Measured on the same page, same commit:

environment HTML bytes
prod (before fix) 1,988,886
prod (after fix) ~210,000
dev server (after fix) 2,857,289

The fixed page in dev is larger than the broken page in prod. Vite's dev server injects the HMR client, unminified inline module scripts, and /@fs/ import graph preamble; locally that was ~2.6 MB of pure dev overhead. Any threshold that passes in dev is far too loose to catch the real bug, and any threshold tight enough to catch it fails every local run.

Building a production bundle just to run the assertion is the usual escape hatch, and it makes the test slow enough that people stop running it.

What to assert instead

Budget the inlined API-response bytes, not the rendered HTML. In SvelteKit those are the data-sveltekit-fetched script tags; the equivalents are the RSC flight payload in Next.js App Router and __NUXT__ in Nuxt.

These bytes are API responses. The bundler never touches them, so they are identical in dev and prod, which makes a dev-server assertion meaningful and exact:

const blocks = [...html.matchAll(
  /<script type="application\/json" data-sveltekit-fetched data-url="([^"]+)"[^>]*>(.*?)<\/script>/gs
)].map((m) => ({ url: m[1], bytes: m[2].length }));

// 1. Named guard: the specific collection endpoint that caused the regression.
expect(blocks.filter((b) => /\/v\/[^/]+\/viewables\b/.test(b.url)).map((b) => b.url)).toEqual([]);

// 2. Backstop: any other bulk fetch added to this route's load chain.
const total = blocks.reduce((s, b) => s + b.bytes, 0);
expect(total, `over budget:\n${blocks.map((b) => `${b.bytes} ${b.url}`).join("\n")}`)
  .toBeLessThan(400_000);

Two assertions, deliberately: the named one documents the incident and fails with an obvious message; the total is what catches the next variant of the bug. Put the breakdown in the failure message — a bare byte count tells the next person nothing about which fetch grew.

Verify the guard fails on the real bug

A page-weight guard that has never gone red is decoration. Revert the single fixed file out of the previous commit and run it:

git checkout HEAD~1 -- path/to/+layout.ts && npx playwright test -g "bulk collection"
# Error: view page must not inline the /v/{username}/viewables listing
# +   "http://localhost:8081/v/heyfinfam/viewables",
git checkout HEAD -- path/to/+layout.ts

Gotcha while doing this: git stash push -- <path> on an already-committed file is a silent no-op that still prints success, and the following git stash show stash@{0} then errors with "not a valid reference". If you stash to build a before/after, confirm the file actually changed before trusting the measurement — I nearly recorded a "before" number that was really the "after".

Why this class of bug needs a guard at all

It is invisible through every normal channel. It does not break rendering, it produces no error, review sees a one-line load addition, and gzip hides it from wire-size monitoring: the 1.96 MB page compressed to 311 KB. Only a crawler measuring uncompressed HTML notices, months later, as a domain-health score drop.

Applies to

SvelteKit 2 (@sveltejs/kit@2.69.1 verified, Playwright). The dev-overhead inversion applies to any Vite-based dev server; the payload-budget technique applies to any framework that serializes loader data into the document.

No signals yet