ReviewOS

stacks/bun-router

perf: defer optional native crypto across the public router entry

#883
Closed chris opened this 8 days ago ·
8 days ago

Tracking the dependency-level prerequisite discovered in Stacks #2450.

The public @stacksjs/bun-router 0.1.11 entry and its eagerly imported chunks initialize node:crypto even for a bare router. In source, static imports are present in auth.ts and middleware/{csrf,response_cache,performance_monitor,file_upload,request_id}.ts. These imports survive into the published entry/chunks because the barrel re-exports the features.

On Bun 1.3.14/macOS arm64, a fresh node:crypto import alone costs about 4 ms and 5 MB RSS. This is a direction-only diagnostic, not an amount that can simply be subtracted from application startup. A proposed Stacks-only deferral showed no RSS improvement because these upstream imports already initialize the same native module; that candidate was discarded.

The source checkout is on main at 47153e4. Investigate deferring optional native crypto initialization across the actual eager graph, using standard runtime APIs and preserving the same cryptographic primitives and synchronous public contracts. Do not patch installed dependencies, hide imports from the bundler with constructed strings, or replace constant-time verification with ordinary equality.

Acceptance: prove the built public entry's eager graph no longer initializes optional native crypto, measure fresh source and built entry startup/RSS, exercise cold and warm authentication/CSRF/signature/hash/request-ID paths, run router tests/lint/typechecks, and link the implementation commit and downstream adoption requirement.

8 days ago

Implemented and pushed on main in 272f696, with the repository's configured Chris identity.

The six modules now use one cached, literal require('node:crypto'), preserving synchronous behavior and the same native comparison, hashing, randomness, and UUID primitives. Production bundling retains the require inside the loader function. No constructed imports, replacement cryptography, installed-dependency patches, or dependency externalization were needed. ResponseCache is a direct module, not a public barrel export; the other five modules participate in the public entry.

Local diagnostic results on Bun 1.3.14, macOS arm64, NODE_ENV=production:

MeasurementBefore 47153e4After 272f696
Built import + Router construction + one route, median16.740 ms16.288 ms
Built final RSS, median37,535,744 B36,945,920 B
Built RSS delta, median13,582,336 B13,000,704 B
Source import + construction + route, median25.310 ms23.873 ms
Source final RSS, median56,508,416 B55,304,192 B
First JWT verification, median0.185 ms0.849 ms
Warm 200,000 JWT verifications, median CPU285.575 ms284.424 ms

Built measurements use 30 fresh processes per variant, alternating order. Source measurements use 30 fresh processes per variant in sequential blocks and are more susceptible to drift. Warm measurements use 15 alternating pairs, 30,000 warmup verifications, then 200,000 measured verifications. Warm CPU ranges overlap substantially: 273.572-309.949 ms before and 276.205-318.158 ms after. Do not interpret the small warm median difference as a throughput gain. The first crypto-using request pays the deferred loading cost; applications that immediately use crypto should not expect the full bare-router startup saving.

The built startup improvement is about 2.7%, with about 0.59 MB lower final RSS. These are direction-only local measurements, not an HTTP benchmark or a framework ranking. The canonical benchmark environment/runtime still needs its own run. The larger standalone crypto-import diagnostic in the original issue was not realized as an equivalent application saving.

Reproduction: build both revisions with the package build's three entrypoints (src/index.ts, src/cli.ts, src/container/index.ts), splitting: true, target: 'bun', format: 'esm', minify: true, and the existing external list. Save each build to a separate temporary directory. Run this fixture in a fresh Bun process from a directory without project preloads, passing each built index.js or source src/index.ts path:

const start = performance.now()
const before = process.memoryUsage().rss
const { Router } = await import(process.argv[2])
const router = new Router()
router.get('/ready', () => ({ ready: true }))
const after = process.memoryUsage().rss
console.log(JSON.stringify({ ms: performance.now() - start, rss: after, delta: after - before }))

For the warm check, construct new JWT('benchmark-secret'), sign { sub: 'user' }, time the first verify, warm with 30,000 verifies, then time/measure process.cpuUsage around 200,000 verifies and assert every returned subject is user.

Validation: 531 tests / 0 failures; bun run lint, bun run typecheck, and bun run build all pass. The emitted static-import regression fails before the change and passes after it. It follows only eagerly reachable output chunks, excluding dynamic bunfig chunks. The scanner establishes the absence of static crypto imports; manual inspection establishes that the remaining require call is inside the lazy function. Native built-in initialization is not exposed by Bun's require.cache and was not mocked.

Fresh subprocess tests cover first and repeated JWT valid/tampered checks, issued/mismatched CSRF tokens, generated/preserved request IDs, unique upload names and file contents, monitoring/profiling IDs, and literal expected cache ETags plus cache hits. All five public features are exercised against production-style built output; all six are exercised from source. Both review axes are clear.

Downstream adoption remains tracked in Stacks #2450. This upstream commit has not changed the published dependency installed by Stacks, so no Stacks startup saving is claimed yet.

Sign in to comment on this issue.