Skip to content

Acceptance gates

Run these before a release. The same checks run in PR and release CI.

CommandWhat it checks
npm testShared React SSR tests across Node, Express, Fastify, Hono and edge; real React cancellation before/after shell; HTTP/2, redirects, cookies, HEAD/statuses, bounded buffering and compressed shell delivery
npm run lint:check / npm run ts:checkLint and public/internal TypeScript contracts
npm run buildPublished JavaScript and declaration output
npm run test:sizeBrowser public-entry and combined gzip budgets; rejects server dependencies in browser bundles (requires lib/)
npm run test:edge:packedBuilt edge output running in Miniflare/workerd
npm run test:worker:packedPacked Worker template, Cloudflare types and static/lazy route assets
npm run test:bunBuilt Fetch SSR served by Bun, including cookies, HEAD, redirects and POST (requires Bun)
npm run test:core:no-optionalPacked installation with --omit=optional; core and edge run without Express or compression
npm run test:templateTemplate SSR in dev/production, cold styles, SSR module reload, streamed and crawler HTML, gzip, static JS/CSS, redirects, HEAD, 404, subpath deployment, standalone SPA, client gzip budget, production cold-start/RSS budgets and baseline TTFB comparison
npm run docs:buildDocumentation build and links

CI pins vite-template to d15557c1d03a16a97521d38432a22cd8fe2b95c3. Locally, install its dependencies in ../vite-template, or pass its path to node scripts/test-template.mjs. The script works on a copy; route typing, HMR edits and subpath configuration changes stay in that copy.

In development, production and production under a basename, /deferred must send its title and promise placeholder in the first HTML chunk's __ssrBoostStream init frame, then a resolve frame and the three rendered users in later chunks. A separate Googlebot request must contain the resolved lists with zero pending Suspense markers (<!--$?-->). The summary records the delay from first HTML to the resolve frame; the template's loader waits 1.5 seconds.

Before shipping, also check the template in a browser: hydration and console errors, client navigation, Suspense, crawler mode, HMR and SPA deep links. HTTP acceptance checks do not replace browser checks. Use SSR_BOOST_KEEP_TEMPLATE=1 npm run test:template to retain the test copy for inspection. With Chromium installed (npx playwright install chromium), run SSR_BOOST_TEMPLATE_BROWSER=1 npm run test:template to also test the pinned template's deferred page in Chromium in all three SSR modes. This checks shell interactivity, both Await/use() lists, init/resolve frames and hydration/console errors. Browser failures fail this opt-in gate.

PR and release CI run the full suite on React/React DOM 18.2.0 and 19.2.8. The release waits for both versions. Template TTFB comparisons are advisory, including the streamed/home ratio. The ratio compares 11 alternating pairs after three warm-up pairs, using the first decoded HTML chunk with the same browser user agent and identity encoding. A ratio above 1.5 is printed without failing acceptance; early-stream checks retry up to three times to tolerate shared-runner scheduling. Crawler rendering is checked through Suspense completion markers instead of comparing timings between separate requests.

Production startup and memory budgets

Both pinned and SSR_BOOST_TEMPLATE_CURRENT=1 acceptance enforce these limits in the same run:

MetricFailure threshold
Production cold startAt most 2 × the plain baseline median measured in the same run
Production server RSS after TTFBAt most 1.6 × the plain baseline RSS measured in the same run
Production retained heap growth after 10,000 additional requestsAt most the plain baseline's retained-heap delta + 8 MiB, both measured after forced GC

The plain baseline is an ESM Express server rendering one React element with react-dom/server's renderToPipeableStream, using the template's installed Express/React versions. Both servers run with NODE_ENV=production and the same memory preload. Cold-start samples use npm; cold start is npm process spawn to the first complete HTTP 200 on /, polling every 25 ms. Five fresh processes per implementation are interleaved; the median is compared without rounding. Filesystem caches stay warm, matching the public benchmark's process-cold methodology.

RSS comes from process.memoryUsage() in the listening Node process, immediately after the existing TTFB run (two warm-up requests and seven samples). The candidate also exercises the existing production HTTP checks before TTFB. The runner records the listening PID and requests a sample by signal; it does not measure npm's RSS or add an application endpoint, and it records RSS before the forced collections used for the retained-heap rows.

