El motor JavaScript de Safari tiene un fallo oculto: cuando el script de entrada de un Web Worker de estilo módulo se importa en cualquier otro lugar del bundle, Safari ejecuta ese script de entrada una segunda vez. La ejecución duplicada rompe el estado de los singletons, saboteando silenciosamente a los workers que dependen de cachés compartidas u objetos de instancia única.

El problema surgió mientras desarrollaba una aplicación de procesamiento de vídeo basada en el navegador que utiliza Web Workers para decodificar archivos ProRes. Chrome y Firefox manejaron el código sin incidentes, pero Safari fallaba constantemente al cargar el vídeo. La consola solo informaba de un error genérico de “cannot read video”, mientras que el verdadero culpable era el código de inicialización del worker ejecutándose dos veces y dejando dos copias independientes del mismo módulo en memoria.

Cómo se manifiesta el error

Los bundlers modernos (Vite, Rollup, etc.) suelen extraer utilidades compartidas hacia el archivo de entrada del worker para que los chunks de carga diferida (lazy-loaded) puedan importar ese código de nuevo desde el punto de entrada. En los navegadores que siguen el comportamiento estándar del cargador de módulos, una vez que el módulo de entrada se instancia, el cargador devuelve el mismo objeto de módulo a cualquier importación posterior, evitando una segunda ejecución.

Safari se desvía de esa expectativa. Cuando un chunk de carga diferida importa el archivo de entrada del worker, Safari trata la importación como una nueva solicitud de módulo y vuelve a ejecutar el script de entrada. El resultado son dos instancias separadas de cada variable, clase o singleton definido allí.

Qué se rompe cuando la entrada se ejecuta dos veces

  • Los singletons y las cachés ya no comparten datos; una copia ve una caché vacía mientras la otra la puebla.
  • Los registros (por ejemplo, una lista de manejadores de mensajes) terminan divididos entre las dos instancias, dejando un lado efectivamente vacío.
  • Los event listeners se adjuntan dos veces, lo que puede causar un manejo duplicado o un aumento excesivo de la memoria.
  • Los módulos WebAssembly (WASM) se cargan dos veces, desperdiciando ancho de banda y tiempo de inicialización.
  • El fallo es silencioso: no se lanza ninguna excepción no capturada, solo la lógica descendente que depende del estado faltante se comporta de forma errática.

Cómo detectar el problema en un proyecto

Un rápido grep en los assets compilados puede revelar si algún chunk importa el archivo de entrada del worker:

grep -l 'from"./your.worker-' dist/assets/*.js

Si el comando enumera algún archivo, es probable que esas importaciones estén activando el error de ejecución doble en Safari.

Soluciones prácticas

  1. Extraer el código compartido de la entrada del worker.
    Configura el bundler para colocar las librerías comunes en su propio chunk (por ejemplo, usando manualChunks de Rollup). Tanto el worker como cualquier módulo de carga diferida importarán entonces la librería desde ese tercer archivo, eliminando la necesidad de importar el punto de entrada del worker.

  2. Usar un archivo de entrada ligero.
    Reduce el script de entrada del worker a una sola línea que re-exporte la implementación real:

    // worker-entry.js
    import("./main.js");
    

    Mientras ningún otro bundle importe worker-entry.js, Safari nunca verá una segunda solicitud de importación, por lo que la entrada se ejecutará solo una vez.

Ambos enfoques mantienen el código de inicialización del worker como un singleton en toda la aplicación.

Por qué es importante este error

Los Web Workers son un patrón común para descargar computaciones pesadas —codificación de vídeo, procesamiento de imágenes, criptografía— fuera del hilo principal. Una división silenciosa del estado puede convertir una funcionalidad perfectamente operativa en un fallo intermitente que solo aparece en Safari, el navegador predeterminado en una gran parte de los dispositivos de escritorio y móviles. Debido a que el error se manifiesta como un fallo genérico de carga de medios, los desarrolladores pueden pasar horas persiguiendo el síntoma equivocado.

El error también resalta un riesgo más amplio: confiar en semánticas de cargadores de módulos que no están implementadas uniformemente en todos los navegadores. Cuando la estrategia de optimización de un bundler asume una única instancia de módulo compartida, cualquier desviación puede romper esa suposición.

Contraargumentos y preguntas abiertas

El comportamiento de Safari se alinea con sus propias reglas de resolución de módulos, que difieren sutilmente de la especificación en casos límite que involucran workers. Algunos desarrolladores argumentan que los bundlers deberían evitar por completo colocar código compartido en el archivo de entrada de un worker, convirtiendo el problema en una cuestión de disciplina en el tiempo de construcción (build-time) en lugar de un defecto del navegador. Otros señalan que la desviación de Safari no está documentada, lo que deja a los desarrolladores sin una forma fiable de anticiparla.

Apple no ha reconocido públicamente el problema y no hay un cronograma conocido para una solución. Hasta que Safari cambie su cargador, la responsabilidad recae en los desarrolladores de reestructurar sus bundles o añadir lógica de detección a sus pipelines de CI.

Qué observar a continuación

  • Actualizaciones del navegador – Esté atento a las notas de lanzamiento de Safari para cualquier mención sobre el manejo de module-worker.
  • Parches de la comunidad de bundlers – Vite, Rollup y otros podrían introducir advertencias o estrategias de fragmentación (chunking) automática para evitar el patrón que desencadena el error.
  • Prácticas de prueba – Incorporar archivos multimedia del mundo real y pruebas de workers de stack completo en Safari antes del lanzamiento puede detectar el fallo silencioso de forma temprana.

Conclusión

Si sus usuarios de Safari experimentan fallos inexplicables relacionados con los workers, compruebe si algún bundle que no sea un worker importa el script de entrada del worker. El error de doble ejecución destruye silenciosamente el estado del singleton, pero mover el código compartido fuera del punto de entrada o reducir la entrada a una reexportación mínima restaura el comportamiento correcto sin tener que esperar a una corrección del navegador.