fix(start): preserve asset object identity when serializing manifest#7147
fix(start): preserve asset object identity when serializing manifest#7147ZacharyL2 wants to merge 1 commit intoTanStack:mainfrom
Conversation
…ifest
`JSON.stringify(startManifest)` produces a structurally-cloned tree,
which breaks the identity-based dedup used by the downstream SSR
serializer. As a result, CSS assets referenced by many routes end up
inlined once per route in the SSR HTML payload.
Introduce `serializeStartManifestAsModule`, which emits each unique
asset as a top-level `const __tsr_aN = {...}` and has every route
reference the shared variable by name. When the generated module is
loaded, routes that share an asset resolve to the same JS object, so
the SSR serializer can dedupe them as intended.
On a medium app, the root HTML drops from ~310 KB to ~57 KB.
📝 WalkthroughWalkthroughThe changes introduce a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bundle Size Benchmarks
Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts`:
- Around line 1249-1252: The helper that evaluates the manifest currently casts
the result to a loose type with "any" (the return of new Function(body)() is
typed as { routes: Record<string, any>; clientEntry: string }); replace that
with a strict manifest type: define or import a Manifest interface (e.g.,
interface Manifest { routes: Record<string, RouteDefinition>; clientEntry:
string }) or use ReturnType from the real manifest builder, then cast the
evaluated result as Manifest (not any) and adjust RouteDefinition to capture the
expected route shape so test assertions are type-checked at compile time; update
the typed symbols "routes" and "clientEntry" accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d8fa3269-e161-4983-9f37-2b3b6597d9e4
📒 Files selected for processing (3)
packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.tspackages/start-plugin-core/src/vite/start-manifest-plugin/plugin.tspackages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
Show resolved
Hide resolved
|
please provide a complete reproducer project as a git repo that shows this issue clearly. |
The problem
The Start manifest's serialization step uses
JSON.stringify(startManifest)in
startManifestPlugin. A single CSS asset referenced by many routes is ashared object in memory at build time — but
JSON.stringifyproduces astructurally-cloned tree, so every route ends up with its own independent
copy of the asset.
Downstream, the SSR serializer dedupes repeated values using reference
identity (
seenMap.has(obj)). Because the post-JSON.stringifyobjectsare all distinct, the serializer can't recognize them as repeats and inlines
each copy in full into the SSR HTML.
On a medium-sized app, this caused the same CSS asset object to be inlined
dozens of times into the root document. Observed impact: root HTML went
from ~310 KB → ~57 KB (~81% reduction) after the fix.
This complements #7030, which deduped the manifest at the data layer. The
data structure is already deduped (each shared asset is the same object
in memory), but the serialization step used to discard that sharing before
the downstream serializer saw it.
The fix
Introduce
serializeStartManifestAsModuleinmanifestBuilder.ts. Insteadof
JSON.stringify(manifest), the manifest is emitted as a JS module:Each unique asset is declared once as a module-level
const; every routereferences it by variable name. When the generated module is loaded, all
routes that share an asset hold the same JS object reference in memory,
so the SSR serializer can collapse them as designed.
Asset uniqueness is determined by the existing
getAssetIdentity()helper,so the identity logic (which already respects
tag,href,src,rel,type, etc.) is reused without change.Implementation is a single
JSON.stringifypass with a replacer that swapseach
assets[]item for a marker string, then a final.replace()overthe produced string converts markers into bare variable references. No
runtime cost — pure build-time codegen.
How I tested it
New unit tests (
manifestBuilder.test.ts, 3 tests)All three are observation-based, using
new Function(...)to evaluate thegenerated module source and inspect the resulting object graph:
asset — 3 routes share 1 asset; the eval'd module must satisfy
routes.a.assets[0] === routes.b.assets[0] === routes.c.assets[0].the JSON replacer only intercepts
assets; all other fields must bepassed through unchanged.
edge case doesn't throw or emit stray declarations.
End-to-end verification in a real app
Measured on a medium TanStack Start app:
The 2 remaining "repeats" per CSS href are the legitimate
<link>tag in<head>plus a single declaration in the embedded SSR payload. EachCSS href now appears exactly where it needs to, once as a
<link>andonce as a data field.
Summary by CodeRabbit
Release Notes
Improvements
Tests