After each server's TTFB sample, the runner completes exactly 10,000 additional / requests with the TTFB run's browser user agent, identity encoding and ten HTTP/1.1 keep-alive connections, consumes every body, then samples memory again in the same process. Every response must be HTTP 200. Both servers run with --expose-gc; each memory sample records resident memory as-is and then the heap in use after two forced collections. The budget compares the retained-heap deltas (after load minus after TTFB) because resident memory also grows with GC timing and allocator high-water marks: a plain Express server gains tens of MiB of RSS over 10,000 requests while its retained heap stays flat. The RSS rows after load stay in the summary as advisory values; 1 MiB = 1,048,576 bytes. Use heap snapshots to attribute a retained-heap regression.

The step summary prints both candidate/baseline cold-start medians and RSS values alongside the existing advisory Production server ready line, which retains the direct CLI spawn and its 100 ms readiness polling. Readiness and TTFB remain advisory; the new cold-start and RSS comparisons fail acceptance. A separate untimed production probe rejects tooling imports and makes Vite config evaluation fail, while checking the root and a lazy route. The missing-build error is also checked. See deployment for the serving import path and profiling commands.

Size budgets

PR and release CI run npm run test:size immediately after the library build. The script expands the JavaScript targets in package.json's exports map against lib/ and deduplicates extensionless and .js aliases. It excludes server/core/edge/Node runtimes, adapters, CLI, plugins, build services, their server/tooling helper and constant paths, and declaration-only stubs. The precise exclusions are documented in scripts/test-browser-size.mjs; shared browser modules such as context/server and helpers/get-server-state remain covered. New browser entries fail until they have a budget.

Each entry is bundled and minified with esbuild as browser ESM. React, React DOM (including react-dom/client and react/jsx-runtime) and React Router stay external. All other dependencies, including the client HOCs' hoist-non-react-statics, count toward size. The combined bundle imports and re-exports every entry's namespace to retain all public APIs while sharing dependencies. Any import of node:, express, compression, isbot, chalk, commander or json5 (including package subpaths) fails the gate, even when the size is within budget.

All sizes below use gzip level 9 and KB = 1024 bytes. Comparisons use unrounded byte counts.

Browser entryMeasured gzip KBBudget KB
browser/entry3.1253.669
browser/stream2.4693.25
components/navigate0.3340.50
components/only-client0.3240.50
components/render-client1.7052.25
components/response-status0.1720.25
components/scroll-to-top0.2030.50
components/with-suspense1.6282.25
constants/common0.0940.25
context/server0.1750.25
helpers/get-server-state0.1010.25
helpers/import-route1.9372.50
interfaces/fc-route0.0910.25
Combined5.727 (5864 bytes)6.657 (6817 bytes)
Template client total141.839154

The template gate sums the gzip size of each build/client/assets/*.js file after the candidate's production SSR build, including lazy chunks and framework/application dependencies. It excludes CSS, images, source maps and server output. This is the full pinned acceptance template, so its total is larger than the minimal example's bundle. Both pinned and current dependency acceptance runs use the same 154 KB limit.

The browser table is printed and appended to GITHUB_STEP_SUMMARY when set. Template acceptance also prints and appends its client size/budget, production cold-start/RSS budgets, server readiness (process start through the first successful HTTP response, including readiness polling), baseline/candidate median TTFB, and development/production/subpath chunk counts and deferred settle delays. Decoded gzip chunks count HTML payload rather than gzip headers. Available measurements are reported even if a later acceptance check fails; unreached timings are marked not measured. Readiness and TTFB remain advisory.

For an intentional increase:

  1. Run npm run build, npm run test:size and template acceptance with the pinned template and current dependencies. Review the added code/dependencies and record the measured before/after sizes and the reason in the PR.
  2. Update the table at the top of scripts/test-browser-size.mjs: each entry's budget is Math.ceil(measuredGzipKB * 1.25 * 4) / 4 (25% headroom, rounded up to 0.25 KB). The combined budget is the measured combined gzip size plus exactly 1 KB; retain byte precision.
  3. Set TEMPLATE_CLIENT_GZIP_BUDGET_KB near the top of scripts/test-template.mjs to Math.ceil(measuredGzipKB * 1.05) using the pinned template (5% headroom, rounded up to a whole KB). Confirm the current-dependency run also fits; investigate any difference before raising it.
  4. Update this table, rerun the gates, and temporarily lower a budget to prove that CI would fail. Restore the reviewed budget before committing. Never update limits automatically on failure.