Skip to content

fix(YouTube): destroy player on unmount to fix memory leak in v2 - #2048

Open
jv8-alt wants to merge 1 commit into
cookpete:v2from
jv8-alt:fix/youtube-destroy-on-unmount
Open

fix(YouTube): destroy player on unmount to fix memory leak in v2#2048
jv8-alt wants to merge 1 commit into
cookpete:v2from
jv8-alt:fix/youtube-destroy-on-unmount

Conversation

@jv8-alt

@jv8-alt jv8-alt commented Sep 3, 2026

Copy link
Copy Markdown

Adds the missing componentWillUnmount so player.destroy() runs when a YouTube player unmounts.

The bug

YouTube has no unmount hook. Player.js calls stop() on unmount, but that maps to stopVideo:

stop () {
  if (!document.body.contains(this.callPlayer('getIframe'))) return
  this.callPlayer('stopVideo')
}

stopVideo halts playback without deregistering the player, so every mount/unmount cycle leaks one — a detached <iframe> retained by window.YT, along with the player's event handlers and, through this.props, the surrounding component tree.

The API keys every player by element id and clears it only in destroy():

this.g.id=ja(this.g);T[this.g.id]=this;   // constructor: register
this.g&&this.g.id&&(T[this.g.id]=null);   // destroy(): the only place it clears

this.g is the iframe (n.getIframe=function(){return this.g}). The docs describe stopVideo() as stopping playback while keeping the player interface intact, and destroy() as the call that "Removes the <iframe> containing the player."

Found via heap snapshots on a production app: Detached HTMLIFrameElement nodes accumulating across route changes.

Previously reported, closed without a fix: #531 ("The YouTube player is not attached to the DOM" on unmount; asks for a destroy hook) and #377 (players not cleaned up when loaded in quick succession).

Full destroy(), verbatim from www-widgetapi.js (build ea6f527e)
n.destroy=function(){
  this.g&&this.g.id&&(T[this.g.id]=null);
  var a=this.i;a&&typeof a.dispose=="function"&&a.dispose();
  if(this.j){a=this.j;var b=this.g,c=b.parentNode;c&&c.replaceChild(a,b)}
  else(a=this.g)&&a.parentNode&&a.parentNode.removeChild(a);
  Z&&(Z[this.id]=null);
  this.options=null;
  this.g&&this.v&&this.g.removeEventListener("load",this.v);
  this.j=this.g=null
};

Z is a second registry, used by a global message listener to route postMessages to players. It is cleared in the same method and nowhere else.

The fix

componentWillUnmount () {
  if (this.player && typeof this.player.destroy === 'function') {
    try {
      this.player.destroy()
    } catch (error) {
      // destroy() throws if the iframe has already been removed
    }
  }
  this.player = null
}

Both guards matter: the component can unmount before getSDK() resolves, so this.player may never exist; and destroy() throws when the iframe is already gone — the exact error in #531, so the catch fixes that console error too. this.player = null drops the closure chain holding this.props.

Testing

Three cases in test/players/YouTube.js using the existing zora/sinon/react-test-renderer harness: destroy() called once and reference nulled; unmount before the player exists doesn't throw; a throwing destroy() doesn't break unmount.

Reverting src/players/YouTube.js alone makes them fail, so they aren't passing vacuously:

not ok 28 - destroy() is called on unmount
not ok 29 - the player reference is released

Full suite green and npm run lint clean on Node 18 (matching CI).

Note on v3

v3 has the same gap — it delegates to youtube-video-element, which defines no disconnectedCallback. Opened separately as muxinc/media-elements#256.

The YouTube iframe API keeps an internal reference to every player it
creates, so removing the container from the DOM does not release the
iframe. `stop()` only calls `stopVideo`, which halts playback but leaves
the player registered, so unmounting a YouTube player leaves a detached
HTMLIFrameElement retained by `window.YT` along with everything its
event handlers close over.

Add `componentWillUnmount` to call `player.destroy()`, which removes the
iframe and deregisters it from the API. The call is guarded and wrapped
in try/catch because `destroy()` throws when the iframe has already been
removed, and the player may not exist yet if the component unmounts
before the SDK resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JM3M1hmNuwrvKiEcvpHPWV
@jv8-alt jv8-alt changed the title fix(YouTube): destroy player on unmount to fix memory leak fix(YouTube): destroy player on unmount to fix memory leak in v2 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants