perf(studio): the layer walk skips clips the runtime hid, so scrubbing a long film stays smooth (#6416)

The off-canvas indicators (always mounted) and the Layers panel rebuild their layer list every
100 ms while a scrub mutates the preview, and each rebuild read the computed style of every element
of every scene, on screen or not. The runtime already hides everything inside a clip its visibility
pass hid, so those subtrees hold no layer: the walk now skips a clip marked data-hf-clip-hidden
with an inline visibility of hidden. The marker constant moves to studio-preview-mark so the runtime
and Studio share one owner.
This commit is contained in:
Miguel Ángel
2026-10-26 13:22:09 +00:00
committed by GitHub
parent 575f68cf5b
commit dc56b3d20a
4 changed files with 47 additions and 4 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
/** The visibility pass's mark on a timed clip it hides; the runtime's rules hide the clip's contents by it. */
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
import { CLIP_HIDDEN_ATTR } from "../studioPreviewMark";
export { CLIP_HIDDEN_ATTR };
type ShaderWindow = Window & {
HyperShader?: unknown;
+4
View File
@@ -5,6 +5,7 @@ export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
export const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
export const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
export const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
export const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
export const PREVIEW_ONLY_ATTRS = [
@@ -16,6 +17,9 @@ export const PREVIEW_ONLY_ATTRS = [
RUNTIME_STAMPED_ATTR,
] as const;
export const isClipHiddenByRuntime = (el: HTMLElement) =>
el.hasAttribute(CLIP_HIDDEN_ATTR) && el.style.visibility === "hidden";
export function applyAuthoredTiming(
el: Element,
attrs: ReadonlyArray<readonly [name: string, value: string]>,
@@ -380,3 +380,41 @@ describe("domEditSelectionToFacts hasAudio", () => {
expect((await factsFor(`<div id="d1"></div>`)).hasAudio).toBe(false);
});
});
describe("collectDomEditLayerItems and clips the runtime hid", () => {
afterEach(() => {
document.body.innerHTML = "";
document.head.innerHTML = "";
});
// As the runtime marks and styles a clip outside the playhead, with its rule that hides the clip's contents.
function filmWithHiddenScene(hiddenStyle: string): HTMLElement {
document.head.innerHTML = `<style>[data-hf-clip-hidden][data-hf-clip-hidden] * { visibility: hidden !important; }</style>`;
const cells = (prefix: string, n: number) =>
Array.from({ length: n }, (_, i) => `<div id="${prefix}-${i}"></div>`).join("");
document.body.innerHTML =
`<div data-composition-id="main">` +
`<div id="now" data-start="0" data-duration="5">${cells("now", 3)}</div>` +
`<div id="later" data-start="5" data-duration="5" data-hf-clip-hidden style="${hiddenStyle}">${cells("later", 50)}</div>` +
`</div>`;
return document.querySelector<HTMLElement>("[data-composition-id]")!;
}
it("lists what is on screen without reading the style of anything inside a hidden clip", () => {
const root = filmWithHiddenScene("visibility: hidden");
const read = vi.spyOn(window, "getComputedStyle");
const ids = collectDomEditLayerItems(root, opts).map((item) => item.id);
const insideHidden = read.mock.calls.filter(([el]) =>
document.getElementById("later")!.contains(el),
);
expect(ids).toEqual(["now", "now-0", "now-1", "now-2"]);
expect(insideHidden).toHaveLength(0);
});
// Older shader builds reveal a clip through its inline style alone, leaving the mark on.
it("still lists a marked clip whose inline style shows it", () => {
const root = filmWithHiddenScene("visibility: visible");
const ids = collectDomEditLayerItems(root, opts).map((item) => item.id);
expect(ids).toContain("later");
});
});
@@ -1,4 +1,4 @@
import { PREVIEW_ONLY_ATTRS } from "@hyperframes/core/studio-preview-mark";
import { isClipHiddenByRuntime, PREVIEW_ONLY_ATTRS } from "@hyperframes/core/studio-preview-mark";
import { knownSourceAnswer, probeSourceElement, type ProbeTarget } from "./probeSourceElement";
import { isAudibleVideoNode } from "../../player/lib/timelineElementHelpers";
import type { PatchOperation } from "../../utils/sourcePatcher";
@@ -454,7 +454,7 @@ export function collectDomEditLayerItems(
const items: DomEditLayerItem[] = [];
// fallow-ignore-next-line complexity
const visit = (el: HTMLElement, depth: number) => {
if (items.length >= maxItems) return;
if (items.length >= maxItems || isClipHiddenByRuntime(el)) return;
const entry = readDomEditLayerWalkEntry(el, options.activeCompositionPath, cache);
if (entry) {