Repository navigation
fix(core): keep SSR-shared text intact when a keyed diff reorders or replaces siblings - #9368
Conversation
🦋 Changeset detectedLatest commit: 1853fee The changes in this PR will be included in the next version bump. This PR includes changesets to release 8 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
@qwik.dev/core
@qwik.dev/router
@qwik.dev/utils
eslint-plugin-qwik
create-qwik
@qwik.dev/optimizer
@qwik.dev/devtools
@qwik.dev/mcp
commit: |
|
⚡ Docs preview for acd4b11 |
Varixo
left a comment
There was a problem hiding this comment.
hey, instead we can use exisitng walkDirectChildren:
// Stop both sibling walks at the boundary of the shared SSR text.
while (isText(cursor) && cursor.node === sharedTextNode) {
const next = nextSibling(cursor);
const isLast = !isText(next) || next.node !== sharedTextNode;
// Existing text-splitting logic.
cursor = next;
}
// Make the callback optional.
walkDirectChildren(journal, parent, callback?) {
// Existing traversal and text inflation.
callback?.(child);
}
// Before unlinking:
walkDirectChildren(journal, newChild);
unlink(newChild);…replaces siblings After resume, the text of adjacent keyed fragments shares one SSR Text node and is split lazily by vnode_ensureTextInflated. A keyed diff could corrupt it: - removing the element that separated two SSR text runs made the split walk across both runs (duplicated or overwritten text); - moving a keyed fragment unlinked it before its text was split, so its last text vnode took over the shared node and the other fragment's text was lost. vnode_ensureTextInflated now only treats text vnodes that still point at the same SSR Text node as one run, and vnode_insertVirtualBefore splits a moved virtual's text before unlinking it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
acd4b11 to
1ad577e
Compare
Both sibling walks in vnode_ensureTextInflated now stop at the first vnode that no longer points at the shared SSR Text node, which also removes the quadratic lookahead. The vnode_walkDirectChildren callback is optional for callers that only need the text inflated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What is it? Bug fix
Description
After resume, the text of adjacent keyed fragments shares one SSR Text node and is split lazily by
vnode_ensureTextInflated, which assumes that text vnodes that are neighbours in the current vnode tree came from the same SSR Text node. A keyed diff can break that assumption in two ways:cleanupSideBuffer→removeUnmatchedSiblingsremoves the element that separated two SSR text runs;vnode_insertElementBefore→vnode_findInsertBeforethen inflates text and the split walks across both runs, so text is duplicated or overwritten.reorderMatchedSiblingsmoves a keyed Fragment;vnode_insertVirtualBeforeunlinks it first and its text is split afterwards, so its last text vnode looks like the end of the run, takes over the shared node, and the other fragment's text is lost.This PR:
vnode_ensureTextInflated: only treats text vnodes that still point at the same SSR Text node as one run, both when walking the previous siblings and when deciding which vnode keeps the shared node. Runs from different SSR Text nodes that became adjacent are left alone.vnode_insertVirtualBefore: inflates a moved virtual's text while it is still linked to its siblings, beforevnode_unlinkFromOldParent(the same idea asvnode_inflateProjectionTrailingText, for keyed fragments).Reproduction
With
ssrRenderToDom, after the click: expectedX0t1t2X3t4(whatdomRendergives), actual onmainX0t1t2t1t2X3t4. With a third swapped element instead of the trailingt4,t1is lost (X0t2X3X4).Real-world impact: a chord-sheet page renders
<Skeleton key={i}/>placeholders between lyric fragments until a client-only preference loads; after the swap, lyrics blocks repeat and text goes missing. The same JSX works in v1 (1.20.3), which wrapped each keyed Fragment's text in its own comment-delimited node.Tests
render-regression.spec.tsx, run with bothssrRenderToDomanddomRender: element placeholders with trailing text, without trailing text, and component placeholders replaced by keyed fragments. All 3 SSR cases fail onmain('X0t1t2t1t2X3t4','X0t2X3X4','X0t1t2t4X3t4').vitest run packages/qwik/src: 133 files, 2982 passed.Checklist
pnpm change🤖 Generated with Claude Code
Summary by CodeRabbit