hls.js no usa worker en la mayoría de setups ESM: cómo comprobarlo
La build ESM de hls.js no incrusta el transmuxer: va en un archivo aparte y, sin workerPath, todo el trabajo cae en el hilo principal. enableWorker sigue en true y no significa nada.

Si importas Hls desde 'hls.js' en un entorno ESM, lo más probable es que el transmuxer esté corriendo en el hilo principal y no en un worker. No es un fallo de la librería: la build ESM, disponible desde la versión 1.4, envía el worker como archivo separado en vez de incrustarlo. Hasta que no le pases una ruta con workerPath, ese archivo no se descarga nunca.
Cómo saber en qué modo estás
No hay que adivinar. Durante la reproducción, en la consola del navegador:
performance.getEntriesByType('resource')
.filter(e => e.name.includes('worker'))
.map(e => e.name);
Array vacío significa que el archivo del worker no se pidió jamás. En DevTools, Sources → Threads en Chrome o la lista de workers del depurador en Firefox: si solo aparece el hilo principal, hls.js está haciendo el transmuxing en línea.
La trampa gorda está en hls.config.enableWorker. Vale true por defecto y sigue valiendo true aunque no se haya podido crear ningún worker. Significa "usa un worker si hay uno disponible", no "hay un worker corriendo". Es la principal fuente de falsa confianza con esta librería.
Poner workerPath
Hace falta una URL real a hls.js/dist/hls.worker.js, y la sintaxis cambia según el bundler. En Vite o Rollup, import workerUrl from 'hls.js/dist/hls.worker.js?url'. En webpack 5, new URL('hls.js/dist/hls.worker.js', import.meta.url).toString(). En Next.js con app router y componente cliente, igual que en webpack pero dentro del useEffect que crea la instancia.
Una ruta mal puesta falla en silencio, así que conviene verificar tras el arranque:
hls.on(Hls.Events.MANIFEST_PARSED, () => {
const gotWorker = performance.getEntriesByType('resource')
.some(e => e.name.includes('hls.worker'));
console.log('[hls] worker active:', gotWorker);
});
Separar un atasco de red de uno de hilo
Un BUFFER_STALLED_ERROR dice que el buffer se quedó seco, no si los bytes llegaron tarde o si el hilo estaba ocupado. Un PerformanceObserver de tipo longtask reporta cada tarea que retuvo el hilo principal más de 50 ms. Guardando los últimos 10 segundos se puede calcular cuántos milisegundos estuvo bloqueado en los últimos 3 y adjuntarlo al evento de error. Así, blocked_ms_3s: 1180 con 0,2 s de buffer apunta al hilo; blocked_ms_3s: 0 con 0,1 s apunta a la red. Son tickets distintos, y en muchos equipos acaban en el mismo saco.
Ojo con la compatibilidad: longtask no existe en Safari. Hay que detectarlo con PerformanceObserver.supportedEntryTypes.includes('longtask') y, si se necesita cobertura cruzada, tirar de huecos entre llamadas a requestAnimationFrame.
Para validar que la instrumentación se mueve antes de fiarse de sus números, se puede meter un bucle que bloquee el hilo 120 ms cada 500 ms. Solo para reproducir, nunca en producción.
Chrome soporta MediaSource dentro de dedicated workers desde la 108, lo que abre la puerta a sacar más trabajo del hilo principal, pero eso es otra conversación. Todo lo anterior está comprobado contra hls.js 1.7.x, así que si estás en una versión anterior los detalles del bundler pueden cambiar.
