From 0a679fb68789c459d26231ea15a765a26fb83cbf Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 05:04:46 -0400 Subject: [PATCH 1/6] perf(core): mounting a scene no longer restyles it to look for raster hints --- .../src/runtime/previewRasterHints.test.ts | 31 ++++++++++++++++ .../core/src/runtime/previewRasterHints.ts | 36 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/core/src/runtime/previewRasterHints.test.ts b/packages/core/src/runtime/previewRasterHints.test.ts index be593886714..70ac781c2cb 100644 --- a/packages/core/src/runtime/previewRasterHints.test.ts +++ b/packages/core/src/runtime/previewRasterHints.test.ts @@ -172,6 +172,37 @@ describe("setPreviewRasterScale", () => { reads.mockRestore(); }); + it("reads no computed style for a mounted scene that nothing gives a hint", async () => { + setPreviewRasterScale(0.5); + const reads = vi.spyOn(window, "getComputedStyle"); + const scene = document.createElement("div"); + const sheet = document.createElement("style"); + sheet.textContent = ".mc-layer { position: absolute }"; + scene.append(sheet); + for (let i = 0; i < 50; i += 1) + layer("position: absolute; left: 4px", scene).className = "mc-layer"; + document.body.append(scene); + await Promise.resolve(); + document.body.append(layer("position: absolute", document.createElement("div"))); + await Promise.resolve(); + const readCount = reads.mock.calls.length; + reads.mockRestore(); + expect(readCount).toBe(0); + }); + + it("marks a layer added later that only a grouped stylesheet rule hints", async () => { + const sheet = document.createElement("style"); + sheet.textContent = "@media all { .late-hinted { will-change: transform } }"; + document.head.append(sheet); + setPreviewRasterScale(0.5); + const late = layer("position: absolute"); + late.className = "late-hinted"; + const plain = layer("position: absolute"); + await Promise.resolve(); + expect([late, plain].map(marked)).toEqual([true, false]); + sheet.remove(); + }); + it("re-checks the page when a linked stylesheet loads", async () => { const sheet = document.createElement("style"); document.head.append(sheet); diff --git a/packages/core/src/runtime/previewRasterHints.ts b/packages/core/src/runtime/previewRasterHints.ts index 0277b6ab181..639fc5b32ca 100644 --- a/packages/core/src/runtime/previewRasterHints.ts +++ b/packages/core/src/runtime/previewRasterHints.ts @@ -81,21 +81,49 @@ function unmarkHolders(element: Element): void { } } +// Only an element hinted inline or by a rule can carry will-change, and matching reads no style. +function hintedSelector(): string { + const selectors = ['[style*="will-change"]']; + const collect = (rules: CSSRuleList, relative: boolean): void => { + for (const rule of rules) { + const isStyleRule = rule.type === CSSRule.STYLE_RULE; + if (isStyleRule && (rule as CSSStyleRule).style.getPropertyValue("will-change")) { + selectors.push(relative ? "*" : (rule as CSSStyleRule).selectorText); + } + const imported = (rule as CSSImportRule).styleSheet; + if (imported) collect(imported.cssRules, relative); + if ("cssRules" in rule) collect((rule as CSSGroupingRule).cssRules, relative || isStyleRule); + } + }; + try { + for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) { + collect(sheet.cssRules, false); + } + document.querySelector(selectors.join(",")); + } catch { + // Cross-origin rules are unreadable, and a namespaced selector does not parse here. + return "*"; + } + return selectors.join(","); +} + function followTree(root: Element, batch: FollowBatch): void { + batch.hinted ??= hintedSelector(); const decidedBeforeAnyWrite: Array<[Element, boolean]> = []; for (const element of [root, ...root.querySelectorAll("*")]) { if (batch.checked.has(element)) continue; batch.checked.add(element); + const readOwnHints = element === root || batch.changed.has(element); decidedBeforeAnyWrite.push([ element, - fits(element, element === root || batch.changed.has(element)), + element.matches(batch.hinted) && fits(element, readOwnHints), ]); } for (const [element, drop] of decidedBeforeAnyWrite) writeMark(element, drop); for (const [element] of decidedBeforeAnyWrite) unmarkHolders(element); } -type FollowBatch = { changed: Set; checked: Set }; +type FollowBatch = { changed: Set; checked: Set; hinted?: string }; // Scripts restyle inline every frame, so only what can break a mark is looked at. function followStyle(element: Element): void { @@ -117,7 +145,9 @@ function clearMarks(): void { // Every mark is decided before any is written, so the page restyles twice, not once per layer. function markLayers(): void { clearMarks(); - const layers = [...document.querySelectorAll("*")].filter((element) => fits(element, true)); + const layers = [...document.querySelectorAll(hintedSelector())].filter((element) => + fits(element, true), + ); for (const layer of layers) layer.setAttribute(PREVIEW_RASTER_ATTR, ""); } From db53e23018b93bf76668306e5d8707b9ba2ab228 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 05:15:20 -0400 Subject: [PATCH 2/6] perf(core): raster hints read inline hints directly instead of matching every style --- .../src/runtime/previewRasterHints.test.ts | 6 +++-- .../core/src/runtime/previewRasterHints.ts | 24 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/core/src/runtime/previewRasterHints.test.ts b/packages/core/src/runtime/previewRasterHints.test.ts index 70ac781c2cb..7c063abbf3e 100644 --- a/packages/core/src/runtime/previewRasterHints.test.ts +++ b/packages/core/src/runtime/previewRasterHints.test.ts @@ -175,6 +175,7 @@ describe("setPreviewRasterScale", () => { it("reads no computed style for a mounted scene that nothing gives a hint", async () => { setPreviewRasterScale(0.5); const reads = vi.spyOn(window, "getComputedStyle"); + const pageQueries = vi.spyOn(document, "querySelector"); const scene = document.createElement("div"); const sheet = document.createElement("style"); sheet.textContent = ".mc-layer { position: absolute }"; @@ -185,9 +186,10 @@ describe("setPreviewRasterScale", () => { await Promise.resolve(); document.body.append(layer("position: absolute", document.createElement("div"))); await Promise.resolve(); - const readCount = reads.mock.calls.length; + const counts = [reads.mock.calls.length, pageQueries.mock.calls.length]; reads.mockRestore(); - expect(readCount).toBe(0); + pageQueries.mockRestore(); + expect(counts).toEqual([0, 0]); }); it("marks a layer added later that only a grouped stylesheet rule hints", async () => { diff --git a/packages/core/src/runtime/previewRasterHints.ts b/packages/core/src/runtime/previewRasterHints.ts index 639fc5b32ca..3a5ad743755 100644 --- a/packages/core/src/runtime/previewRasterHints.ts +++ b/packages/core/src/runtime/previewRasterHints.ts @@ -81,9 +81,9 @@ function unmarkHolders(element: Element): void { } } -// Only an element hinted inline or by a rule can carry will-change, and matching reads no style. -function hintedSelector(): string { - const selectors = ['[style*="will-change"]']; +// Only an element hinted inline or by a rule can carry will-change; neither check reads style. +function hintRules(): string { + const selectors: string[] = []; const collect = (rules: CSSRuleList, relative: boolean): void => { for (const rule of rules) { const isStyleRule = rule.type === CSSRule.STYLE_RULE; @@ -99,7 +99,7 @@ function hintedSelector(): string { for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) { collect(sheet.cssRules, false); } - document.querySelector(selectors.join(",")); + document.createDocumentFragment().querySelector(selectors.join(",") || "*"); } catch { // Cross-origin rules are unreadable, and a namespaced selector does not parse here. return "*"; @@ -107,8 +107,13 @@ function hintedSelector(): string { return selectors.join(","); } +function mayHint(element: Element, rules: string): boolean { + if ((element as HTMLElement).style?.getPropertyValue("will-change")) return true; + return rules !== "" && element.matches(rules); +} + function followTree(root: Element, batch: FollowBatch): void { - batch.hinted ??= hintedSelector(); + batch.rules ??= hintRules(); const decidedBeforeAnyWrite: Array<[Element, boolean]> = []; for (const element of [root, ...root.querySelectorAll("*")]) { if (batch.checked.has(element)) continue; @@ -116,14 +121,14 @@ function followTree(root: Element, batch: FollowBatch): void { const readOwnHints = element === root || batch.changed.has(element); decidedBeforeAnyWrite.push([ element, - element.matches(batch.hinted) && fits(element, readOwnHints), + mayHint(element, batch.rules) && fits(element, readOwnHints), ]); } for (const [element, drop] of decidedBeforeAnyWrite) writeMark(element, drop); for (const [element] of decidedBeforeAnyWrite) unmarkHolders(element); } -type FollowBatch = { changed: Set; checked: Set; hinted?: string }; +type FollowBatch = { changed: Set; checked: Set; rules?: string }; // Scripts restyle inline every frame, so only what can break a mark is looked at. function followStyle(element: Element): void { @@ -145,8 +150,9 @@ function clearMarks(): void { // Every mark is decided before any is written, so the page restyles twice, not once per layer. function markLayers(): void { clearMarks(); - const layers = [...document.querySelectorAll(hintedSelector())].filter((element) => - fits(element, true), + const rules = hintRules(); + const layers = [...document.querySelectorAll("*")].filter( + (element) => mayHint(element, rules) && fits(element, true), ); for (const layer of layers) layer.setAttribute(PREVIEW_RASTER_ATTR, ""); } From 50ae37e2177487cf51fccef7a650906460463595 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 06:29:36 -0400 Subject: [PATCH 3/6] perf(core): raster hints read the stylesheets once per page check, not per batch --- .../src/runtime/previewRasterHints.test.ts | 27 +++++++++++++++++++ .../core/src/runtime/previewRasterHints.ts | 24 ++++++++++------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/core/src/runtime/previewRasterHints.test.ts b/packages/core/src/runtime/previewRasterHints.test.ts index 7c063abbf3e..4273f0b24b8 100644 --- a/packages/core/src/runtime/previewRasterHints.test.ts +++ b/packages/core/src/runtime/previewRasterHints.test.ts @@ -205,6 +205,33 @@ describe("setPreviewRasterScale", () => { sheet.remove(); }); + it("reads the stylesheets once, not for every batch of added layers", async () => { + setPreviewRasterScale(0.5); + const sheetReads = vi.spyOn(Document.prototype, "styleSheets", "get"); + for (let frame = 0; frame < 3; frame += 1) { + layer("position: absolute"); + await Promise.resolve(); + } + const reads = sheetReads.mock.calls.length; + sheetReads.mockRestore(); + expect(reads).toBeLessThanOrEqual(1); + }); + + it("sees a hint rule from a style that arrives while the preview is small", async () => { + setPreviewRasterScale(0.5); + layer("position: absolute"); + await Promise.resolve(); + const sheet = document.createElement("style"); + sheet.textContent = ".arrived-hint { will-change: transform }"; + document.head.append(sheet); + await Promise.resolve(); + const late = layer("position: absolute"); + late.className = "arrived-hint"; + await Promise.resolve(); + expect(marked(late)).toBe(true); + sheet.remove(); + }); + it("re-checks the page when a linked stylesheet loads", async () => { const sheet = document.createElement("style"); document.head.append(sheet); diff --git a/packages/core/src/runtime/previewRasterHints.ts b/packages/core/src/runtime/previewRasterHints.ts index 3a5ad743755..6cffd62fcb4 100644 --- a/packages/core/src/runtime/previewRasterHints.ts +++ b/packages/core/src/runtime/previewRasterHints.ts @@ -13,6 +13,7 @@ const RUNTIME_PLAYBACK_ATTRS = new Set([...PREVIEW_ONLY_ATTRS, CLIP_HIDD let sheet: HTMLStyleElement | null = null; let observer: MutationObserver | null = null; +let hintRuleSelector: string | null = null; const markedWith = new WeakMap(); // Other hints (opacity, filter) make boundaries of their own, so only a pure transform hint goes. @@ -82,7 +83,7 @@ function unmarkHolders(element: Element): void { } // Only an element hinted inline or by a rule can carry will-change; neither check reads style. -function hintRules(): string { +function readHintRules(): string { const selectors: string[] = []; const collect = (rules: CSSRuleList, relative: boolean): void => { for (const rule of rules) { @@ -107,28 +108,30 @@ function hintRules(): string { return selectors.join(","); } -function mayHint(element: Element, rules: string): boolean { +// Re-read where the page is re-checked, when a style or link arrives or loads. +function hintRules(): string { + return (hintRuleSelector ??= readHintRules()); +} + +function mayHint(element: Element): boolean { if ((element as HTMLElement).style?.getPropertyValue("will-change")) return true; + const rules = hintRules(); return rules !== "" && element.matches(rules); } function followTree(root: Element, batch: FollowBatch): void { - batch.rules ??= hintRules(); const decidedBeforeAnyWrite: Array<[Element, boolean]> = []; for (const element of [root, ...root.querySelectorAll("*")]) { if (batch.checked.has(element)) continue; batch.checked.add(element); const readOwnHints = element === root || batch.changed.has(element); - decidedBeforeAnyWrite.push([ - element, - mayHint(element, batch.rules) && fits(element, readOwnHints), - ]); + decidedBeforeAnyWrite.push([element, mayHint(element) && fits(element, readOwnHints)]); } for (const [element, drop] of decidedBeforeAnyWrite) writeMark(element, drop); for (const [element] of decidedBeforeAnyWrite) unmarkHolders(element); } -type FollowBatch = { changed: Set; checked: Set; rules?: string }; +type FollowBatch = { changed: Set; checked: Set }; // Scripts restyle inline every frame, so only what can break a mark is looked at. function followStyle(element: Element): void { @@ -150,9 +153,9 @@ function clearMarks(): void { // Every mark is decided before any is written, so the page restyles twice, not once per layer. function markLayers(): void { clearMarks(); - const rules = hintRules(); + hintRuleSelector = null; const layers = [...document.querySelectorAll("*")].filter( - (element) => mayHint(element, rules) && fits(element, true), + (element) => mayHint(element) && fits(element, true), ); for (const layer of layers) layer.setAttribute(PREVIEW_RASTER_ATTR, ""); } @@ -201,6 +204,7 @@ function restore(): void { observer = null; sheet?.remove(); sheet = null; + hintRuleSelector = null; clearMarks(); } From ea0238dd6dcac2fd2381caff960ab8c05170dc07 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 07:27:50 -0400 Subject: [PATCH 4/6] perf(core): raster hints watch no attributes while nothing is marked or hinted --- .../src/runtime/previewRasterHints.test.ts | 26 ++++++++ .../core/src/runtime/previewRasterHints.ts | 62 +++++++++++++------ 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/packages/core/src/runtime/previewRasterHints.test.ts b/packages/core/src/runtime/previewRasterHints.test.ts index 4273f0b24b8..8e2cffa5632 100644 --- a/packages/core/src/runtime/previewRasterHints.test.ts +++ b/packages/core/src/runtime/previewRasterHints.test.ts @@ -232,6 +232,32 @@ describe("setPreviewRasterScale", () => { sheet.remove(); }); + it("walks no holders for a mounted scene with nothing marked in or around it", async () => { + setPreviewRasterScale(0.5); + const walks = vi.spyOn(Element.prototype, "closest"); + const scene = document.createElement("div"); + for (let i = 0; i < 50; i += 1) layer("position: absolute", scene); + document.body.append(scene); + await Promise.resolve(); + const count = walks.mock.calls.length; + walks.mockRestore(); + expect(count).toBeLessThanOrEqual(1); + }); + + it("does no work for attribute and style writes on a page that nothing hints", async () => { + const paths = Array.from({ length: 20 }, () => layer("position: absolute")); + setPreviewRasterScale(0.5); + const walks = vi.spyOn(Element.prototype, "querySelectorAll"); + for (const path of paths) { + path.setAttribute("fill", "#fff"); + path.style.left = "4px"; + } + await Promise.resolve(); + const count = walks.mock.calls.length; + walks.mockRestore(); + expect(count).toBe(0); + }); + it("re-checks the page when a linked stylesheet loads", async () => { const sheet = document.createElement("style"); document.head.append(sheet); diff --git a/packages/core/src/runtime/previewRasterHints.ts b/packages/core/src/runtime/previewRasterHints.ts index 6cffd62fcb4..d6dce24cbf7 100644 --- a/packages/core/src/runtime/previewRasterHints.ts +++ b/packages/core/src/runtime/previewRasterHints.ts @@ -14,6 +14,8 @@ const RUNTIME_PLAYBACK_ATTRS = new Set([...PREVIEW_ONLY_ATTRS, CLIP_HIDD let sheet: HTMLStyleElement | null = null; let observer: MutationObserver | null = null; let hintRuleSelector: string | null = null; +let watchingAttributes: boolean | null = null; +const marked = new Set(); const markedWith = new WeakMap(); // Other hints (opacity, filter) make boundaries of their own, so only a pure transform hint goes. @@ -53,19 +55,23 @@ function inlineFit(element: Element, style: CSSStyleDeclaration): string { // the layer, so that only happens when the element itself changed. function fits(element: Element, readOwnHints: boolean): boolean { if (element === sheet) return false; - const marked = element.hasAttribute(PREVIEW_RASTER_ATTR); - if (marked && readOwnHints) element.removeAttribute(PREVIEW_RASTER_ATTR); + const isMarked = marked.has(element); + if (isMarked && readOwnHints) setMark(element, false); const style = getComputedStyle(element); - const hintsFit = (marked && !readOwnHints) || onlyTransformHints(style); + const hintsFit = (isMarked && !readOwnHints) || onlyTransformHints(style); const drop = hintsFit && canDropHint(element, style); if (drop) markedWith.set(element, inlineFit(element, style)); return drop; } +function setMark(element: Element, on: boolean): void { + element.toggleAttribute(PREVIEW_RASTER_ATTR, on); + if (on) marked.add(element); + else marked.delete(element); +} + function writeMark(element: Element, drop: boolean): void { - if (drop !== element.hasAttribute(PREVIEW_RASTER_ATTR)) { - element.toggleAttribute(PREVIEW_RASTER_ATTR, drop); - } + if (drop !== marked.has(element)) setMark(element, drop); } function check(element: Element, readOwnHints: boolean): void { @@ -76,7 +82,7 @@ function unmarkHolders(element: Element): void { let holder = element.parentElement?.closest(MARKED); while (holder) { if (escapes(element, getComputedStyle(holder).zIndex !== "auto")) { - holder.removeAttribute(PREVIEW_RASTER_ATTR); + setMark(holder, false); } holder = holder.parentElement?.closest(MARKED); } @@ -97,8 +103,8 @@ function readHintRules(): string { } }; try { - for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) { - collect(sheet.cssRules, false); + for (const rules of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) { + if (rules !== sheet?.sheet) collect(rules.cssRules, false); } document.createDocumentFragment().querySelector(selectors.join(",") || "*"); } catch { @@ -114,7 +120,9 @@ function hintRules(): string { } function mayHint(element: Element): boolean { - if ((element as HTMLElement).style?.getPropertyValue("will-change")) return true; + if (marked.has(element) || (element as HTMLElement).style?.getPropertyValue("will-change")) { + return true; + } const rules = hintRules(); return rules !== "" && element.matches(rules); } @@ -128,6 +136,7 @@ function followTree(root: Element, batch: FollowBatch): void { decidedBeforeAnyWrite.push([element, mayHint(element) && fits(element, readOwnHints)]); } for (const [element, drop] of decidedBeforeAnyWrite) writeMark(element, drop); + if (!marked.size) return; for (const [element] of decidedBeforeAnyWrite) unmarkHolders(element); } @@ -135,7 +144,7 @@ type FollowBatch = { changed: Set; checked: Set }; // Scripts restyle inline every frame, so only what can break a mark is looked at. function followStyle(element: Element): void { - if (element.hasAttribute(PREVIEW_RASTER_ATTR)) { + if (marked.has(element)) { const style = getComputedStyle(element); if (style.position === "static" || inlineFit(element, style) !== markedWith.get(element)) { check(element, true); @@ -145,9 +154,8 @@ function followStyle(element: Element): void { } function clearMarks(): void { - for (const element of document.querySelectorAll(MARKED)) { - element.removeAttribute(PREVIEW_RASTER_ATTR); - } + for (const element of document.querySelectorAll(MARKED)) setMark(element, false); + marked.clear(); } // Every mark is decided before any is written, so the page restyles twice, not once per layer. @@ -157,7 +165,8 @@ function markLayers(): void { const layers = [...document.querySelectorAll("*")].filter( (element) => mayHint(element) && fits(element, true), ); - for (const layer of layers) layer.setAttribute(PREVIEW_RASTER_ATTR, ""); + for (const layer of layers) setMark(layer, true); + watch(); } function followAttribute(record: MutationRecord, batch: FollowBatch): void { @@ -186,17 +195,33 @@ function ownAttributeChanges(records: MutationRecord[]): Set { return changed; } +// With nothing marked and no rule giving a hint, no attribute or inline style change can make or +// break a mark: only added elements are followed, and a hint missed that way stays as authored. +function watch(): void { + const attributes = marked.size > 0 || hintRules() !== ""; + if (!observer || attributes === watchingAttributes) return; + watchingAttributes = attributes; + observer.observe(document.documentElement, { subtree: true, childList: true, attributes }); +} + +function forgetRemovedMarks(records: MutationRecord[]): void { + if (!records.some((record) => record.removedNodes.length)) return; + for (const element of marked) if (!element.isConnected) marked.delete(element); +} + function follow(records: MutationRecord[]): void { const rules = records.flatMap(addedRules); for (const rule of rules) { rule.addEventListener("load", () => observer && markLayers(), { once: true }); } if (rules.length) return markLayers(); + forgetRemovedMarks(records); const batch: FollowBatch = { changed: ownAttributeChanges(records), checked: new Set() }; for (const record of records) { if (record.type === "attributes") followAttribute(record, batch); else for (const node of record.addedNodes) if (isElementNode(node)) followTree(node, batch); } + watch(); } function restore(): void { @@ -205,6 +230,7 @@ function restore(): void { sheet?.remove(); sheet = null; hintRuleSelector = null; + watchingAttributes = null; clearMarks(); } @@ -218,9 +244,5 @@ export function setPreviewRasterScale(scale: number): void { (document.head ?? document.documentElement).append(sheet); markLayers(); observer = new MutationObserver(follow); - observer.observe(document.documentElement, { - subtree: true, - childList: true, - attributes: true, - }); + watch(); } From 32984da486462c423bb37754892fdc11a0726e76 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 07:55:32 -0400 Subject: [PATCH 5/6] fix(core): a copied or re-attached marked layer keeps being followed --- .../src/runtime/previewRasterHints.test.ts | 27 +++++++++++++++++++ .../core/src/runtime/previewRasterHints.ts | 5 ++++ 2 files changed, 32 insertions(+) diff --git a/packages/core/src/runtime/previewRasterHints.test.ts b/packages/core/src/runtime/previewRasterHints.test.ts index 8e2cffa5632..a656e66740e 100644 --- a/packages/core/src/runtime/previewRasterHints.test.ts +++ b/packages/core/src/runtime/previewRasterHints.test.ts @@ -258,6 +258,33 @@ describe("setPreviewRasterScale", () => { expect(count).toBe(0); }); + it("gives the hint back to a marked layer re-attached after removal once a child needs it", async () => { + const words = layer(POSITIONED); + const child = layer("", words); + setPreviewRasterScale(0.5); + expect(marked(words)).toBe(true); + words.remove(); + await Promise.resolve(); + document.body.append(words); + await Promise.resolve(); + child.style.position = "fixed"; + await Promise.resolve(); + expect(marked(words)).toBe(false); + }); + + it("follows a copy of a marked layer that arrives with the mark", async () => { + const words = layer(POSITIONED); + layer("", words); + setPreviewRasterScale(0.5); + const copy = words.cloneNode(true) as HTMLElement; + words.remove(); + document.body.append(copy); + await Promise.resolve(); + (copy.firstElementChild as HTMLElement).style.position = "fixed"; + await Promise.resolve(); + expect(marked(copy)).toBe(false); + }); + it("re-checks the page when a linked stylesheet loads", async () => { const sheet = document.createElement("style"); document.head.append(sheet); diff --git a/packages/core/src/runtime/previewRasterHints.ts b/packages/core/src/runtime/previewRasterHints.ts index d6dce24cbf7..43a5d051676 100644 --- a/packages/core/src/runtime/previewRasterHints.ts +++ b/packages/core/src/runtime/previewRasterHints.ts @@ -70,6 +70,10 @@ function setMark(element: Element, on: boolean): void { else marked.delete(element); } +function adoptCopiedMark(element: Element): void { + if (element.hasAttribute(PREVIEW_RASTER_ATTR)) marked.add(element); +} + function writeMark(element: Element, drop: boolean): void { if (drop !== marked.has(element)) setMark(element, drop); } @@ -132,6 +136,7 @@ function followTree(root: Element, batch: FollowBatch): void { for (const element of [root, ...root.querySelectorAll("*")]) { if (batch.checked.has(element)) continue; batch.checked.add(element); + adoptCopiedMark(element); const readOwnHints = element === root || batch.changed.has(element); decidedBeforeAnyWrite.push([element, mayHint(element) && fits(element, readOwnHints)]); } From 87c82d43dbfbf60371406c0e4e263d61fbd9d2c7 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 08:20:56 -0400 Subject: [PATCH 6/6] chore(producer): raise the packed size budget by the 336 bytes the raster-hint fix adds The producer bundles the runtime. On main (aae32e43ed) it packs at 773560 bytes (2821768 unpacked) and this branch at 773896 (2822665): packed rises by 336, unpacked by 897. --- scripts/package-size-budgets.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/package-size-budgets.json b/scripts/package-size-budgets.json index c078389849e..ceb05c18314 100644 --- a/scripts/package-size-budgets.json +++ b/scripts/package-size-budgets.json @@ -36,8 +36,8 @@ "unpacked": 541601 }, "@hyperframes/producer": { - "packed": 768210, - "unpacked": 2807938 + "packed": 768546, + "unpacked": 2808835 }, "@hyperframes/sdk": { "packed": 90662,