Repository navigation
feat(router): resolve clientChunks follow-up gates; default-on is the 1.0 stance - #536
Merged
ivogt merged 1 commit intoJun 5, 2026
Conversation
… 1.0 stance Follow-up to #535. Closes the four deferred gates in docs/research/build-chunking-plan.md with a deterministic stance: per-route client splitting (clientChunks) ships DEFAULT-ON for 1.0. Optimized for apps at scale (where shipping every route's client code on every visit is most wasteful); small apps where the split is a wash opt out with one line. Gate 1 (cost-side benchmark): tools/bench-client-chunks.mjs measures vite-rsc-demo both ways. First-load app bytes drop 38.3 KB -> 13-19 KB (~51-66%); +1 multiplexed request; +5% total client JS, paid only on a full cold crawl. The win scales with app size. Gate 2 (min-chunk-size coalescing): reasoned WON'T-FIX. Byte-based coalescing is structurally impossible inside plugin-rsc's clientChunks callback (grouping is finalized before chunks are rendered; the callback only sees id/normalizedId/ serverChunk) and unnecessary (route-lazy chunks impose no first-load tax). Real apps produce multi-KB groups; toy apps opt out with clientChunks: false. Gate 3 (CSS ordering/FOUC): mini gains a /combined route that renders two route groups' CSS on one page; e2e asserts both stylesheets apply with the correct cascade, deterministically across a reload, in dev and production. Gate 4 (convention completeness): the clientChunks function is the configurability path for unrecognized layouts (not an ever-growing marker list); new rango:chunks debug namespace makes the previously-silent shared fallback observable. Also fixed a stale doc claim (unmarked paths inherit the default grouping, not a parent-named app-components chunk). Validation: typecheck, unit (3315), lint, format, mini clientChunks e2e (dev+prod incl. /combined), vite-rsc-demo kanban production e2e — all green.
8 tasks done
ivogt
added a commit
that referenced
this pull request
Jun 5, 2026
…for 1.0) (#535) Per-route client ("use client") splitting, on by default. Groups components by route id; flat and host-split layouts inherit the default grouping unchanged. Opt out with clientChunks: false, or a function for full control. Includes the follow-up that resolved the four deferred gates (was #536, merged into this branch): cost-side benchmark (first-load app bytes ~51-66% lower, +1 multiplexed request, +5% total only on a full cold crawl), coalescing won't-fix rationale, multi-group CSS co-render e2e (mini /combined, dev+prod), and rango:chunks observability. Default-on is the settled 1.0 stance.
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
PR #535 shipped per-route client splitting (
clientChunks) default-on butexplicitly labeled it "see it in action, not proven optimal", deferring four gates
to a follow-up (see
docs/research/build-chunking-plan.md). Until those gates wereclosed, a consumer had no measured reason to trust the default, no way to see
why their layout did or didn't split, and the user guide carried a stale claim
about the fallback behavior. This PR closes all four gates and takes a
deterministic stance.
Verdict:
clientChunksships DEFAULT-ON for 1.0Optimized for apps at scale — the larger the app, the more wasteful it is to ship
every route's client code on every visit, and the smaller the fraction any single
user downloads. Apps where every route's client code is trivial (the small-app
case) opt out with one line. No public API shape changed; this PR makes the
existing default provably correct and observable.
What changes for a consumer
DEBUG=rango:chunks pnpm buildlogs every module's group or its shared-fallback reasonsrc/parts/…)app-componentschunk (wrong)clientChunksfunction for full controlnode tools/bench-client-chunks.mjs <dist-off> <dist-on>Before vs. after — observing the split (new)
The second line is the actionable case: app code that did not split by route.
The consumer either colocates it under a marker dir or passes a
clientChunksfunction — which is the supported configurability path, not widening an infinite
marker list.
End-to-end consumer example — the one-line opt-out
The four gates, resolved
Gate 1 — cost-side benchmark: PASSED
tools/bench-client-chunks.mjsmeasurestests/vite-rsc-demo(5 feature routes,~33 KB gz app client code) built both ways:
clientChunks: falseHonest cost side: +1 first-load request and +5% total client JS (per-chunk gzip
overhead), the latter fully paid only on a cold crawl of every route in one
session. Land-on-one-route and typical 1–3 route sessions both win. The win is a
function of app size — the baseline's every-route app chunk grows with total
app code, the split's per-route first-load does not.
Gate 2 — min-chunk-size coalescing: WON'T-FIX (by design)
clientReferenceMetaMapbefore any client chunk is rendered; the callback only sees{id, normalizedId, serverChunk}. Emitted byte sizes (whatexperimentalMinChunkSize, removed in Vite 8/Rolldown, keyed on) don't exist yet.vite-rsc-demo's groups are 3.7–9.4 KB. Sub-1 KB groups only appear in toy apps (mini), which opt out withclientChunks: false.Reopen trigger: a real consumer report of pathological fragmentation → the fix
would be a post-build chunk-merge pass (the only place real sizes exist), not a
callback change.
Gate 3 — CSS ordering / FOUC: COVERED
New e2e (
e2e/mini.test.ts,/combined) renders client components from tworoute groups on one page — the case where per-group
<link>precedence actuallyinteracts (
vite-plugin-react#1100). Asserts both stylesheets apply with thecorrect cascade, deterministically across a reload, in dev and production.
mini gains a
/combinedroute (and the syncedrouter.named-routes.gen.ts).Gate 4 — convention completeness: RESOLVED
clientChunksfunction is the configurability path for any layout — the marker list is intentionally finite, not the mechanism.rango:chunksdebug namespace surfaces every grouping decision (and the reason for a shared fallback), resolving the previously-silent behavior.app-componentschunk (which would re-introduce cross-app leakage).Semantics (unchanged contract, now documented + observable)
clientChunksroutes/<id>/…(marker)app-<id>(+ CSS)split <id> -> app-<id>src/components/…(no marker)shared <id> (no route-root marker…)import()falseundefined→ defaultCode-change summary
src/vite/debug.ts— addrango:chunksnamespace.src/vite/utils/client-chunks.ts— log each grouping decision (split + the actionable shared-fallback) insidedirectoryClientChunks; control-flow refactor preserves behavior exactly (15 unit tests unchanged,resolveClientChunks(true) === directoryClientChunksstill holds for zero per-call cost when the namespace is off).tools/bench-client-chunks.mjs— static cost-side analyzer (gzip sizes, shared baseline, per-route first-load + full-crawl model).tests/vite-rsc-demo/vite.config.ts—RANGO_BENCH_CHUNKSconsumer-side toggle (also a live demo of the public opt-out; defaults to on when unset, so e2e is unaffected — verified the env-unset build emits identical chunk hashes).e2e/mini/src/router.tsx+.gen.ts+e2e/mini.test.ts—/combinedmulti-group CSS co-render route + dev/prod e2e.packages/rangojs-router/docs/client-chunking.md— corrected fallback section, observability section, measured cost-side table;docs/research/build-chunking-plan.md— gates marked RESOLVED with the deterministic stance.Test plan
client-chunks.test.ts(15)clientChunksincl. new/combined(7 dev+prod), vite-rsc-demokanbanproduction (8) against the split buildrango:chunksobservability smoke-verified on a real buildNotes
RANGO_BENCH_CHUNKSlives in the test app's own config (consumer-side), not the plugin — no plugin env knob (the research PoC env-gating was deliberately removed before feat(router): per-route client chunking via clientChunks (default-on) #535).