diff --git a/.changeset/hydrate-claim-path.md b/.changeset/hydrate-claim-path.md new file mode 100644 index 000000000..0d38a0f7c --- /dev/null +++ b/.changeset/hydrate-claim-path.md @@ -0,0 +1,6 @@ +--- +"@solidjs/web": patch +"@solidjs/signals": patch +--- + +Trim per-node work on the hydration claim path. `gatherHydratable` asks once whether the root contains frame regions and tests containment against that list, instead of walking every keyed node's ancestor chain with `closest("[data-fid]")`; `insert()` builds a parent's claim array in one indexed pass over `childNodes` that drops separators as it copies, instead of an iterator spread followed by a compacting pass; and `clearSnapshots` assigns `undefined` to the extension's `_snapshotValue` rather than `delete`-ing it, which pushed every hydrated source's extension object into dictionary mode. diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index b12470546..9e5f90e62 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -213,10 +213,15 @@ function releaseSubtree(owner: Owner): void { export function clearSnapshots(): void { if (snapshotSources) { for (const source of snapshotSources) { - delete source._x?._snapshotValue; + // The extension is a fixed-shape object with `_snapshotValue` + // pre-initialized to undefined (see ext()), and every reader tests + // `!== undefined` — assign, don't `delete`: deleting a field pushes the + // object to dictionary mode for every later read of every field. + const x = source._x; + if (x != null) x._snapshotValue = undefined; // StoreNode targets share one pre-initialized hidden class (see - // createStoreProxy) — assign undefined instead of deleting, and only - // when present so signal-node sources don't grow the field. + // createStoreProxy) — same rule, and only when present so signal-node + // sources don't grow the field. if (source[STORE_SNAPSHOT_PROPS] !== undefined) source[STORE_SNAPSHOT_PROPS] = undefined; } snapshotSources = null; diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index c579b39ae..053e6ee2a 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -1118,8 +1118,8 @@ export function installHydrationRuntime() { // hydration, dropping server text-hole separators. claimInitial(parent, multi, initial) { if (isHydrating(parent)) { - if (!multi && initial === undefined && parent) initial = [...parent.childNodes]; - if (Array.isArray(initial)) stripTextSeparators(initial); + if (!multi && initial === undefined && parent) initial = claimChildNodes(parent); + else if (Array.isArray(initial)) stripTextSeparators(initial); } return initial; }, @@ -1151,7 +1151,7 @@ export function installHydrationRuntime() { nodes.unshift(node); node = node.previousSibling; } - } else nodes = [...parent.childNodes]; + } else return claimChildNodes(parent); return stripTextSeparators(nodes); }, // eventHandler(): replayed server events are deduped against the live @@ -1180,20 +1180,49 @@ function stripTextSeparators(nodes) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i], t = node.nodeType; - if (t === 8) { - const v = node.nodeValue; - if (v === "!$") { - node.remove(); - continue; - } - if (v.startsWith("pl-")) continue; - } else if (t === 1 && node.localName === "template" && node.id.startsWith("pl-")) continue; + if (t === 8 && node.nodeValue === "!$") { + node.remove(); + continue; + } + if (isPlaceholderScaffolding(node, t)) continue; nodes[j++] = node; } nodes.length = j; return nodes; } +// The claim array for a parent's children: one indexed pass over the live +// childNodes with the separators dropped as it copies. This runs for every +// insert() during hydration, so it avoids `[...parent.childNodes]` (the +// iterator protocol over a live NodeList) followed by a second compacting +// pass. Removing a `` shifts the live list, so the index holds. +function claimChildNodes(parent) { + const live = parent.childNodes; + const out = []; + for (let i = 0, n = live.length; i < n; i++) { + const node = live[i], + t = node.nodeType; + if (t === 8 && node.nodeValue === "!$") { + node.remove(); + i--; + n--; + continue; + } + if (isPlaceholderScaffolding(node, t)) continue; + out.push(node); + } + return out; +} + +// A pending boundary's placeholder scaffolding — `