Summary
VideoFrameThumbnail's error handler calls cleanup(), which sets video.src = "" and calls video.load(). Assigning an empty/invalid src itself fires a new error event on the element, which re-enters the same handler — an event-loop-speed infinite loop. Every pass allocates a React update object via setFailed(true) (dispatchSetState).
The same loop also starts on the success path: the seeked handler calls cleanup() after extracting the frame, and the resulting synthetic error re-enters the handler.
Every VideoFrameThumbnail ends up in this loop, whether the video loads or not.
Affected versions
Verified in the shipped studio bundles of 0.8.40 and 0.8.45 (current latest) — assets/index-*.js still contains const c=()=>{a.src="",a.load()} with a.addEventListener("error",()=>{s(!0),c()}).
Source: packages/studio/src/components/ui/VideoFrameThumbnail.tsx
Minimal repro
<script>
const video = document.createElement("video");
video.addEventListener("error", () => { video.src = ""; video.load(); }); // ← re-enters on its own synthetic error
video.src = "/missing.mp4";
video.load();
</script>
Open DevTools → Performance: the error handler spins forever. Instrumenting the src setter counted ~475,000 assignments/second in a real session.
Measured impact (real project, embedded studio)
- Allocation sampling: ~8.6 MB/s of React update objects (
{lane, revertLane, gesture, action, hasEagerState, eagerState, next}) allocated inside dispatchSetState, called from this component's setFailed(true).
- A heap snapshot after ~40 minutes showed 24.6M update objects / 983 MB (93% of heap) and a renderer pinned at ~100% CPU; in our embedded (Electron) use the JS heap grew to 2.8 GB at ~7 MB/s until the page died.
Suggested fix
Remove the listener before clearing src, and ignore the synthetic error cleanup() itself provokes:
const onError = () => {
if (!video.getAttribute("src")) return; // synthetic error from cleanup below
setFailed(true);
cleanup();
};
const cleanup = () => {
video.removeEventListener("error", onError);
video.src = "";
video.load();
};
// ... video.addEventListener("error", onError); instead of the inline arrow
Happy to open a PR if helpful.
Summary
VideoFrameThumbnail's error handler callscleanup(), which setsvideo.src = ""and callsvideo.load(). Assigning an empty/invalidsrcitself fires a newerrorevent on the element, which re-enters the same handler — an event-loop-speed infinite loop. Every pass allocates a React update object viasetFailed(true)(dispatchSetState).The same loop also starts on the success path: the
seekedhandler callscleanup()after extracting the frame, and the resulting syntheticerrorre-enters the handler.Every
VideoFrameThumbnailends up in this loop, whether the video loads or not.Affected versions
Verified in the shipped studio bundles of 0.8.40 and 0.8.45 (current latest) —
assets/index-*.jsstill containsconst c=()=>{a.src="",a.load()}witha.addEventListener("error",()=>{s(!0),c()}).Source:
packages/studio/src/components/ui/VideoFrameThumbnail.tsxMinimal repro
Open DevTools → Performance: the
errorhandler spins forever. Instrumenting thesrcsetter counted ~475,000 assignments/second in a real session.Measured impact (real project, embedded studio)
{lane, revertLane, gesture, action, hasEagerState, eagerState, next}) allocated insidedispatchSetState, called from this component'ssetFailed(true).Suggested fix
Remove the listener before clearing
src, and ignore the synthetic errorcleanup()itself provokes:Happy to open a PR if helpful.