Skip to content

Commit 7f8eb9c

Browse files
committed
perf(core): raster hints watch no attributes while nothing is marked or hinted
1 parent 6386e45 commit 7f8eb9c

2 files changed

Lines changed: 68 additions & 20 deletions

File tree

‎packages/core/src/runtime/previewRasterHints.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ describe("setPreviewRasterScale", () => {
232232
sheet.remove();
233233
});
234234

235+
it("walks no holders for a mounted scene with nothing marked in or around it", async () => {
236+
setPreviewRasterScale(0.5);
237+
const walks = vi.spyOn(Element.prototype, "closest");
238+
const scene = document.createElement("div");
239+
for (let i = 0; i < 50; i += 1) layer("position: absolute", scene);
240+
document.body.append(scene);
241+
await Promise.resolve();
242+
const count = walks.mock.calls.length;
243+
walks.mockRestore();
244+
expect(count).toBeLessThanOrEqual(1);
245+
});
246+
247+
it("does no work for attribute and style writes on a page that nothing hints", async () => {
248+
const paths = Array.from({ length: 20 }, () => layer("position: absolute"));
249+
setPreviewRasterScale(0.5);
250+
const walks = vi.spyOn(Element.prototype, "querySelectorAll");
251+
for (const path of paths) {
252+
path.setAttribute("fill", "#fff");
253+
path.style.left = "4px";
254+
}
255+
await Promise.resolve();
256+
const count = walks.mock.calls.length;
257+
walks.mockRestore();
258+
expect(count).toBe(0);
259+
});
260+
235261
it("re-checks the page when a linked stylesheet loads", async () => {
236262
const sheet = document.createElement("style");
237263
document.head.append(sheet);

‎packages/core/src/runtime/previewRasterHints.ts‎

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const RUNTIME_PLAYBACK_ATTRS = new Set<string>([...PREVIEW_ONLY_ATTRS, CLIP_HIDD
1414
let sheet: HTMLStyleElement | null = null;
1515
let observer: MutationObserver | null = null;
1616
let hintRuleSelector: string | null = null;
17+
let watchingAttributes: boolean | null = null;
18+
const marked = new Set<Element>();
1719
const markedWith = new WeakMap<Element, string>();
1820

1921
// 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 {
5355
// the layer, so that only happens when the element itself changed.
5456
function fits(element: Element, readOwnHints: boolean): boolean {
5557
if (element === sheet) return false;
56-
const marked = element.hasAttribute(PREVIEW_RASTER_ATTR);
57-
if (marked && readOwnHints) element.removeAttribute(PREVIEW_RASTER_ATTR);
58+
const isMarked = marked.has(element);
59+
if (isMarked && readOwnHints) setMark(element, false);
5860
const style = getComputedStyle(element);
59-
const hintsFit = (marked && !readOwnHints) || onlyTransformHints(style);
61+
const hintsFit = (isMarked && !readOwnHints) || onlyTransformHints(style);
6062
const drop = hintsFit && canDropHint(element, style);
6163
if (drop) markedWith.set(element, inlineFit(element, style));
6264
return drop;
6365
}
6466

67+
function setMark(element: Element, on: boolean): void {
68+
element.toggleAttribute(PREVIEW_RASTER_ATTR, on);
69+
if (on) marked.add(element);
70+
else marked.delete(element);
71+
}
72+
6573
function writeMark(element: Element, drop: boolean): void {
66-
if (drop !== element.hasAttribute(PREVIEW_RASTER_ATTR)) {
67-
element.toggleAttribute(PREVIEW_RASTER_ATTR, drop);
68-
}
74+
if (drop !== marked.has(element)) setMark(element, drop);
6975
}
7076

7177
function check(element: Element, readOwnHints: boolean): void {
@@ -76,7 +82,7 @@ function unmarkHolders(element: Element): void {
7682
let holder = element.parentElement?.closest(MARKED);
7783
while (holder) {
7884
if (escapes(element, getComputedStyle(holder).zIndex !== "auto")) {
79-
holder.removeAttribute(PREVIEW_RASTER_ATTR);
85+
setMark(holder, false);
8086
}
8187
holder = holder.parentElement?.closest(MARKED);
8288
}
@@ -97,8 +103,8 @@ function readHintRules(): string {
97103
}
98104
};
99105
try {
100-
for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) {
101-
collect(sheet.cssRules, false);
106+
for (const rules of [...document.styleSheets, ...(document.adoptedStyleSheets ?? [])]) {
107+
if (rules !== sheet?.sheet) collect(rules.cssRules, false);
102108
}
103109
document.createDocumentFragment().querySelector(selectors.join(",") || "*");
104110
} catch {
@@ -114,7 +120,9 @@ function hintRules(): string {
114120
}
115121

116122
function mayHint(element: Element): boolean {
117-
if ((element as HTMLElement).style?.getPropertyValue("will-change")) return true;
123+
if (marked.has(element) || (element as HTMLElement).style?.getPropertyValue("will-change")) {
124+
return true;
125+
}
118126
const rules = hintRules();
119127
return rules !== "" && element.matches(rules);
120128
}
@@ -128,14 +136,15 @@ function followTree(root: Element, batch: FollowBatch): void {
128136
decidedBeforeAnyWrite.push([element, mayHint(element) && fits(element, readOwnHints)]);
129137
}
130138
for (const [element, drop] of decidedBeforeAnyWrite) writeMark(element, drop);
139+
if (!marked.size) return;
131140
for (const [element] of decidedBeforeAnyWrite) unmarkHolders(element);
132141
}
133142

134143
type FollowBatch = { changed: Set<Element>; checked: Set<Element> };
135144

136145
// Scripts restyle inline every frame, so only what can break a mark is looked at.
137146
function followStyle(element: Element): void {
138-
if (element.hasAttribute(PREVIEW_RASTER_ATTR)) {
147+
if (marked.has(element)) {
139148
const style = getComputedStyle(element);
140149
if (style.position === "static" || inlineFit(element, style) !== markedWith.get(element)) {
141150
check(element, true);
@@ -145,9 +154,8 @@ function followStyle(element: Element): void {
145154
}
146155

147156
function clearMarks(): void {
148-
for (const element of document.querySelectorAll(MARKED)) {
149-
element.removeAttribute(PREVIEW_RASTER_ATTR);
150-
}
157+
for (const element of document.querySelectorAll(MARKED)) setMark(element, false);
158+
marked.clear();
151159
}
152160

153161
// Every mark is decided before any is written, so the page restyles twice, not once per layer.
@@ -157,7 +165,8 @@ function markLayers(): void {
157165
const layers = [...document.querySelectorAll("*")].filter(
158166
(element) => mayHint(element) && fits(element, true),
159167
);
160-
for (const layer of layers) layer.setAttribute(PREVIEW_RASTER_ATTR, "");
168+
for (const layer of layers) setMark(layer, true);
169+
watch();
161170
}
162171

163172
function followAttribute(record: MutationRecord, batch: FollowBatch): void {
@@ -186,17 +195,33 @@ function ownAttributeChanges(records: MutationRecord[]): Set<Element> {
186195
return changed;
187196
}
188197

198+
// With nothing marked and no rule giving a hint, no attribute or inline style change can make or
199+
// break a mark: only added elements are followed, and a hint missed that way stays as authored.
200+
function watch(): void {
201+
const attributes = marked.size > 0 || hintRules() !== "";
202+
if (!observer || attributes === watchingAttributes) return;
203+
watchingAttributes = attributes;
204+
observer.observe(document.documentElement, { subtree: true, childList: true, attributes });
205+
}
206+
207+
function forgetRemovedMarks(records: MutationRecord[]): void {
208+
if (!records.some((record) => record.removedNodes.length)) return;
209+
for (const element of marked) if (!element.isConnected) marked.delete(element);
210+
}
211+
189212
function follow(records: MutationRecord[]): void {
190213
const rules = records.flatMap(addedRules);
191214
for (const rule of rules) {
192215
rule.addEventListener("load", () => observer && markLayers(), { once: true });
193216
}
194217
if (rules.length) return markLayers();
218+
forgetRemovedMarks(records);
195219
const batch: FollowBatch = { changed: ownAttributeChanges(records), checked: new Set() };
196220
for (const record of records) {
197221
if (record.type === "attributes") followAttribute(record, batch);
198222
else for (const node of record.addedNodes) if (isElementNode(node)) followTree(node, batch);
199223
}
224+
watch();
200225
}
201226

202227
function restore(): void {
@@ -205,6 +230,7 @@ function restore(): void {
205230
sheet?.remove();
206231
sheet = null;
207232
hintRuleSelector = null;
233+
watchingAttributes = null;
208234
clearMarks();
209235
}
210236

@@ -218,9 +244,5 @@ export function setPreviewRasterScale(scale: number): void {
218244
(document.head ?? document.documentElement).append(sheet);
219245
markLayers();
220246
observer = new MutationObserver(follow);
221-
observer.observe(document.documentElement, {
222-
subtree: true,
223-
childList: true,
224-
attributes: true,
225-
});
247+
watch();
226248
}

0 commit comments

Comments
 (0)