From 8c87864a857951c167fc192981d864e9f2397aee Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 2 Oct 2026 13:14:20 -0700 Subject: [PATCH 1/5] fix(web): a late hydration claim leaves server nodes in place (#3749) A settled whose lazy() module is still loading when hydration starts returns nothing in the root pass and claims its server nodes on a later resume. The insert holding the boundary's value sits outside the boundary: its claim pass saw `undefined` and tracked the region as empty, so on resume it took the client path (appendNodes / appendChild) and re-inserted the connected, already-claimed nodes, moving them and blurring a focused input. insertExpression now leaves nodes in place when an untracked region receives nodes already sitting in it in order. The check lives on the hydration runtime slot, so client-only bundles carry none of it. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- ...dration-late-claim-keeps-nodes-in-place.md | 5 + packages/web/src/client.ts | 15 +- .../loading-lazy-resume-3749.spec.tsx | 137 ++++++++++++++++++ scripts/size/floor-caps.json | 6 +- scripts/size/scenarios.js | 37 ++++- 5 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 .changeset/hydration-late-claim-keeps-nodes-in-place.md create mode 100644 packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx diff --git a/.changeset/hydration-late-claim-keeps-nodes-in-place.md b/.changeset/hydration-late-claim-keeps-nodes-in-place.md new file mode 100644 index 000000000..ced14e888 --- /dev/null +++ b/.changeset/hydration-late-claim-keeps-nodes-in-place.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +Hydration: a `` around `lazy()` whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. An insert now leaves nodes in place when they already sit in its region in order. Fixes #3749. diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index 490932549..2b792ad9f 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -1306,6 +1306,16 @@ export function installHydrationRuntime() { } else return claimChildNodes(parent); return stripTextSeparators(nodes); }, + // insertExpression(): a region tracked as empty can receive nodes that + // already sit in it, in order — server nodes a boundary claims on a late + // resume, after the enclosing insert's claim pass saw no value (#3749). + // They stay put: re-inserting a connected node moves it, and a move blurs + // a focused input. + inPlace(parent, nodes) { + for (let i = nodes.length, next; i--; next = nodes[i]) + if (nodes[i].parentNode !== parent || (next && nodes[i].nextSibling !== next)) return false; + return true; + }, // eventHandler(): replayed server events are deduped against the live // event queue during hydration. dedupEvent(e) { @@ -2789,7 +2799,8 @@ function insertExpression(parent, value, current, marker) { // Truthiness would skip this replace and leave that text node beside // the new element (#3571). parent.replaceChild(value, parent.firstChild); - } else { + } else if (hydrationRt === null || value.parentNode !== parent) { + // Already in place: a late claim (see hydrationRt.inPlace). parent.appendChild(value); } if (marker) value[$$SLOT] = marker; @@ -2818,7 +2829,7 @@ function insertExpression(parent, value, current, marker) { if (current.length === 0) { appendNodes(parent, value, marker); } else reconcileArrays(parent, current, value, marker); - } else { + } else if (current != null || hydrationRt === null || !hydrationRt.inPlace(parent, value)) { // Same sole-primitive case: `0` / `NaN` still own a text node (#3571). if (current != null) cleanChildren(parent, current); appendNodes(parent, value); diff --git a/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx b/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx new file mode 100644 index 000000000..a0b658cfc --- /dev/null +++ b/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx @@ -0,0 +1,137 @@ +/** + * @jsxImportSource @solidjs/web + * @vitest-environment jsdom + * + * #3749: a settled whose lazy() module is still loading when + * hydration starts renders nothing in the root pass and claims its server + * nodes once the module lands. The insert holding the boundary's value sits + * outside the boundary, so it lands them as a client insert — and must not + * re-insert nodes that are already in place: moving a connected node blurs a + * focused input the user is typing in. + */ +import { afterEach, describe, expect, test } from "vitest"; +import { createSignal, flush, lazy, Loading, type Component } from "solid-js"; +import { hydrate } from "@solidjs/web"; + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +let setCount!: (v: number) => void; +function FragmentPage() { + const [count, set] = createSignal(0); + setCount = set; + return ( + <> +
Header
+
+ + +
+ + ); +} +function ElementPage() { + const [count, set] = createSignal(0); + setCount = set; + return ( +
+ + +
+ ); +} + +// renderToStream output for each tree below (the module resolved during the +// render, a manifest answering "/assets/Page.js"): the boundary's fragment is +// settled and its content inlined, with its module map under `1_assets`. +const FRAGMENT_HTML = + '
Header
'; +const ELEMENT_HTML = + '
'; + +describe("#3749: Loading around lazy() keeps server nodes in place on a late module", () => { + let dispose: (() => void) | undefined; + let container: HTMLDivElement | undefined; + afterEach(async () => { + dispose?.(); + dispose = undefined; + await sleep(0); + container?.remove(); + }); + + async function hydrateWithLateModule(Page: Component, html: string) { + container = document.createElement("div"); + document.body.appendChild(container); + container.innerHTML = html; + const server = [...container.firstChild!.childNodes]; + const input = container.querySelector("input")!; + + // The page module is still in flight when hydration starts. + let land!: () => void; + const fr: any = Promise.resolve(true); + fr.s = 1; + fr.v = true; + const hy: any = { + events: [], + completed: new WeakSet(), + r: { "1_assets": { "1000": "/assets/Page.js" }, "1_fr": fr }, + fe() {}, + modules: {}, + loading: {} + }; + hy.loading["1000"] = new Promise(r => (land = r)).then(() => { + hy.modules["1000"] = { default: Page }; + }); + (globalThis as any)._$HY = hy; + + // The user is typing before hydration. + input.focus(); + input.value = "typed"; + + const LazyPage = lazy(() => Promise.resolve({ default: Page }), undefined, "src/Page.tsx"); + const moved: Node[] = []; + const observer = new MutationObserver(records => { + for (const r of records) moved.push(...r.addedNodes, ...r.removedNodes); + }); + observer.observe(container, { childList: true, subtree: true }); + + dispose = hydrate( + () => ( +
+ + + +
+ ), + container + ); + flush(); + await sleep(10); + land(); + await sleep(10); + flush(); + await sleep(10); + observer.disconnect(); + + expect([...container.firstChild!.childNodes]).toEqual(server); + for (let i = 0; i < server.length; i++) + expect(container.firstChild!.childNodes[i]).toBe(server[i]); + expect(moved).toEqual([]); + expect(document.activeElement).toBe(input); + expect(input.value).toBe("typed"); + } + + test("a fragment page claims in place and keeps focus", async () => { + await hydrateWithLateModule(FragmentPage, FRAGMENT_HTML); + // Hydrated: the button's text hole is live. + setCount(1); + flush(); + expect(container!.textContent).toBe("HeaderCount: 1"); + }); + + test("a single-element page claims in place and keeps focus", async () => { + await hydrateWithLateModule(ElementPage, ELEMENT_HTML); + setCount(1); + flush(); + expect(container!.textContent).toBe("Count: 1"); + }); +}); diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index dea5dea79..3dfa70dae 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -1,9 +1,9 @@ { "signals: core floor (createSignal/Memo/Effect/Root/flush)": "9.51 KB", "app: render + one signal (the simple-app floor)": "12.05 KB", - "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "19.69 KB", - "page: base server components (hydrating + dynamic + frames + sf reference)": "46.20 KB", - "page: live server components (base + live/GET + action + isPending/latest)": "50.45 KB", + "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "19.74 KB", + "page: base server components (hydrating + dynamic + frames + sf reference)": "46.27 KB", + "page: live server components (base + live/GET + action + isPending/latest)": "50.46 KB", "server: floor (getRequestEvent + isServer)": "1.34 KB", "server: renderToString (the server-render floor)": "20.38 KB" } diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 9af17dd83..f9dfb4ad5 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1567,6 +1567,15 @@ module.exports = [ // above is gone (no heap refusal, no `_manualWriteTime`, no // `deleteFromHeap` in the setter). Reverts the #3740 raise. The cap is // frozen again at 19.69 KB. + // Size-Exception (#3749, 2026-10-02): 19.69 -> 19.74 KB, measured at + // 19,732 B against `next` @ 98d35b9bc's 19,667 (+65 B; +180 B minified). + // A hydrating insert that tracked an empty region and later receives + // nodes already sitting in it in order (a claiming its server + // nodes on a late lazy() module) leaves them in place instead of + // re-inserting them, which blurred a focused input. Lives on the + // hydration runtime slot, so client-only bundles carry none of it. + // Pending maintainer review. The cap is + // frozen again at 19.74 KB. limit: floorCaps["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], alias }, @@ -1966,7 +1975,15 @@ module.exports = [ // 30,818 B against `next` @ 3ed381009's 30,817 (+1 B; -82 B minified). The // same mask removal as the core floor note; brotli layout over the larger // bundle keeps it 28 B over the pre-#3740 30.79 KB, so that raise stays. - limit: "30.82 KB", + // Size-Exception (#3749, 2026-10-02): 30.82 -> 30.88 KB, measured at + // 30,874 B against `next` @ 98d35b9bc's 30,818 (+56 B; +182 B minified). + // A hydrating insert that tracked an empty region and later receives + // nodes already sitting in it in order (a claiming its server + // nodes on a late lazy() module) leaves them in place instead of + // re-inserting them, which blurred a focused input. Lives on the + // hydration runtime slot, so client-only bundles carry none of it. + // Pending maintainer review. + limit: "30.88 KB", alias }, { @@ -3098,6 +3115,15 @@ module.exports = [ // above is gone (no heap refusal, no `_manualWriteTime`, no // `deleteFromHeap` in the setter). Reverts the #3740 raise. The cap is // frozen again at 46.20 KB. + // Size-Exception (#3749, 2026-10-02): 46.20 -> 46.27 KB, measured at + // 46,269 B against `next` @ 98d35b9bc's 46,181 (+88 B; +181 B minified). + // A hydrating insert that tracked an empty region and later receives + // nodes already sitting in it in order (a claiming its server + // nodes on a late lazy() module) leaves them in place instead of + // re-inserting them, which blurred a focused input. Lives on the + // hydration runtime slot, so client-only bundles carry none of it. + // Pending maintainer review. The cap is + // frozen again at 46.27 KB. limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"], alias: pageAlias }, @@ -3183,6 +3209,15 @@ module.exports = [ // at 50,346 B against #3713 @ 73640f5cd's 50,150 (a 50.15 -> 50.35 KB // raise); the base moved under the PR (the fake-`Promise` cap above). // Size vetted by the maintainer. The cap is frozen again at 50.45 KB. + // Size-Exception (#3749, 2026-10-02): 50.45 -> 50.46 KB, measured at + // 50,460 B against `next` @ 98d35b9bc's 50,385 (+75 B; +183 B minified). + // A hydrating insert that tracked an empty region and later receives + // nodes already sitting in it in order (a claiming its server + // nodes on a late lazy() module) leaves them in place instead of + // re-inserting them, which blurred a focused input. Lives on the + // hydration runtime slot, so client-only bundles carry none of it. + // Pending maintainer review. The cap is + // frozen again at 50.46 KB. limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"], alias: pageAlias }, From 1241b6752f771e270765467e4b4612cc9c396867 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 2 Oct 2026 14:21:01 -0700 Subject: [PATCH 2/5] fix(web): shrink the late-claim in-place check (#3749) One guard at the top of insertExpression replaces the hydration-runtime helper and its two call sites: an untracked region whose incoming nodes are all already children of the parent returns without touching the DOM. The first node rejects a fresh render without allocating. +90 B minified on hydrating bundles instead of +180, and under every existing cap. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- ...dration-late-claim-keeps-nodes-in-place.md | 2 +- packages/web/src/client.ts | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.changeset/hydration-late-claim-keeps-nodes-in-place.md b/.changeset/hydration-late-claim-keeps-nodes-in-place.md index ced14e888..2570ebb5b 100644 --- a/.changeset/hydration-late-claim-keeps-nodes-in-place.md +++ b/.changeset/hydration-late-claim-keeps-nodes-in-place.md @@ -2,4 +2,4 @@ "@solidjs/web": patch --- -Hydration: a `` around `lazy()` whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. An insert now leaves nodes in place when they already sit in its region in order. Fixes #3749. +Hydration: a `` around `lazy()` whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. An insert whose region is untracked now leaves the incoming nodes in place when every one of them is already a child of its parent. Fixes #3749. diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index de660eb47..c50359da1 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -1305,16 +1305,6 @@ export function installHydrationRuntime() { } else return claimChildNodes(parent); return stripTextSeparators(nodes); }, - // insertExpression(): a region tracked as empty can receive nodes that - // already sit in it, in order — server nodes a boundary claims on a late - // resume, after the enclosing insert's claim pass saw no value (#3749). - // They stay put: re-inserting a connected node moves it, and a move blurs - // a focused input. - inPlace(parent, nodes) { - for (let i = nodes.length, next; i--; next = nodes[i]) - if (nodes[i].parentNode !== parent || (next && nodes[i].nextSibling !== next)) return false; - return true; - }, // eventHandler(): replayed server events are deduped against the live // event queue during hydration. dedupEvent(e) { @@ -2764,6 +2754,20 @@ function insertExpression(parent, value, current, marker) { return value; } if (value === current) return value; + // A region tracked as empty can receive nodes that already sit in it: + // server nodes a boundary claims on a late resume, after this insert's + // claim pass saw no value (#3749). Re-inserting them would move connected + // nodes and blur a focused input, so they stay put. The first node rejects + // a fresh render without allocating; every item must be a node in `parent` + // (a raw primitive is a failed text claim and still needs inserting). + if ( + hydrationRt !== null && + current == null && + value && + (value[0] || value).parentNode === parent && + [].concat(value).every(n => n?.parentNode === parent) + ) + return value; const t = typeof value, multi = marker !== undefined; @@ -2798,8 +2802,7 @@ function insertExpression(parent, value, current, marker) { // Truthiness would skip this replace and leave that text node beside // the new element (#3571). parent.replaceChild(value, parent.firstChild); - } else if (hydrationRt === null || value.parentNode !== parent) { - // Already in place: a late claim (see hydrationRt.inPlace). + } else { parent.appendChild(value); } if (marker) value[$$SLOT] = marker; @@ -2828,7 +2831,7 @@ function insertExpression(parent, value, current, marker) { if (current.length === 0) { appendNodes(parent, value, marker); } else reconcileArrays(parent, current, value, marker); - } else if (current != null || hydrationRt === null || !hydrationRt.inPlace(parent, value)) { + } else { // Same sole-primitive case: `0` / `NaN` still own a text node (#3571). if (current != null) cleanChildren(parent, current); appendNodes(parent, value); From 3153ca911a1caec0dafc81e13cbfcd3ab5ba7fb4 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 2 Oct 2026 16:01:11 -0700 Subject: [PATCH 3/5] fix(web): scope the late-claim in-place guard to hydration (#3749) The guard now runs only while `sharedConfig.hydrating` is set: the root pass and each boundary's resume window, where the #3749 landing happens. Then claimed server nodes are the only nodes of ours already in the parent, so order needs no check. After hydration completes, inserts behave exactly as on `next`; a new test pins that. Encoded without the first-node fast path (the allocation is now hydration-only): +91 B minified on hydrating bundles, under every cap. Also marks the #3741 page-base ledger note as accepted. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- ...dration-late-claim-keeps-nodes-in-place.md | 2 +- packages/web/src/client.ts | 13 ++++++---- .../loading-lazy-resume-3749.spec.tsx | 25 ++++++++++++++++++- scripts/size/scenarios.js | 4 +-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.changeset/hydration-late-claim-keeps-nodes-in-place.md b/.changeset/hydration-late-claim-keeps-nodes-in-place.md index 2570ebb5b..67102f193 100644 --- a/.changeset/hydration-late-claim-keeps-nodes-in-place.md +++ b/.changeset/hydration-late-claim-keeps-nodes-in-place.md @@ -2,4 +2,4 @@ "@solidjs/web": patch --- -Hydration: a `` around `lazy()` whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. An insert whose region is untracked now leaves the incoming nodes in place when every one of them is already a child of its parent. Fixes #3749. +Hydration: a `` around `lazy()` whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. While hydration is in progress, an insert whose region is untracked now leaves the incoming nodes in place when every one of them is already a child of its parent. Once hydration completes, inserts behave as before. Fixes #3749. diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index 959d326d8..68d296f7a 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -2757,15 +2757,18 @@ function insertExpression(parent, value, current, marker) { // A region tracked as empty can receive nodes that already sit in it: // server nodes a boundary claims on a late resume, after this insert's // claim pass saw no value (#3749). Re-inserting them would move connected - // nodes and blur a focused input, so they stay put. The first node rejects - // a fresh render without allocating; every item must be a node in `parent` - // (a raw primitive is a failed text claim and still needs inserting). + // nodes and blur a focused input, so they stay put. Only while hydrating + // (the resume window sets the flag): then claimed server nodes are the only + // nodes of ours already in `parent`, and they arrive in server order. Every + // item must be a node in `parent` (a raw primitive is a failed text claim + // and still needs inserting); an empty array keeps its clear. if ( hydrationRt !== null && + sharedConfig.hydrating && current == null && value && - (value[0] || value).parentNode === parent && - [].concat(value).every(n => n?.parentNode === parent) + [].concat(value).every(n => n?.parentNode === parent) && + value.length !== 0 ) return value; const t = typeof value, diff --git a/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx b/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx index a0b658cfc..2d075d47a 100644 --- a/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx +++ b/packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx @@ -10,7 +10,7 @@ * focused input the user is typing in. */ import { afterEach, describe, expect, test } from "vitest"; -import { createSignal, flush, lazy, Loading, type Component } from "solid-js"; +import { createSignal, flush, isHydrating, lazy, Loading, type Component } from "solid-js"; import { hydrate } from "@solidjs/web"; const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); @@ -134,4 +134,27 @@ describe("#3749: Loading around lazy() keeps server nodes in place on a late mod flush(); expect(container!.textContent).toBe("Count: 1"); }); + + test("after hydration an untracked region re-inserts nodes already in its parent", async () => { + container = document.createElement("div"); + document.body.appendChild(container); + container.innerHTML = "
"; + (globalThis as any)._$HY = { events: [], completed: new WeakSet(), r: {}, fe() {} }; + const a = document.createElement("b"); + const b = document.createElement("i"); + const [show, setShow] = createSignal(false); + dispose = hydrate(() =>
{show() ? [a, b] : undefined}
, container); + flush(); + await sleep(10); + expect(isHydrating()).toBe(false); + + // User code put the nodes in the region, ahead of a node it doesn't track. + const host = container.firstChild as HTMLElement; + const x = document.createElement("span"); + host.append(a, b, x); + setShow(true); + flush(); + // As on `next`: the run is re-inserted at the end. + expect([...host.childNodes]).toEqual([x, a, b]); + }); }); diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 7d6a4d82a..0ae625930 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -3104,8 +3104,8 @@ module.exports = [ // two blocks swap places; no code is added), so a prop getter's // hydration id lands where the server's attribute walk takes it. Brotli // layout only: three equivalent encodings measured +59..+129 B here and - // -21..+13 B on the live page; this is the smallest. Pending maintainer - // review. + // -21..+13 B on the live page; this is the smallest. Accepted by the + // maintainer. The cap is frozen again at 46.24 KB. limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"], alias: pageAlias }, From e94e3d72f159fd33d4c9f3b6d3a64b6427f02883 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 2 Oct 2026 21:29:20 -0700 Subject: [PATCH 4/5] chore(size): raise the page-base cap to 46.25 KB for #3749 The hydration in-place guard measures 46,249 B on the base server-components page against `next` @ 9e85a092d's 46,240 (+9 B brotli; +92 B minified). Ledger note in scenarios.js. Size-Exception: page base 46.24 -> 46.25 KB: #3749 hydration in-place guard (+9 B brotli, +92 B minified); accepted by the maintainer. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- scripts/size/floor-caps.json | 2 +- scripts/size/scenarios.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 0e4f854d8..28e6129ce 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -2,7 +2,7 @@ "signals: core floor (createSignal/Memo/Effect/Root/flush)": "9.51 KB", "app: render + one signal (the simple-app floor)": "12.05 KB", "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "19.69 KB", - "page: base server components (hydrating + dynamic + frames + sf reference)": "46.24 KB", + "page: base server components (hydrating + dynamic + frames + sf reference)": "46.25 KB", "page: live server components (base + live/GET + action + isPending/latest)": "50.45 KB", "server: floor (getRequestEvent + isServer)": "1.34 KB", "server: renderToString (the server-render floor)": "20.38 KB" diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 0ae625930..ca6be9ea8 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -3106,6 +3106,14 @@ module.exports = [ // layout only: three equivalent encodings measured +59..+129 B here and // -21..+13 B on the live page; this is the smallest. Accepted by the // maintainer. The cap is frozen again at 46.24 KB. + // Size-Exception (#3749, 2026-10-02): 46.24 -> 46.25 KB, measured at + // 46,249 B against `next` @ 9e85a092d's 46,240 (+9 B; +92 B minified). + // While hydrating, an untracked region whose incoming nodes are already + // children of its parent leaves them in place: a claiming its + // server nodes on a late lazy() resume no longer moves them (focus loss). + // Twelve equivalent encodings measured; this is the only one over by + // page base alone. Accepted by the maintainer. The cap is frozen again + // at 46.25 KB. limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"], alias: pageAlias }, From 92e79a134f32ab26424c5ac7ac3959db15d0cae0 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Fri, 2 Oct 2026 21:51:56 -0700 Subject: [PATCH 5/5] ci: re-run the size gate against the PR body's Size-Exception line Co-authored-by: Claude via Cursor Co-authored-by: Cursor