Skip to content

Commit 24180a2

Browse files
authored
perf: improve instance selection performance (#5240)
We have html element mutations in a few places and each can add ~250ms of repaint time on large projects. Here I got rid of it in two places 1. Collapsed logic enforced document height to preserve scroll position. Instead I created an absolutely positioned div which enforced scroll where collapsed elements are reset. 2. Scroll into view logic defined position absolute on scroll container. Though it's not necessary on html element so just ignored it.
1 parent 68621fa commit 24180a2

File tree

4 files changed

+37
-38
lines changed

4 files changed

+37
-38
lines changed

apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type Instance,
88
type Props,
99
descendantComponent,
10+
rootComponent,
1011
} from "@webstudio-is/sdk";
1112
import {
1213
theme,
@@ -344,7 +345,7 @@ export const PropsSectionContainer = ({
344345
});
345346

346347
const propsMetas = useStore($selectedInstancePropsMetas);
347-
if (propsMetas.size === 0) {
348+
if (propsMetas.size === 0 || instance.component === rootComponent) {
348349
return;
349350
}
350351

apps/builder/app/canvas/collapsed.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { $selectedBreakpoint } from "~/shared/nano-states";
1111
import { $selectedPage } from "~/shared/awareness";
1212
import { serverSyncStore } from "~/shared/sync";
13+
import { doNotTrackMutation } from "~/shared/dom-utils";
1314

1415
const isHtmlTag = (tag: string): tag is HtmlTags =>
1516
htmlTags.includes(tag as HtmlTags);
@@ -34,22 +35,6 @@ const isSelectorSupported = (selector: string) => {
3435
}
3536
};
3637

37-
// This mark helps us detect if mutations were caused by the collapse algorithm
38-
const markCollapsedMutationProperty = `--ws-sys-collapsed-mutation`;
39-
40-
/**
41-
* Avoid infinite loop of mutations
42-
*/
43-
export const hasCollapsedMutationRecord = (
44-
mutationRecords: MutationRecord[]
45-
) => {
46-
return mutationRecords.some((record) =>
47-
record.type === "attributes"
48-
? (record.oldValue?.includes(markCollapsedMutationProperty) ?? false)
49-
: false
50-
);
51-
};
52-
5338
const getInstanceSize = (instanceId: string, tagName: HtmlTags | undefined) => {
5439
const metas = $registeredComponentMetas.get();
5540
const breakpoints = $breakpoints.get();
@@ -246,14 +231,25 @@ const recalculate = () => {
246231
// If most elements are collapsed at the next step, scrollHeight becomes equal to clientHeight,
247232
// which resets the scroll position. To prevent this, we set the document's height to the current scrollHeight
248233
// to preserve the scroll position.
249-
const preserveHeight = document.documentElement.style.height;
250-
251-
// Mark that we are in the process of recalculating collapsed elements
252-
document.documentElement.style.setProperty(
253-
markCollapsedMutationProperty,
254-
`true`
255-
);
256-
document.documentElement.style.height = `${document.documentElement.scrollHeight}px`;
234+
// use nested element to avoid full page repaint and freeze on big projects
235+
let collapsedElement = document.querySelector(
236+
"#ws-collapsed"
237+
) as null | HTMLElement;
238+
if (!collapsedElement) {
239+
collapsedElement = document.createElement("div") as HTMLElement;
240+
collapsedElement.style.position = "absolute";
241+
collapsedElement.style.top = "0px";
242+
collapsedElement.style.left = "0px";
243+
collapsedElement.style.right = "0px";
244+
collapsedElement.setAttribute("id", "ws-collapsed");
245+
collapsedElement.setAttribute("hidden", "true");
246+
// Mark that we are in the process of recalculating collapsed elements
247+
// to avoid infinite loop of mutations
248+
doNotTrackMutation(collapsedElement);
249+
document.documentElement.appendChild(collapsedElement);
250+
}
251+
collapsedElement.removeAttribute("hidden");
252+
collapsedElement.style.height = `${document.documentElement.scrollHeight}px`;
257253

258254
// Now combine all operations in batches.
259255

@@ -299,8 +295,7 @@ const recalculate = () => {
299295
element.setAttribute(collapsedAttribute, value);
300296
}
301297

302-
document.documentElement.style.height = preserveHeight;
303-
document.documentElement.style.removeProperty(markCollapsedMutationProperty);
298+
collapsedElement.setAttribute("hidden", "true");
304299
};
305300

306301
/**

apps/builder/app/canvas/instance-selected.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ import {
2727
} from "~/shared/dom-utils";
2828
import { subscribeScrollState } from "~/canvas/shared/scroll-state";
2929
import { $selectedInstanceOutline } from "~/shared/nano-states";
30-
import {
31-
hasCollapsedMutationRecord,
32-
setDataCollapsed,
33-
} from "~/canvas/collapsed";
30+
import { setDataCollapsed } from "~/canvas/collapsed";
3431
import type { InstanceSelector } from "~/shared/tree-utils";
3532
import { $awareness } from "~/shared/awareness";
3633

@@ -236,7 +233,7 @@ const subscribeSelectedInstance = (
236233

237234
// Lightweight update
238235
const updateOutline: MutationCallback = (mutationRecords) => {
239-
if (hasCollapsedMutationRecord(mutationRecords)) {
236+
if (hasDoNotTrackMutationRecord(mutationRecords)) {
240237
return;
241238
}
242239

@@ -250,10 +247,6 @@ const subscribeSelectedInstance = (
250247
return;
251248
}
252249

253-
if (hasCollapsedMutationRecord(mutationRecords)) {
254-
return;
255-
}
256-
257250
update();
258251
};
259252

apps/builder/app/shared/dom-utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export const getAllElementsBoundingBox = (
219219

220220
const doNotTrackMutationAttribute = "data-ws-do-not-track-mutation";
221221

222+
export const doNotTrackMutation = (element: Element) => {
223+
element.setAttribute(doNotTrackMutationAttribute, "true");
224+
};
225+
222226
export const hasDoNotTrackMutationAttribute = (element: Element) => {
223227
return element.hasAttribute(doNotTrackMutationAttribute);
224228
};
@@ -363,7 +367,10 @@ export const scrollIntoView = (anchor: HTMLElement, rect: DOMRect) => {
363367

364368
requestAnimationFrame(() => {
365369
const savedPosition = (scrollParent as HTMLElement).style.position;
366-
(scrollParent as HTMLElement).style.position = "relative";
370+
// avoid updating <html> to prevent full page repaint and freeze on big projects
371+
if (scrollParent.tagName !== "HTML") {
372+
(scrollParent as HTMLElement).style.position = "relative";
373+
}
367374

368375
const matrix = getViewportToLocalMatrix(scrollParent);
369376

@@ -387,6 +394,9 @@ export const scrollIntoView = (anchor: HTMLElement, rect: DOMRect) => {
387394

388395
scrollParent.removeChild(scrollDiv);
389396

390-
(scrollParent as HTMLElement).style.position = savedPosition;
397+
// avoid updating <html> to prevent full page repaint and freeze on big projects
398+
if (scrollParent.tagName !== "HTML") {
399+
(scrollParent as HTMLElement).style.position = savedPosition;
400+
}
391401
});
392402
};

0 commit comments

Comments
 (0)