From 53129a4ba10a72364c2bb991dabf6d9fc4ff2d35 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 25 Oct 2026 09:32:51 -0400 Subject: [PATCH] perf(core): scene cuts stop re-reading every clip's position --- packages/core/src/runtime/init.test.ts | 94 ++++++++++++++++++++++++++ packages/core/src/runtime/init.ts | 19 +++--- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts index 57b606a4f5b..20c7d823a4b 100644 --- a/packages/core/src/runtime/init.test.ts +++ b/packages/core/src/runtime/init.test.ts @@ -3631,6 +3631,100 @@ describe("initSandboxRuntimeModular", () => { expect(video.style.visibility).toBe("hidden"); }); + it("keeps a hidden clip's in-flow reading when another timed clip is added", async () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-duration", "10"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + const clips = [0, 1, 2].map((i) => { + const clip = document.createElement("div"); + clip.id = `clip-${i}`; + clip.style.position = "absolute"; + clip.setAttribute("data-start", "5"); + clip.setAttribute("data-duration", "2"); + root.appendChild(clip); + return clip; + }); + window.__timelines = { main: createMockTimeline(10) }; + initSandboxRuntimeModular(); + const player = window.__player; + player?.seek(0); + const added = document.createElement("div"); + added.id = "added"; + added.setAttribute("data-start", "8"); + added.setAttribute("data-duration", "1"); + root.appendChild(added); + await vi.waitFor(() => expect(JSON.stringify(window.__clipManifest)).toContain('"added"')); + const reads = vi.spyOn(window, "getComputedStyle"); + player?.seek(1); + player?.seek(9); + const clipReads = reads.mock.calls.filter(([el]) => clips.includes(el as HTMLDivElement)); + reads.mockRestore(); + expect(clipReads).toHaveLength(0); + }); + + it("re-reads a clip's flow when its own inline position changes", () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-duration", "10"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + const holder = document.createElement("div"); + holder.style.position = "absolute"; + root.appendChild(holder); + const clip = document.createElement("div"); + clip.style.position = "absolute"; + clip.setAttribute("data-start", "5"); + clip.setAttribute("data-duration", "2"); + holder.appendChild(clip); + window.__timelines = { main: createMockTimeline(10) }; + initSandboxRuntimeModular(); + const player = window.__player; + player?.seek(0); + expect(clip.style.display).not.toBe("none"); + clip.style.position = "static"; + player?.seek(1); + expect(clip.style.display).toBe("none"); + }); + + it("re-reads a clip's flow when its classes change", () => { + const sheet = document.createElement("style"); + sheet.textContent = ".placed { position: absolute }"; + document.head.appendChild(sheet); + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-duration", "10"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + const holder = document.createElement("div"); + holder.style.position = "absolute"; + root.appendChild(holder); + const clip = document.createElement("div"); + clip.className = "placed"; + clip.setAttribute("data-start", "5"); + clip.setAttribute("data-duration", "2"); + holder.appendChild(clip); + window.__timelines = { main: createMockTimeline(10) }; + initSandboxRuntimeModular(); + const player = window.__player; + player?.seek(0); + expect(clip.style.display).not.toBe("none"); + clip.className = ""; + player?.seek(1); + expect(clip.style.display).toBe("none"); + sheet.remove(); + }); + it("un-hides a later root-level video once active, even though it starts inactive and unstyled", () => { // Root-level `[data-start]` children with no authored `position` start out // `position: static` until `applyClipLayout` force-absolutizes them, so a diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index f0b4a60f8ce..791a0d56f48 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2700,14 +2700,16 @@ export function initSandboxRuntimeModular(): void { // In-flow clips must leave the flow when hidden — `visibility:hidden` reserves // their layout box, so a split sibling would stack below the active half // instead of overlapping it. Positioned clips keep `visibility:hidden` (cheaper, - // and avoids disturbing absolute media playback). Computed once per element. - let timedClipInFlow = new WeakMap(); + // and avoids disturbing absolute media playback). Re-read only when the clip's + // own inline position or classes change: the read forces a style recalc. + const timedClipInFlow = new WeakMap(); const isTimedClipInFlow = (el: Element): boolean => { + const inputs = `${(el as HTMLElement).style?.position ?? ""}|${el.getAttribute("class") ?? ""}`; const cached = timedClipInFlow.get(el); - if (cached !== undefined) return cached; + if (cached?.inputs === inputs) return cached.inFlow; const pos = window.getComputedStyle(el).position; const inFlow = pos === "static" || pos === "relative" || pos === "sticky"; - timedClipInFlow.set(el, inFlow); + timedClipInFlow.set(el, { inputs, inFlow }); return inFlow; }; @@ -2726,13 +2728,10 @@ export function initSandboxRuntimeModular(): void { return leaf; }; - // Both caches key on live DOM facts that change when the timed-element set - // changes: leaf status flips when a clip gains/loses a nested `[data-start]` - // descendant (sub-composition load/unload, studio insert/delete), and a swapped - // element can reuse an identity whose in-flow status differs. WeakMap has no - // `clear()`, so drop both maps wholesale — re-derived lazily on next access. + // Leaf status flips when a clip gains/loses a nested `[data-start]` descendant + // (sub-composition load/unload, studio insert/delete), so it is dropped + // wholesale when the timed-element set changes; WeakMap has no `clear()`. const invalidateTimedClipCaches = () => { - timedClipInFlow = new WeakMap(); timedClipIsLeaf = new WeakMap(); };