Silnik JavaScript w Safari posiada ukrytą wadę: gdy skrypt wejściowy Web Workera w stylu modułowym zostanie zaimportowany w dowolnym innym miejscu w paczce (bundle), Safari wykonuje ten skrypt po raz drugi. Powtórne uruchomienie niszczy stan singletona, po cichu sabotując workerów polegających na współdzielonych pamięciach podręcznych lub obiektach o pojedynczej instancji.

Problem ujawnił się podczas tworzenia przeglądarkowej aplikacji do przetwarzania wideo, która wykorzystuje Web Workery do dekodowania plików ProRes. Chrome i Firefox obsługiwały kod bez żadnych problemów, ale Safari konsekwentnie nie potrafiło załadować wideo. Konsola zgłaszała jedynie ogólny błąd „cannot read video”, podczas gdy prawdziwym winowajcą był kod inicjalizacyjny workera uruchamiany dwukrotnie, co pozostawiało w pamięci dwie niezależne kopie tego samego modułu.

Jak objawia się ten błąd

Nowoczesne bundlery (Vite, Rollup itp.) często przenoszą wspólne narzędzia (utilities) do pliku wejściowego workera, aby leniwie ładowane chunki mogły zaimportować ten kod z powrotem z punktu wejściowego. W przeglądarkach przestrzegających standardowego zachowania ładowarki modułów, po zainstancjonowaniu modułu wejściowego, ładowarka zwraca ten sam obiekt modułu do każdego kolejnego importu, zapobiegając ponownemu wykonaniu.

Safari odbiega od tych oczekiwań. Gdy leniwie ładowany chunk importuje plik wejściowy workera, Safari traktuje ten import jako nowe żądanie modułu i ponownie wykonuje skrypt wejściowy. Rezultatem są dwie oddzielne instancje każdej zmiennej, klasy lub singletona zdefiniowanego w tym miejscu.

Co ulega awarii, gdy skrypt wejściowy uruchamia się dwukrotnie

  • Singletony i pamięci podręczne nie współdzielą już danych; jedna kopia widzi pustą pamięć podręczną, podczas gdy druga ją wypełnia.
  • Rejestry (na przykład lista handlerów wiadomości) zostają rozdzielone między dwie instancje, co sprawia, że jedna strona pozostaje w praktyce pusta.
  • Listenerzy zdarzeń są podpinani dwukrotnie, co może powodować duplikowanie obsługi lub nadmierne zużycie pamięci.
  • Moduły WebAssembly (WASM) ładują się dwukrotnie, marnując przepustowość i czas inicjalizacji.
  • Awaria jest cicha: nie jest rzucany żaden nieobsłużony wyjątek, jedynie logika zależna od brakującego stanu zaczyna działać nieprawidłowo.

Wykrywanie problemu w projekcie

Szybkie użycie polecenia grep na zbudowanych zasobach może ujawnić, czy jakikolwiek chunk importuje plik wejściowy workera:

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

Jeśli polecenie wypisze jakiekolwiek pliki, te importy prawdopodobnie wywołują błąd podwójnego uruchomienia w Safari.

Praktyczne obejścia

  1. Wyodrębnij wspólny kod ze skryptu wejściowego workera.
    Skonfiguruj bundler tak, aby umieścił wspólne biblioteki w osobnym chunku (np. używając manualChunks w Rollupie). Zarówno worker, jak i wszelkie leniwie ładowane moduły importują wtedy bibliotekę z tego trzeciego pliku, co eliminuje potrzebę importowania punktu wejściowego workera.

  2. Użyj lekkiego pliku wejściowego.
    Zredukuj skrypt wejściowy workera do jednej linii, która re-eksportuje rzeczywistą implementację:

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

    Dopóki żaden inny bundle nie zaimportuje worker-entry.js, Safari nigdy nie zobaczy drugiego żądania importu, więc skrypt wejściowy uruchomi się tylko raz.

Oba podejścia sprawiają, że kod inicjalizacyjny workera zachowuje charakter singletona w całej aplikacji.

Dlaczego ten błąd jest istotny

Web Workery to powszechny wzorzec odciążania głównego wątku od ciężkich obliczeń — takich jak kodowanie wideo, przetwarzanie obrazu czy kryptografia. Ciche rozdzielenie stanu może zmienić w pełni funkcjonalną funkcję w sporadyczną awarię, która pojawia się tylko w Safari — domyślnej przeglądarce na dużej części urządzeń stacjonarnych i mobilnych. Ponieważ błąd objawia się jako ogólny błąd ładowania mediów, programiści mogą spędzać godziny na tropieniu niewłaściwych objawów.

Błąd ten uwypukla również szersze ryzyko: poleganie na semantyce ładowarki modułów, która nie jest jednolicie implementowana we wszystkich przeglądarkach. Gdy strategia optymalizacji bundlera zakłada istnienie pojedynczej, współdzielonej instancji modułu, każde odstępstwo może zburzyć to założenie.

Kontrargumenty i otwarte pytania

Zachowanie Safari jest zgodne z jego własnymi regułami rozwiązywania modułów (module resolution), które w przypadkach brzegowych dotyczących workerów subtelnie różnią się od specyfikacji. Niektórzy programiści argumentują, że bundlery powinny w ogóle unikać umieszczania wspólnego kodu w pliku wejściowym workera, co czyniłoby ten problem kwestią dyscypliny na etapie budowania, a nie błędem przeglądarki. Inni zauważają, że odstępstwo Safari nie jest udokumentowane, co pozostawia programistów bez niezawodnego sposobu na jego przewidzenie.

Apple nie przyznało publicznie, że problem istnieje, i nie podano żadnych terminów naprawy. Dopóki Safari nie zmieni swojej ładowarki, ciężar restrukturyzacji paczek lub dodania logiki wykrywania do potoków CI spoczywa na programistach.

Co obserwować dalej

  • Aktualizacje przeglądarek – Śledź notatki wydawnicze Safari pod kątem wszelkich wzmianek o obsłudze module-worker.
  • Łatki społeczności bundlerów – Vite, Rollup i inne narzędzia mogą wprowadzić ostrzeżenia lub strategie automatycznego dzielenia na paczki (chunking), aby uniknąć wzorca wywołującego ten błąd.
  • Praktyki testowe – Uwzględnienie rzeczywistych plików multimedialnych oraz pełnych testów workerów w Safari przed wydaniem wersji może pozwolić na wczesne wykrycie cichej awarii.

Podsumowanie

Jeśli użytkownicy Safari doświadczają niewytłumaczalnych błędów związanych z workerami, sprawdź, czy jakakolwiek paczka (bundle) niebędąca workerem nie importuje skryptu wejściowego (entry script) workera. Błąd podwójnego wykonania po cichu niszczy stan singletona, ale przeniesienie współdzielonego kodu poza punkt wejściowy lub ograniczenie punktu wejściowego do prostego re-exportu przywraca poprawne działanie bez konieczności czekania na poprawkę w przeglądarce.