Repository navigation
perf(vite): ship route manifest as a workerd Text module on cloudflare builds (refs #665) - #710
Merged
Merged
Conversation
commit: |
ivogt
marked this pull request as ready for review
July 7, 2026 17:42
ivogt
force-pushed
the
perf/manifest-text-module
branch
from
July 7, 2026 18:32
bc0492b to
ee7eb9c
Compare
…e builds (refs #665) At route scale the per-router manifest chunk inlines trie + precomputedEntries as JSON.parse('<literal>'). That costs the first request twice — the isolate/process lexes and compiles the multi-MB JS source literal before JSON.parse runs — and ships bloated, because the build minifier re-quotes the literal to double quotes, escaping every JSON " (measured 5.71MB vs 4.64MB un-escaped at 26k routes). The only way to fix both is to keep the JSON out of JavaScript source, as a raw asset file that never passes through the JS minifier/compiler. On cloudflare that is a workerd Text module: importing a staged .txt hands the isolate the raw un-escaped bytes as a string with no compile pass (measured ~17% / ~88ms cold first-hit on a real edge deploy at 26k, and it shrinks the worker upload against the hard size limit). The CF vite plugin types .txt as Text; wrangler's default rules type **/*.txt as Text for deploy and dev. Scoped to cloudflare deliberately. A Text module is the only no-JS byte channel needing no filesystem, and cloudflare is where it pays (per- request cold isolates, upload-size limit). node/vercel keep the inline literal: an efficient channel there needs fs (a raw asset read), whose fragility isn't worth a large-app-only win that Fluid Compute amortizes and that essentially no real app reaches. Verified that ?raw is NOT a node/vercel shortcut — in this build it inlines the JSON as a double- quoted compiled JS string literal (same bloat, same compile as inline). Gating: build mode only (dev rebuilds per HMR, parse cost irrelevant); 512KB serialized-payload threshold (~2500 routes — the crossover where the benefit clears the extra artifact; the long tail of normal apps stays byte-identical inline); RANGO_MANIFEST_TEXT=0 forces inline, =1 forces the Text module and bypasses the threshold. Staged filename carries an FNV-1a hash of the router id so distinct ids that sanitize to the same string ("a/b" vs "a.b") don't overwrite each other. Coverage: unit tests over the gate matrix (cloudflare->Text above threshold, inline below/dev, node+vercel always inline even when forced, collision-safe filenames, un-escaped staged payload); cloudflare-basic production e2e (forced via RANGO_MANIFEST_TEXT=1 since its 15KB manifest is below threshold — whole prod suite dogfoods the Text channel on real workerd, asserts the .txt exists, is imported, and is raw un-escaped JSON). bench/{cold-only,edge-cold}.ts A/B harnesses retained.
ivogt
force-pushed
the
perf/manifest-text-module
branch
from
July 7, 2026 20:09
ee7eb9c to
7e768c8
Compare
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
At route scale the per-router manifest chunk inlines the route trie +
precomputedEntriesasJSON.parse('<literal>'). That costs the first request twice — the isolate/process must lex and compile the multi-megabyte JS source literal beforeJSON.parseeven runs — and it ships bloated: the build minifier re-quotes the literal to double quotes, escaping every JSON"as\". Measured at 26k routes: 5.71MB (double-quoted) vs 4.64MB un-escaped, and ~37ms cold manifest-load on Node.The one fact that shaped the design
The only way to fix both the compile and the bloat is to keep the JSON out of JavaScript source — as a raw asset file that never passes through the JS minifier/compiler. Anything that lands the JSON as a JS string literal gets double-quoted and compiled. This was verified empirically in the router's own build, not assumed:
JSON.parse("...")(status quo)import("…?raw").txtText module (raw asset)?rawlooked like a clean universal option but delivers neither win here — the router's minifier double-quotes its inlined string just like the inline literal. Only a raw asset file escapes that.Design — Text module on Cloudflare, inline everywhere else
.txtas a workerd Text module. workerd has no filesystem, so a Text module is the only way to get raw bytes into an isolate without JS compilation. The isolate gets the un-escaped bytes as a string, no compile pass. Measured on a realwrangler deployat 26k routes: 517ms → 429ms cold first hit (~17% / ~88ms), and it shrinks the worker upload against the hard size limit.fs(a raw asset read), whose fragility (asset co-location, runtime ENOENT) isn't worth a win that only appears past ~5,000 routes, that Fluid Compute amortizes, and that essentially no real app reaches.Why Cloudflare-only (the scale calibration)
The win is a large-app phenomenon, and it's concentrated on the edge. Cold-start delta the channel removes, by route count (Node local; edge amplifies ~5–6×):
Real apps land at 200–1,000 routes, where it's ~0–0.3ms locally — so the machinery is scoped to the one runtime where it pays (Cloudflare: per-request cold isolates under load and on every deploy, plus the upload-size limit), and gated behind a 512KB threshold (~2,500 routes) so the entire long tail of normal apps stays byte-identical inline.
Semantics
cloudflare, ≥ 512KB.txt)cloudflare, < 512KBnode/vercel(any size)RANGO_MANIFEST_TEXT=0RANGO_MANIFEST_TEXT=1Bundle hygiene preserved: data still lives in exactly one lazy artifact per router; a Text asset can't be duplicated into an eager chunk. Staged filename carries an FNV-1a hash of the router id so distinct ids that sanitize to the same string (
a/bvsa.b) don't overwrite each other.Code changes
src/vite/discovery/virtual-module-codegen.ts—generatePerRouterModulegains the cloudflare Text-module branch (stage{t,p}JSON tonode_modules/.rango/manifest-<id>-<hash>.txt, import as Text) gated on build mode + preset + 512KB threshold +RANGO_MANIFEST_TEXT;shortHashfor collision-safe filenames. Everything else falls through to the existing inlinejsonParseExpression.tests/cloudflare-basic/playwright.config.ts— production webServer setsRANGO_MANIFEST_TEXT=1(its 15KB manifest is below threshold) so the prod suite dogfoods the Text channel on real workerd.This supersedes the earlier fs-asset (Node/Vercel) channel and its
import.meta.ROLLUP_FILE_URL/co-location machinery and filename-collision bug — dropped after the scaling data and an empirical check showed the fs win is a large-app-only concern real apps don't hit, and?raw(its universal stand-in) delivers no win in this build.Test plan
virtual-module-codegen.test.ts(10) — cloudflare→Text above threshold, inline below/dev,node+vercelalways inline even when forced, collision-safe filenames, un-escaped staged payload,RANGO_MANIFEST_TEXT0/1.manifest-text-module.test.ts— routes resolve through the Text-module manifest on real workerd; asserts the.txtexists, is imported by a chunk, and is raw un-escaped JSON (no\"). 2 passed.pnpm run typecheck,test:unit:all,lint,format,check:e2e-bucketing— green.route-types-hmr(vite-plugin obligation) — 18/18.Notes
precomputedEntries) attacks the ~22msJSON.parsefloor the channel can't touch, is portable, and shrinks the payload everywhere. Ship order would be diet first, this second.