perf(studio): clicking an element in the preview selects it without restyling the whole film - #4707
Conversation
…estyling the whole film
Every hit test added a `* { pointer-events: auto !important }` style element to the
preview document and removed it again. Each insert and removal restyled every element,
and the head mutation made the off-canvas indicators rebuild their layer walk from
scratch. The override only let pointer-events:none elements into the hit list, and the
author pointer-events filter removed them again, so the hit test now uses the browser's
own result: the candidates are what Chrome itself hit-tests.
…ifts pointer suppression
somanshreddy
left a comment
There was a problem hiding this comment.
Verified claims
-
Diff scope confirmed.
packages/studio/src/utils/studioPreviewHelpers.ts(-80/+37):forcePointerEventsAuto/removePointerEventsOverride/POINTER_EVENTS_OVERRIDE_IDand the try/finally wrapping them are deleted from bothgetPreviewTargetFromPointerandgetAllPreviewTargetsFromPointer. The rest of the hit-test chain — nativeelementsFromPoint→filterAuthorInteractiveTargets→ full-bleed check →findGroupAtPointfallback →doc.elementFromPointfallback — is preserved verbatim, just de-nested out of the removedtry/finally.useInlineTextEditing.tsxonly rewords a comment (no logic change). One new test added tostudioPreviewHelpers.test.ts. -
The redundancy claim that makes the removal safe is independently confirmed, not just trusted from the PR body. The PR argues the override predates
filterAuthorInteractiveTargets/hasAuthorPointerEventsNone("the author pointer-events filter"), so by the time both existed together, the override did the expensive part (force everything into the hit list) and the filter immediately undid it (excluded pointer-events:none elements again) — pure redundant round-tripping. I checked this against real GitHub history viagh api repos/heygen-com/hyperframes/contents/<path>?ref=<sha>(bypassing local shallow-clone grafting, which falsely collapses history on this box):forcePointerEventsAutoalready existed at091137e3c(2026-06-22, "instant, flicker-free manual editing");hasAuthorPointerEventsNonewas introduced later, at02e9d6142(2026-07-04, "make canvas selection hit intended elements #1907"). Confirmed true, not just plausible narrative. -
Test suite passes.
bunx vitest run src/utils/studioPreviewHelpers.test.tsinpackages/studio: 12/12 pass, including the pre-existingpointer-events:noneancestor-inheritance tests. (Those mockdoc.elementsFromPointdirectly, so they don't exercise real-browser native pointer-events exclusion — that part rests on the PR's own disclosed headless-Chrome validation.) -
Mutation-tested the new regression test.
"hit-tests without adding styles to the preview document"asserts zero style/adoptedStyleSheet count at hit-test time and zero DOM mutations via a liveMutationObserver. I reintroduced the deleted override in a scratch edit and reran: this test failed exactly as expected (stylesAtHitTestwent from[0,0,0]to[1,1,0]), all 11 other tests still passed. Confirms it's a real regression guard, not mock-theater. Reverted immediately after. -
CI is genuinely green.
statusCheckRollup: zero non-SUCCESS/non-SKIPPED checks, zero IN_PROGRESS/QUEUED.mergeable: MERGEABLE,mergeStateStatus: BLOCKED(single approval clears the gate).
Transparency note (non-blocking)
I did not independently reproduce the two disclosed native-Chrome behavioral differences (a pointer-events:none child at z-index:-1 no longer occluding its parent; SVG keyword pointer-events values hit-testing per real CSS semantics instead of forced auto) — that requires the same real-browser headless setup the author used. Resting on their own side-by-side validation (5,280 points × 6 playhead times, only these two differences found), same as their measured latency numbers.
Verdict
Approve. The core safety claim (override redundant once the filter existed) is independently verified against real commit history, not just narrative. The diff is a clean, mechanical removal that preserves the rest of the hit-test chain untouched. The new test is a genuine, mutation-tested regression guard. All tests pass, CI is fully green. No blocking issues.
Upstream v0.8.89..v0.8.90: 3 commits, Studio-only (heygen-com#4707, heygen-com#4718, release). Clean merge, no conflicts. Render packages (core, engine, parsers, player, producer, shader-transitions) unchanged between the tags. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
What
Clicking or hovering an element in Studio's preview no longer restyles the whole film. On a 1,426-element text film, click-to-selected drops from 219 ms to 120 ms (median) and the inspector fills in at 68 ms instead of 163 ms. Nothing changes in what gets selected, except two edge cases noted below where the new result is the one Chrome itself gives.
Why
Selecting by hand in the preview felt slow, sometimes too slow to edit with. Every canvas hit test (a click runs up to four, a hover runs one per pointer move) added a
* { pointer-events: auto !important }style element to the preview document, calledelementsFromPoint, and removed it. That had two costs that grow with the number of elements:elementsFromPointand the nextgetComputedStyleeach recalculated style for the whole document (a trace showed 18 full-document recalcs of 1,414 elements per click);The override was added before the author pointer-events filter existed. Since that filter landed, every element the override let into the hit list (those with computed
pointer-events: none) is removed again, so the override did the expensive part and the filter undid it.How
getPreviewTargetFromPointerandgetAllPreviewTargetsFromPointercall the browser's ownelementsFromPointand keep the existing author pointer-events filter. The override helpers are deleted.Behaviour: on the heavy film, a grid of 5,280 points over six playhead times gave the same pick and the same click-through list from the old and new code, with no duplicates. A side-by-side run of both versions in headless Chrome found two edge cases that now follow the browser: a
pointer-events: nonechild painted under its parent (z-index: -1) no longer hides the parent from selection, and SVG shapes with keywordpointer-eventsvalues (fill,bounding-box) hit-test the way Chrome defines them instead of asauto.Test plan
studioPreviewHelpers.test.ts: both hit tests run with no style added to the preview document and no DOM mutation. Fails on the old code (style count[1, 1, 1], and separately the mutation list), passes on this branch. The file: 12 passed.hyperframes preview) with headless Chrome: pointerdown to the frame after the selection box and the inspector both show the new element, 24 clicks per run plus 8 drag starts, text-heavy repo film (packages/producer/tests/spanish-empire-cdn-inline, 1,426 elements) and a light one (sub-comp-t0, 33 elements).Measurements
Built CLI Studio, headless Chrome on a shared 8-core Linux box. A click is timed from pointerdown to the frame after both the selection box and the inspector show the new element; a drag start from the first pointermove to the first frame the box moved. Only runs whose 1-minute load stayed under 16 at start and end are pooled.
The cost grows with the number of elements: the light film is the same before and after within run-to-run spread (single-run medians range from 72 to 154 ms on both sides). The p95 column is dominated by the first clicks of each run and by other work on the box; runs that went over load 16 were dropped, and in those the text film's run medians were 237 to 516 ms before and 87 to 182 ms after.
Trace of 10 clicks on the text film (per click):
CPU profile of the same 24 clicks and 8 drag starts (total ms over the run, source-mapped; the box was busier during the before run, so compare ratios):
getPreviewTargetFromPointer(studioPreviewHelpers.ts)elementsFromPoint(native, self)hasAuthorPointerEventsNone(studioPreviewHelpers.ts, self)recomputeOffCanvasIndicators(offCanvasIndicatorGeometry.ts)What is left on a click is mostly the network round trip for the source probe and React rendering the inspector; that is a separate change.
Before
v0.8.86: 10 clicks on the text film, the latency of each shown on the page (median 262 ms in this recording).
studio-select-before.mp4
After
This branch: the same clicks (median 120 ms in this recording).
studio-select-after.mp4