fix(youtube-video-element): destroy the player on disconnect to fix memory leak - #256
Open
jv8-alt wants to merge 1 commit into
Open
fix(youtube-video-element): destroy the player on disconnect to fix memory leak#256jv8-alt wants to merge 1 commit into
jv8-alt wants to merge 1 commit into
Conversation
|
@staypuft3 is attempting to deploy a commit to the Mux Team on Vercel. A member of the Team first needs to authorize it. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
jv8-alt
force-pushed
the
fix/youtube-destroy-player-on-disconnect
branch
from
September 3, 2026 02:34
1c30859 to
d50f376
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.
Reviewed by Cursor Bugbot for commit d50f376. Configure here.
The YouTube iframe API keeps every player it creates in module-scoped registries, so removing the element is not enough to release the <iframe>: it stays detached but reachable from `window.YT`, along with everything the player's event handlers close over. The element defines no `disconnectedCallback`, and `api.destroy()` is otherwise only reachable from `load()` when `src` is cleared, so every mount/unmount cycle leaks a player. - Add `connectedCallback`/`disconnectedCallback` following the pattern already used in vimeo-video-element: destroy the player and reset the load state on disconnect, reload on reconnect so a DOM move still works (`load()` is otherwise only triggered by attributeChangedCallback, which does not fire on a move). - Always release the previous player in `load()`. A new one is constructed unconditionally further down, so `oldApi` is never reused, but it was only destroyed when `src` was empty. Every `src` change therefore orphaned a player in the registries holding its discarded iframe. Every attribute that triggers load() is part of the iframe URL, so the iframe was already being rebuilt in these cases; this only releases the player that was being dropped. - Guard the load with a `#loadId` generation counter, bumped on disconnect and on each load, so an attempt superseded while awaiting the API script or `loadComplete` bails out at both points. - Track and clear the two polling intervals started at the end of `load()`. Neither timer id was stored, so the 50ms seek poller had no cancel path at all and the 100ms progress poller only cancelled itself once fully buffered. Both closures capture `this`, keeping the element and its player alive independently of the registries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jv8-alt
force-pushed
the
fix/youtube-destroy-player-on-disconnect
branch
from
September 3, 2026 03:06
d50f376 to
3992fa2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Destroys the YouTube player when the element leaves the DOM, and cleans up the pollers
load()leaves running.The bug
youtube-video-elementdefines nodisconnectedCallback, and its onlyapi.destroy()is unreachable except whensrcis cleared. So every mount/unmount cycle leaks a player.Two independent things retain the detached
<iframe>:The API's registries. It keys every player by element id and never releases it without
destroy():this.gis the iframe (n.getIframe=function(){return this.g}).The two
setIntervalpollers at the end ofload(). Neither id is stored; the 50ms seek poller has no cancel path at all, and the 100ms progress poller only self-cancels once fully buffered. Both closures capturethis, so the timer queue keeps the element — and through it the player and the iframe — alive on its own.Found via heap snapshots on a production React app (through
react-playerv3):Detached HTMLIFrameElementnodes accumulating across route changes.vimeo-video-elementin this repo already handles (1).youtube-video-elementnever got the same treatment.Full
destroy()and the second registry, verbatim fromwww-widgetapi.js(buildea6f527e)A global
messagelistener routes postMessages through a second registry,Z, cleared in the same method:The changes
1.
connectedCallback/disconnectedCallback— mirroringvimeo-video-element. Destroy the player and reset load state on disconnect; reload on reconnect, becauseload()only runs fromattributeChangedCallback, which doesn't fire on a DOM move.2. A
#loadIdgeneration counter.load()awaits the API script and thenloadComplete. An element disconnected during either window would still attach a player, or start pollers for one, afterwards. Bumping#loadIdon disconnect and on each load makes the superseded attempt bail at both points.3.
#timers+#clearTimers(). Tracks both intervals, clears them on disconnect and before starting new ones so a reload can't stack them.Change 3 is pre-existing on
mainrather than introduced here — happy to split it out if you'd prefer, though reload-on-reconnect is what turns it from a leak into a stacking leak, so they're awkward to review apart.Testing
Three cases added to
test/test.jsin the existing zora/fixture()style: destroy-on-disconnect, new-player-on-reconnect, disconnect-before-ready.I could not run
wet test— it needs network access to YouTube, which my sandbox blocks. Worth confirming in CI.I verified the logic offline with jsdom instead: the real element module,
window.YTstubbed with a fakePlayer(loadScript()short-circuits when the global exists, so nothing hits the network), asserting on player construction,destroy()calls, and live interval counts.maintodayHarness output — 15/15 with the fix, 4/15 without
With the source change reverted, 11 of these fail, including
destroy() called exactly once on disconnect (got 0)andevery player created was destroyed (2 created, 2 leaked).npx eslint youtube-video-element.jsis clean.Worth a maintainer's eye
vimeo-video-elementlikely has the same in-flight race — it nulls#loadRequestedon disconnect, but its pendingload()still proceeds to attach a player. Left alone to keep this PR to one package.disconnectedCallback:wistia,twitch,spotify,tiktok,jwplayer,cloudflare. I haven't checked whether each SDK leaks the same way — flagging the pattern, not claiming the bug.Note
Medium Risk
Touches core
load()and custom-element lifecycle; behavior changes on unmount/remount and during async load, but scope is limited to youtube-video-element with targeted tests.Overview
Fixes a memory leak where removing
<youtube-video>from the DOM left YouTube iframe players and polling timers alive.Lifecycle: Adds
disconnectedCallbackto callapi.destroy(), clear seek/progresssetIntervalhandlers, reset load state, and bump a#loadIdso in-flightload()work stops before attaching another player. AddsconnectedCallbackto callload()again after a DOM move (no attribute change). Eachload()now always destroys the previous player before creating a new one, not only whensrcis cleared.Race safety: After awaiting the iframe API script or
loadComplete,load()exits if#loadIdchanged (disconnect or superseding load). Interval IDs are stored in#timersand cleared on disconnect and before starting new pollers.Tests: Three zora tests cover destroy on disconnect, fresh player on reconnect, and disconnect mid-load without throwing.
Reviewed by Cursor Bugbot for commit 3992fa2. Bugbot is set up for automated code reviews on this repo. Configure here.