diff --git a/RESULT-perf-task3.md b/RESULT-perf-task3.md new file mode 100644 index 00000000000..cb4d97adced --- /dev/null +++ b/RESULT-perf-task3.md @@ -0,0 +1,123 @@ +# RESULT — perf/task3-ssr-manifest-cache + +## Goal + +`packages/router-core/src/ssr/ssr-server.ts` `dehydrate()` re-serializes the static +route manifest (matched-route asset descriptors) through seroval on **every** +request, although it is byte-identical for a given matched-route set. This work +caches the _serialized_ manifest fragment in an LRU and emits it as a separate +script assignment so only dynamic data goes through the per-request stream. + +## Cross-reference analysis (task 1) — why splitting is safe here + +Seroval's `crossSerializeStream` builds one reference-ID graph (`$R["tsr"]`) +per stream call and deduplicates shared object identity inside that graph. +Naive splitting would be unsafe **only if** objects were shared across the +manifest/match boundary — then one side would reference an ID the other side +never defines, breaking hydration. + +Analysis of the data: + +- The manifest fragment (`preparedManifest.routes`, `scriptFormat`, + inline-CSS placeholder) is produced by the bundler plugin at build time: + plain JSON-safe data (strings, arrays, plain objects, booleans). +- The per-request part (`matches` → `dehydrateMatch()`: loaderData, + beforeLoadContext, errors; plus optional request-scoped assets) is created + independently at runtime by loaders/user code. +- No code path assigns a manifest route object into match data or vice versa; + the two trees never share references (request assets are merged via + `{...spread}` copies into a fresh root-route object, + `mergeRequestAssetsIntoRootRoute`). + +Conclusion: no cross-references exist between the two portions, so splitting is +hydration-safe. Additionally, the split halves are emitted into the _same_ +inline `