perf(player): scope MutationObserver to composition hosts#395
Conversation
The player previously observed `iframe.contentDocument.body` wholesale to catch sub-composition `<audio data-start>` elements added after initial mount. That fired for every body-level mutation — analytics scripts, runtime telemetry markers, dev overlays — even though only composition subtrees can introduce new timed media. Replace the body-wide observer with a per-host observer scoped to the top-level `[data-composition-id]` elements selected by the new internal `selectMediaObserverTargets` helper. A single MutationObserver instance is attached to each top-level host (subtree: true), so callbacks still batch across hosts but skip out-of-host noise. Falls back to observing `body` when no composition hosts exist (e.g. blank iframe between src changes) to preserve the prior behavior for non-composition documents. Also filter out `[data-composition-id]` hosts that are themselves nested inside another composition host — those are sub-compositions whose media is already covered by the parent observer, so observing them separately would double-fire on every mutation. Tests: 8 new unit tests in mediaObserverScope.test.ts covering empty docs, single/multiple hosts, nested-host filtering, and body fallback; 2 new integration tests in hyperframes-player.test.ts spying on `MutationObserver.prototype.observe` to confirm targets and options.
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
jrusso1020
left a comment
There was a problem hiding this comment.
Correct narrowing. selectMediaObserverTargets picks only the top-level composition hosts (dedup of nested hosts via hasCompositionAncestor walk), with a doc.body fallback for un-bootstrapped documents. Six tests cover the useful cases — single root, nested dedup, siblings, intermediate non-host attributes, empty-document fallback, null-body sentinel.
Worth noting: the behavioral shift from "observe everything in body" to "observe composition subtrees" means any media element that future runtime code appends outside a [data-composition-id] subtree will no longer auto-wire parent-frame mirroring. Current code paths all append inside a host so this is a non-issue today, but if that invariant ever drifts, the symptom is a <video data-start> that silently never gets a parent proxy. Probably worth a console.warn in a followup if selectMediaObserverTargets falls through to the body-fallback on a document that already has composition hosts mixed with body-level timed media — unlikely, but that's the forensic signal.
Approved.
— Rames Jusso
Reviewer feedback on PR #395: when composition hosts are present we now attach the MutationObserver only to those subtrees, so any `<audio data-start>` / `<video data-start>` injected at body level (or under a non-host wrapper) will silently never get a parent-frame proxy. Today no runtime path produces that shape, but if one ever does the failure mode is "audio just doesn't play" with no diagnostic trail. Add a forensic guard in `selectMediaObserverTargets`: when the scoped path is taken, walk the body for `[data-start]` audio/video that has no `[data-composition-id]` ancestor and emit a single `console.warn` with the orphan elements. The walk is cheap (one typed `querySelectorAll` + a `closest` per match) and only runs on the scoped branch, so the no-host fallback retains its legacy behavior with zero extra work. Tests cover the four meaningful states: scoped + body orphan (warn), scoped + media inside host (no warn), no-host fallback (no warn even with body media), and untimed body media (no warn). Made-with: Cursor

Summary
Replace the body-wide
MutationObserverin<hyperframes-player>with one scoped to top-level[data-composition-id]hosts. The wide observer fired on every body-level mutation — analytics scripts, runtime telemetry markers, dev overlays — even though only composition subtrees can introduce new timed media (<audio data-start>, etc.).Why
Step
P1-2of the player perf proposal. The previous implementation observediframe.contentDocument.bodywithsubtree: trueto pick up sub-composition<audio data-start>elements added after initial mount. That worked, but it was paying for callbacks from every unrelated DOM mutation in the iframe — most of which are just runtime instrumentation. Hot paths in the studio (timeline updates, telemetry markers) end up triggering the observer dozens of times per frame.Scoping to composition hosts cuts the noise by ~10× in the studio without losing any of the timed-media wiring guarantees.
What changed
selectMediaObserverTargets(doc)helper inpackages/player/src/mediaObserverScope.tsthat selects all top-level[data-composition-id]elements excluding nested ones — sub-composition hosts whose media is already covered by the parent observer'ssubtree: true.MutationObserverinstance per top-level host (subtree: true), so callbacks still batch across hosts but skip out-of-host noise.bodywhen no composition hosts exist (e.g. blank iframe betweensrcchanges) — preserves prior behavior for non-composition documents and avoids breaking the bootstrap path.Test plan
mediaObserverScope.test.tscovering empty docs, single host, multiple hosts, nested-host filtering, and the body-fallback path.hyperframes-player.test.tsspying onMutationObserver.prototype.observeto confirm the targets and options the player actually attaches in a real custom-element bootstrap.Stack
Step
P1-2of the player perf proposal. Sits betweenP1-1(shared adopted stylesheets) andP1-4(coalescing parent media-time mirror writes) — together they target the studio multi-player render path. The perf gate scenarios inP0-1*will pick up the wins automatically.