JavaScript-двигун Safari має приховану ваду: коли вхідний скрипт Web Worker у стилі модулів імпортується будь-де інде в бандлі, Safari виконує цей вхідний скрипт вдруге. Повторний запуск порушує стан синглтона, тихо саботуючи роботу воркерів, які покладаються на спільні кеші або об'єкти, що існують в одному екземплярі.

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

Як проявляється цей баг

Сучасні бандлери (Vite, Rollup тощо) часто витягують спільні утиліти у вхідний файл воркера, щоб чінки, що завантажуються ліниво (lazy-loaded), могли імпортувати цей код назад з точки входу. У браузерах, що дотримуються стандартної поведінки завантажувача модулів, після інстанціювання вхідного модуля завантажувач повертає той самий об'єкт модуля для будь-якого наступного імпорту, запобігаючи повторному виконанню.

Safari відхиляється від цих очікувань. Коли lazy-loaded чінк імпортує вхідний файл воркера, Safari сприймає цей імпорт як новий запит модуля і повторно виконує вхідний скрипт. У результаті створюються два окремі екземпляри кожної змінної, класу або синглтона, визначеного в ньому.

Що ламається при повторному запуску вхідного файлу

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

Як виявити проблему в проєкті

Швидкий пошук за допомогою grep у зібраних асетах може показати, чи імпортує якийсь чінк вхідний файл воркера:

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

Якщо команда виведе список файлів, ці імпорти, ймовірно, спричиняють баг із подвійним запуском у Safari.

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

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

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

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

    Доки жоден інший бандл не імпортує worker-entry.js, Safari не побачить другого запиту на імпорт, тому вхідний файл виконається лише один раз.

Обидва підходи гарантують, що код ініціалізації воркера залишатиметься унікальним (singleton) для всього додатка.

Чому цей баг важливий

Web Workers — це поширений патерн для перенесення важких обчислень (кодування відео, обробка зображень, криптографія) з основного потоку. Тихий розкол стану може перетворити цілком функціональну функцію на періодичну помилку, яка з'являється лише в Safari — браузері за замовчуванням на великій частці настільних і мобільних пристроїв. Оскільки помилка проявляється як загальна помилка завантаження медіафайлу, розробники можуть витратити години на пошук хибних симптомів.

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

Контраргументи та відкриті питання

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

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

За чим стежити далі

  • Оновлення браузерів – Слідкуйте за нотатками до випусків Safari на предмет будь-яких згадок про обробку module-worker.
  • Патчі спільноти бандлерів – Vite, Rollup та інші можуть запровадити попередження або стратегії автоматичного розділення на чанки (chunking), щоб уникнути патерну, який спричиняє цей баг.
  • Практики тестування – Використання реальних медіафайлів та проведення повностекових тестів воркерів у Safari перед релізом допоможе на ранньому етапі виявити приховану помилку.

Висновок

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