refactor: simplify hydration markers to data-free sequential format#7474
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new data-free, sequential hydration marker format across FAST SSR and client hydration, replacing index/ID-encoded comment markers and multi-format attribute markers. It also updates benchmarks and documentation to reflect the new SSR output, and updates the Rust @microsoft/fast-build renderer + tests accordingly.
Changes:
- Replace hydration markers with fixed strings (
fe:b,fe:/b,fe:r,fe:/r,fe:e,fe:/e) and replace attribute marker variants withdata-fe="N". - Refactor client hydration walking to consume factories sequentially (no regex parsing / embedded indices).
- Update Rust SSR emission + tests, benchmark SSR rendering to use the WASM renderer, and update migration/docs.
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| sites/website/src/docs/3.x/migration-guide.md | Adds 3.x migration note for the new hydration marker format |
| sites/benchmarks/vite.config.ts | Switches benchmark SSR generation to use the WASM renderer with per-scenario caching |
| sites/benchmarks/src/scenarios/when/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/when/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/when/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/repeat/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/repeat/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/repeat/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/ref-slotted/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/ref-slotted/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/ref-slotted/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/dot-syntax/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/dot-syntax/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/dot-syntax/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/bind-event/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/bind-event/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/bind-event/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/basic/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/basic/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/basic/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| sites/benchmarks/src/scenarios/attr-reflect/hydration/state.json | Adds scenario state for WASM-based SSR rendering |
| sites/benchmarks/src/scenarios/attr-reflect/hydration/render.ts | Removes hardcoded HTML SSR output (replaced by WASM rendering) |
| sites/benchmarks/src/scenarios/attr-reflect/hydration/entry.html | Adds scenario entry HTML used by WASM renderer |
| packages/fast-html/RENDERING_LIFECYCLE.md | Updates lifecycle docs to show new comment markers |
| packages/fast-html/RENDERING.md | Updates rendering docs for new markers and data-fe="N" |
| packages/fast-html/MIGRATION.md | Adds migration section mapping old markers to new markers |
| packages/fast-html/DESIGN.md | Updates design docs examples/diagrams for new markers |
| packages/fast-element/src/templating/repeat.ts | Refactors repeat hydration to pair fe:r / fe:/r using depth counting |
| packages/fast-element/src/templating/TEMPLATE-BINDINGS.md | Updates hydration marker documentation/flowcharts for new format |
| packages/fast-element/src/hydration/target-builder.ts | Refactors hydration target-building to sequential factory consumption with data-fe="N" and fe:* comment markers |
| packages/fast-element/src/components/hydration.ts | Replaces regex-based marker parsing with fixed-string marker utilities and parseAttributeBindingCount |
| packages/fast-element/MIGRATION.md | Adds v3 migration notes for the new hydration marker format and API changes |
| crates/microsoft-fast-build/tests/hydration.rs | Updates Rust unit tests to assert the new marker format |
| crates/microsoft-fast-build/src/node.rs | Updates SSR emission to use count markers and data-free content markers |
| crates/microsoft-fast-build/src/lib.rs | Updates crate docs to reflect new count marker format |
| crates/microsoft-fast-build/src/hydration.rs | Simplifies HydrationScope to data-free marker helpers and removes scoped naming |
| crates/microsoft-fast-build/src/directive.rs | Updates directive emission (when, repeat, custom elements) to new marker format |
| crates/microsoft-fast-build/src/attribute.rs | Replaces compact marker injection with data-fe="N" marker injection |
| crates/microsoft-fast-build/README.md | Updates documentation examples to new markers |
| crates/microsoft-fast-build/DESIGN.md | Updates design documentation to new marker format |
| change/@microsoft-fast-html-fe4f7812-d5d7-40a1-bb1b-31abe4d47aec.json | Records prerelease change note for @microsoft/fast-html |
| change/@microsoft-fast-element-9dd7b073-98cc-401b-a04d-0c02bee14306.json | Records major breaking change note for @microsoft/fast-element |
db4ac60 to
f510b47
Compare
Replace verbose HTML comment markers (73+ chars per pair) with compact data-free markers (f:b, f:/b, f:r, f:/r, f:e, f:/e). Replace regex parsing with string equality checks and balanced depth counting. Single data-fe="N" attribute replaces three old formats. BREAKING CHANGE: SSR output format changed - all hydration markers use new sequential format requiring matched SSR/client versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…attribute Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace handwritten render functions that hardcoded hydration markers with entry.html + state.json files rendered by @microsoft/fast-build WASM at Vite build time. This ensures benchmark SSR output always matches the actual crate output and eliminates marker drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update MIGRATION.md for fast-element and fast-html packages, and the 3.x website migration guide with the new marker format, removed APIs, and migration impact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix doc comment: f: prefix → fe: prefix in hydration.ts - Add input validation to parseAttributeBindingCount (throw on non-numeric) - Add null guard for factory in content binding path (target-builder.ts) - Add null guard for first sibling in targetContentBinding (remove non-null assertion) - Fix duplicate Rust test assertions: use count-based checks instead of contains - Fix repeat marker test: assert exact count (2) instead of duplicate contains - Expand migration guide marker table with repeat end and element boundary markers - Update TEMPLATE-BINDINGS.md error table to reflect count-based semantics - Add post-loop validation in repeat.ts hydrateViews for item count mismatch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Widen HydrationTargetElementError.node type from Element to Node (content binding path passes Comment, not Element) - Add strict digit-only regex validation in parseAttributeBindingCount (rejects partial numbers like '1abc') - Fix duplicate assertion in slotted Rust test (use count-based check) - Fix MIGRATION.md API names to match actual exports (isRepeatViewStartMarker, not isRepeatStartMarker, etc.) - Add repeat overflow detection in hydrateViews (throws if DOM has more repeat markers than items.length) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add @microsoft/fast-build dependency to benchmarks package.json (was relying on workspace hoisting) - Clear element boundary marker data (fe:e/fe:/e) during hydration (consistent with content/repeat marker cleanup) - Throw HydrationTargetElementError when fe:/e end marker is not found (fail fast on malformed SSR output instead of silent success) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Make XMLSerializer lazy in hydrateViews (only allocated on error path) - Use FAST.error(Message.invalidHydrationAttributeMarker) instead of plain Error for invalid data-fe values (consistent with library error reporting conventions) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add debug message for error code 1210 (invalidHydrationAttributeMarker) so FAST.error() shows a descriptive message instead of 'Unknown Error' - Cache pre-stringified templates JSON in ScenarioCache to avoid repeated JSON.stringify() on every WASM render call (1000+ per build) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add element boundary error case and repeat overflow to error table - Add FAST.error(1210) for invalid data-fe values - Widen 'element node' to 'target node' (type is now Node, not Element) - Update flowchart: element boundary now clears marker data and throws on missing end marker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a repeat item renders no content between its fe:r/fe:/r markers, the start/end range passed to template.hydrate() was inverted. Detect this case by checking if end === startMarker (adjacent markers) and use the cleared end marker comment as both first and last node. The second comment (add focused hydration test) is noted for follow-up but not addressed in this commit as it requires new test fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bound repeat hydration scan to this directive's content binding boundaries (via bindingViewBoundaries) to prevent scanning into sibling repeat blocks - Fix comment on empty-item handling to match actual implementation full diagnostic property bag - Add Rust tests: single-item repeat, many-item repeat (count-based assertions), and multiple attribute bindings on one element Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix MIGRATION.md: correct old API names in Removed column (parseRepeatStartMarker not parseRepeatViewStartMarker, parseElementBoundaryStartMarker(content) not (node)) - Upgrade targetContentBinding errors from plain Error to HydrationTargetElementError with factories and node context - Fix scanStop boundary: use previousSibling of boundary.first so the boundary node itself is still eligible for matching as a start marker - Remove unused isShadowRoot helper function Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The committed fixture HTML files still had old markers (fe-b$$start$$...) because the WASM binary was rebuilt locally but the fixtures were not re-committed. Rebuilt all 21 fixtures with the new WASM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Initialize views array with Array.from() instead of sparse new Array() so .map() in error diagnostics visits all indices - Clear nested element boundary start/end marker data during skipToElementBoundaryEnd (not just the outermost pair) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All 21 fixture HTML files were still using old marker format (fe-b$$start$$...) from before the refactor. Rebuilt with the updated WASM binary that emits fe:b/fe:/b/fe:r/fe:/r markers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Empty repeats (0 items) never enter the main hydration loop, so 'current' still points at this.location.previousSibling — which may contain fe:/r markers from other repeat blocks. The overflow check was incorrectly scanning into sibling content and throwing 'found more repeat items than expected 0'. Skip the overflow check entirely when itemCount is 0 since an empty repeat cannot overflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
17642fa to
e0d46b9
Compare
The overflow check (scan for extra fe:/r markers after hydration) and the scanStop boundary (bindingViewBoundaries-based scan limit) both caused false positives with nested repeats. Data-free markers make it impossible to distinguish fe:/r from this repeat vs a nested repeat at the same DOM depth. The underflow check also misfired when nested repeat items were counted instead of this repeat's items. Remove all three mechanisms. The balanced depth counting in the backward scan correctly handles nesting without them. This matches the original base branch behavior which also did not have overflow/underflow checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The backward scan assigned hydrated views from the end of the array (views[N-1], views[N-2], ...). When SSR had fewer items than the runtime (e.g., state.json has 1 order but class field has 2), the wrong indices got hydrated — views[1] instead of views[0]. Fix by collecting all marker ranges in a backward pass, reversing them to restore forward order, then hydrating from index 0. This matches the old behavior where marker-embedded indices directly set the correct array position. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
I dug through the failing Only
The common pattern is that initial SSR output is fine, but post-hydration updates inside nested repeat/conditional/custom-element subtrees stop propagating:
That lines up with the marker rewrite in this PR: top-level bindings still work, but hydrated subviews inside repeats are getting wired incorrectly after the shift to data-free sequential markers. In practice, this looks like the new sequential factory/boundary bookkeeping is correct for outer views but goes stale for nested repeat descendants. So the pipeline is failing because the new hydration flow is leaving nested repeat/when/custom-element views attached to the wrong boundaries/targets. The first places I’d inspect are:
The strongest signal is that simple/top-level updates still pass, while updates one level down inside hydrated repeat trees do not. |
|
Test Failure Analysis: "should pass parent attribute to child elements" The Root cause — the two-pass The new During the backward walk:
At step 3, However, the real issue may be subtler: after ALL markers are cleared in the first pass, the Suggestion: The old code hydrated each item inline during the backward walk (before clearing adjacent markers). Consider reverting to that pattern — hydrate immediately when each item range is found, rather than collecting ranges first. This avoids any risk of cleared markers affecting boundary node relationships: // Instead of two passes, hydrate inline (backward) like the old code:
let itemIndex = itemCount - 1;
while (current !== null && itemIndex >= 0) {
// find end marker, find start marker...
const view = template.hydrate(itemStart, itemEnd);
this.views[itemIndex] = view;
this.bindView(view, this.items, itemIndex, this.controller);
itemIndex--;
}This preserves the sequential (no-index) marker design while matching the proven hydration-during-walk pattern from the base branch. |
…ments fixture The ElementController constructor captures @observable default values in boundObservables during element upgrade. When connect() runs later during hydration, bindObservables() replays those defaults — overwriting real data that connectedCallback had already set. This caused the repeat to hydrate with 0 items, producing duplicate child-elements. Fix: use @observable with defaults and set data BEFORE super.connectedCallback() so the constructor captures the real data instead of empty defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pull Request
📖 Description
Replace the verbose, index-embedded hydration marker system with a compact, data-free sequential format. This is a breaking change to the SSR output format.
Why: The old markers embedded binding indices and scope IDs into HTML comments (73+ chars per pair) and required regex parsing on the client. Since both the SSR template compiler and client hydration walker traverse the DOM in identical DFS order, these embedded indices are redundant — a simple sequential pointer suffices.
What changed:
Content binding markers:
Old:
fe-b$$start$$<factoryIndex>$$<scopeId>$$fe-b/fe-b$$end$$...New:
fe:b/fe:/bRepeat item markers:
Old:
fe-repeat$$start$$<itemIndex>$$fe-repeat/fe-repeat$$end$$...New:
fe:r/fe:/rElement boundary markers:
Old:
fe-eb$$start$$<elementId>$$fe-eb/fe-eb$$end$$...New:
fe:e/fe:/eAttribute binding markers:
Old: three formats —
data-fe-b="0 1 2"(space-separated indices),data-fe-b-0(enumerated),data-fe-c-0-3(compact start+count)New: single format —
data-fe="N"(binding count only)Parsing: Six regex patterns → string equality checks
Pairing: ID-based start/end matching → balanced depth counting
Walker: Index-based factory lookup → sequential factory pointer
Rust SSR:
HydrationScopesimplified — removedscope_prefix,child(), parameterized marker methodsBenchmarks: Replaced hardcoded
render.tsfunctions with WASM-based SSR via@microsoft/fast-build, ensuring benchmark output always matches the actual crate👩💻 Reviewer Notes
target-builder.ts— the hydration walker now advances afactoryPointersequentially rather than parsing indices from comment data.repeat.tshydration walks backward fromitems.length - 1usingfe:/r/fe:rend/start markers with balanced depth counting. Empty repeat items (adjacent markers with no content) are handled explicitly.HydrationScopemethodsrepeat_start_marker()/repeat_end_marker()are currently unused — repeat markers are emitted as string literals inrender_repeat_items. These could be removed or wired in as a follow-up.fe:e/fe:/e) are supported on the TS client side but not emitted by the Rust crate — DSD (<template shadowrootmode="open">) provides natural isolation. Both start and end boundary markers are cleared after hydration.fe:namespace prefix, consistent with thedata-feattribute naming.parseAttributeBindingCount()validates strictly (digit-only regex, positive integer) and throwsFAST.error(Message.invalidHydrationAttributeMarker)for malformed values.HydrationTargetElementErrorwhen factories are exhausted or end markers are missing.📑 Test Plan
cargo testincrates/microsoft-fast-build)npm run build:fixtures -w @microsoft/fast-htmlusing rebuilt WASM binarygrep/stringsmatches().count()for multi-occurrence markers to verify exact counts✅ Checklist
General
$ npm run change⏭ Next Steps
repeat_start_marker()/repeat_end_marker()methods fromHydrationScopein Rust, or wiring them intorender_repeat_itemsinstead of hardcoded strings.fe:e/fe:/eelement boundary markers for non-DSD scenarios.PROPOSAL_VIEWPORT_DEFERRED_HYDRATION.md) as a higher-impact optimization for reducing TTI.