I am building a custom video player overlay using Vanilla JavaScript and hls.js for dynamic media streaming.
The video initialization and playback work smoothly, but I am running into a memory accumulation issue when users switch between different video streams on the same page. Even after calling .destroy() on the HLS instance, memory profiler snapshots in Chrome DevTools show retained GL texture buffers and media source buffers in the heap.
Here is my current cleanup logic:
JavaScript
function cleanupPlayer(hlsInstance, videoElement) {
if (hlsInstance) {
hlsInstance.stopLoad();
hlsInstance.detachMedia();
hlsInstance.destroy();
hlsInstance = null;
}
if (videoElement) {
videoElement.pause();
videoElement.removeAttribute('src');
videoElement.load();
}
}
The Problem: Despite removing src attributes and calling .destroy(), the MediaSource object remains referenced in memory, leading to tab crashes on low-end mobile browsers after 5–6 stream switches.
Is there a recommended way to explicitly flush SourceBuffer objects before detaching media, or force garbage collection on media element contexts in modern browsers?
Any insights or code examples from developers working with Web APIs and media players would be greatly appreciated!
This is a classic memory leak issue with Chromium-based browsers when swapping hls.js instances on single-page media applications. Simply calling destroy() and resetting the src attribute often leaves references hanging in event listeners and internal SourceBuffer array references inside the engine.
To ensure proper garbage collection, you need to explicitly flush the buffer ranges before detaching, break all bound event references, and forcefully drop the media source reference.
Here is an optimized cleanup routine that handles explicit buffer clearing and node garbage collection:
function cleanupPlayer(hlsInstance, videoElement) {
if (hlsInstance) {
// 1. Stop fetching new segments
hlsInstance.stopLoad();
// 2. Explicitly clear media buffers before detaching
if (hlsInstance.media && hlsInstance.media.sourceBuffers) {
try {
const sb = hlsInstance.media.sourceBuffers;
for (let i = 0; i < sb.length; i++) {
// Evict all buffered time ranges
if (!sb[i].updating) {
sb[i].remove(0, Infinity);
}
}
} catch (e) {
console.warn('SourceBuffer cleanup error:', e);
}
}
// 3. Detach and destroy HLS instance
hlsInstance.detachMedia();
hlsInstance.destroy();
hlsInstance = null;
}
if (videoElement) {
// 4. Pause and clear active playback
videoElement.pause();
// 5. Force MediaSource detachment via empty object URL clearing
videoElement.src = '';
videoElement.removeAttribute('src');
// 6. Reset underlying HTMLMediaElement pipeline
videoElement.load();
// 7. Clone and replace element to drop hidden event listener references
const cleanVideo = videoElement.cloneNode(false);
if (videoElement.parentNode) {
videoElement.parentNode.replaceChild(cleanVideo, videoElement);
}
}
}
i would avoid trying to force GC here, since browsers decide when MediaSource and related media resources are actually released. Your cleanup sequence is mostly correct, and hls.js already removes its sourceBuffers during detachMedia()
one thing I’d check is whether any application-level event listeners, timers promises or references are still holding the old video/HLS instance. I’d also let any pending SourceBuffer operation finish before destroying the player rather than trying to manually flush it In Chrome DevTools, compare heap snapshots after several switches and look specifically for retained references from your own player code.
also, hls.destroy() already calls detachmedia() so calling both is usually redundant. A simpler cleanup path is often enough
if the retained memory is still growing after this, I’d suspect an application side reference or a browser/media pipeline issue rather than a missing sourcebuffer.remove() call