В JavaScript-движке Safari есть скрытый изъян: если входной скрипт модуля Web Worker импортируется где-либо еще в бандле, Safari выполняет этот скрипт второй раз. Дублирующийся запуск нарушает состояние синглтонов, незаметно саботируя воркеров, которые полагаются на общие кэши или объекты-одиночки.

Проблема проявилась при разработке браузерного приложения для обработки видео, использующего Web Workers для декодирования файлов ProRes. Chrome и Firefox справлялись с кодом без происшествий, но Safari постоянно не мог загрузить видео. Консоль выдавала лишь общую ошибку «cannot read video», в то время как реальной причиной был двойной запуск кода инициализации воркера, что приводило к появлению двух независимых копий одного и того же модуля в памяти.

Как проявляется баг

Современные бандлеры (Vite, Rollup и т. д.) часто выносят общие утилиты во входной файл воркера, чтобы лениво загружаемые чанки могли импортировать этот код обратно из точки входа. В браузерах, следующих стандартному поведению загрузчика модулей, после создания экземпляра входного модуля загрузчик возвращает тот же самый объект модуля при любом последующем импорте, предотвращая повторное выполнение.

Safari отклоняется от этих ожиданий. Когда лениво загружаемый чанк импортирует входной файл воркера, Safari воспринимает этот импорт как новый запрос модуля и повторно выполняет входной скрипт. В результате создаются два отдельных экземпляра каждой переменной, класса или синглтона, определенных в нем.

Что ломается при двойном запуске входного скрипта

  • Синглтоны и кэши больше не делят данные; одна копия видит пустой кэш, в то время как другая его наполняет.
  • Реестры (например, список обработчиков сообщений) разделяются между двумя экземплярами, из-за чего одна сторона оказывается фактически пустой.
  • Обработчики событий (event listeners) прикрепляются дважды, что может привести к дублированию обработки или раздуванию памяти.
  • Модули WebAssembly (WASM) загружаются дважды, тратя пропускную способность и время инициализации.
  • Ошибка происходит незаметно: необработанные исключения не выбрасываются, ломается только последующая логика, зависящая от отсутствующего состояния.

Как обнаружить проблему в проекте

Быстрый поиск через grep по собранным ассетам может показать, импортирует ли какой-либо чанк входной файл воркера:

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

Если команда выводит какие-либо файлы, эти импорты, скорее всего, вызывают баг с двойным запуском в Safari.

Практические способы решения

  1. Вынесите общий код из входного файла воркера.
    Настройте бандлер так, чтобы он помещал общие библиотеки в отдельный чанк (например, используя manualChunks в Rollup). Тогда и воркер, и любые лениво загружаемые модули будут импортировать библиотеку из этого третьего файла, что избавит от необходимости импортировать входную точку воркера.

  2. Используйте «тонкий» входной файл.
    Сократите входной скрипт воркера до одной строки, которая повторно экспортирует реальную реализацию:

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

    Пока никакой другой бандл не импортирует worker-entry.js, Safari не увидит второго запроса на импорт, и входной скрипт выполнится только один раз.

Оба подхода гарантируют, что код инициализации воркера останется синглтоном во всем приложении.

Почему этот баг важен

Web Workers — это распространенный паттерн для выноса тяжелых вычислений (кодирование видео, обработка изображений, криптография) из основного потока. Неявное разделение состояния может превратить идеально работающую функцию в периодическую ошибку, которая проявляется только в Safari — браузере по умолчанию для огромной доли десктопных и мобильных устройств. Поскольку ошибка выглядит как общая ошибка загрузки медиафайла, разработчики могут часами искать неверный симптом.

Этот баг также подсвечивает более широкий риск: зависимость от семантики загрузчика модулей, которая реализована не во всех браузерах одинаково. Когда стратегия оптимизации бандлера предполагает наличие одного общего экземпляра модуля, любое отклонение может нарушить это предположение.

Контраргументы и открытые вопросы

Поведение Safari соответствует его собственным правилам разрешения модулей, которые в пограничных случаях с воркерами слегка отличаются от спецификации. Некоторые разработчики утверждают, что бандлерам следует вообще избегать размещения общего кода во входном файле воркера, превращая проблему в вопрос дисциплины сборки, а не дефект браузера. Другие указывают на то, что отклонение Safari не задокументировано, что лишает разработчиков надежного способа его предвидеть.

Apple публично не признала проблему, и сроки исправления неизвестны. Пока Safari не изменит свой загрузчик, ответственность за реструктуризацию бандлов или добавление логики обнаружения в CI-пайплайны лежит на разработчиках.

За чем следить дальше

  • Обновления браузеров — следите за примечаниями к релизам Safari на предмет упоминаний обработки module-worker.
  • Патчи сообщества сборщиков — Vite, Rollup и другие инструменты могут внедрить предупреждения или стратегии автоматического разделения на чанки, чтобы избежать паттерна, вызывающего этот баг.
  • Практики тестирования — использование реальных медиафайлов и проведение полностековых тестов воркеров в Safari перед релизом поможет на ранней стадии обнаружить скрытые сбои.

Итог

Если ваши пользователи Safari сталкиваются с необъяснимыми сбоями, связанными с воркерами, проверьте, не импортирует ли какой-либо бандл, не являющийся воркером, входной скрипт воркера. Баг с двойным выполнением незаметно разрушает состояние синглтонов, но вынос общего кода из точки входа или превращение точки входа в простой реэкспорт восстанавливает корректное поведение, не дожидаясь исправления со стороны браузера.