perf(player): srcdoc composition switching for studio#398
perf(player): srcdoc composition switching for studio#398vanceingalls wants to merge 1 commit intoperf/p3-1-sync-seek-same-originfrom
Conversation
|
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.
Good swap. Pre-fetching the composition HTML and handing it to the iframe via srcdoc is the classic "skip the navigation" win, and the AbortController wiring means a studio project-switch mid-fetch doesn't leave an orphan in-flight request firing into an unmounted component. The comments on the ordering (setting srcdoc/src before appendChild so the first load event fires for the real composition, not an intermediate about:blank) are load-bearing enough that I'm glad they're in-code rather than tribal.
Test coverage is thorough — initial attribute, post-connect change, removal distinguished from empty string, both-srcdoc-and-src-present letting the browser arbitrate via spec precedence, _ready reset on swap. The empty-string vs removal distinction is the kind of thing that only matters once a caller actually exercises it, and nice to see it pinned now.
Non-blocking observations:
-
The studio fetch falls back to
srcon any non-AbortError failure. For consistency, the fallback branch could emit an analytics/perf metric (leveraging #393) so we can spot how often the srcdoc fast path is actually used in the wild. Not a blocker. -
fetch(url, { signal })thenplayer.setAttribute("srcdoc", html)copies the HTML text into an attribute; for large compositions (hundreds of KB inline) this adds a ~1 copy plus DOM attribute cost. Probably still a net win vs network navigation, but worth remembering once compositions grow.
Approved.
— Rames Jusso
3e9f8fc to
a3f8cef
Compare
9c5a23d to
048a3c0
Compare
a3f8cef to
d77730f
Compare
048a3c0 to
61189b6
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
I tested the stack locally with agent-browser sanity checks in Studio plus repeated bun run player:perf -- --mode=measure --scenarios=load,fps,scrub,drift,parity --runs=2 runs on both this stack and main artifacts.
The implementation here looks directionally good: srcdoc support in <hyperframes-player> is wired correctly, the AbortController cleanup is correct, and I don’t see a correctness issue with the fetch-then-inline path for same-origin previews.
The important caveat is evidentiary, not correctness: the perf harness that shows the wins is still measuring the standalone /host.html?fixture=... player flow, not the Studio Player.tsx fetch -> srcdoc preview path introduced in this PR. So I buy the optimization, but I would not over-claim measured Studio impact from the current numbers alone. If we want this PR to carry a strong performance claim, we should either add a Studio-specific switch benchmark later or narrow the wording to “expected Studio composition-switch improvement” rather than presenting it as already proven by the current harness.
Not blocking this PR on that, but I wanted it called out explicitly because the distinction matters at review time.

Summary
Adds
srcdocsupport to<hyperframes-player>and uses it from studio'sPlayer.tsxso composition switches no longer trigger an iframe navigation. Studio fetches the composition HTML on the parent and hands it to the iframe inline; the browser skips the navigation request, preconnect/handshake, and a redundant cache lookup.Why
Step
P3-2of the player perf proposal. Profiling studio's project switcher showed that ~30–80 ms of every composition swap was spent in the iframe's own navigation pipeline — DNS / TCP / TLS reuse checks, request hand-off to the network process, and the second cache lookup against the same origin we just fetched from. For same-origin previews (/api/projects/.../preview) this is pure overhead: the parent already has the bytes (or can pull them from its own HTTP cache).srcdoclets us skip that pipeline entirely. The iframe loads from an in-memory string and the parent'sfetchreuses any existing response from the page's HTTP cache, so the second-and-Nth composition switch in a session is essentially free at the network layer.What changed
<hyperframes-player>(packages/player/src/hyperframes-player.ts)srcdoctoobservedAttributesso runtime swaps actually fireattributeChangedCallback.srcdocandsrcare forwarded to the inner iframe — no manual precedence; the HTML spec already sayssrcdocwins when both are present, so the browser handles arbitration.srcdocbranch inattributeChangedCallback:_ready = falseon every change so the next iframeloadevent re-runs probe/control/poster setup against the fresh document.setAttribute("srcdoc", "")(deliberate empty document) fromremoveAttribute("srcdoc")(fall back tosrc) — the former propagates an empty-string srcdoc; the latter strips the attribute so a previously-setsrccan take over.Studio
Player.tsx(packages/studio/src/player/components/Player.tsx)AbortControllerand resolvedurloutside the dynamic-import.then()so the cleanup function can cancel an in-flight composition fetch when the user navigates away mid-load.fetch(url, { signal })pulls the composition HTML on the parent.player.setAttribute("srcdoc", html).player.setAttribute("src", url). Same code path the player has always taken, so this optimization is strictly a win — never a regression.AbortError→ bail without touching the DOM (component is unmounting).appendChildso the iframe never loads an intermediateabout:blank. That matters because:loadevent must fire for the real composition; the existing handler treatsloadCountRef > 1as a hot-reload and replays the reveal animation. An extraabout:blankload would trigger the reveal on initial mount.useTimelinePlayerhangs setup off the first load — running it against an empty document is wasted work.Test plan
hyperframes-player.test.tscovering:srcdocis inobservedAttributes.srcdocset before connect forwards to the iframe on connect.srcdocset after connect forwards viaattributeChangedCallback._readyresets whensrcdocchanges soonIframeLoadreplays setup.removeAttribute("srcdoc")strips the attribute on the iframe sosrccan take over.srcdocis preserved (not treated as removal).srcandsrcdocset together: both get forwarded to the iframe and the browser arbitrates per spec.srcflow with no regression.Stack
Step
P3-2of the player perf proposal. Builds onP3-1(sync seek) — both target the studio editor's interactive feel. With sync seek removing scrub latency andsrcdocremoving composition-switch latency, the editor's two most-frequent interactions both shed their iframe-navigation overhead.