Safaris JavaScript-Engine hat einen versteckten Fehler: Wenn das Entry-Script eines Web Workers im Modul-Stil an einer anderen Stelle im Bundle importiert wird, führt Safari dieses Entry-Script ein zweites Mal aus. Dieser doppelte Durchlauf zerstört den Singleton-Zustand und sabotiert stillschweigend Worker, die auf gemeinsam genutzten Caches oder Single-Instance-Objekten basieren.
Das Problem trat bei der Entwicklung einer browserbasierten Videoverarbeitungs-App auf, die Web Workers zur Dekodierung von ProRes-Dateien nutzt. Chrome und Firefox verarbeiteten den Code ohne Probleme, aber Safari schlug beim Laden des Videos konsequent fehl. Die Konsole meldete lediglich einen generischen „cannot read video“-Fehler, während der eigentliche Übeltäter der zweimal ausgeführte Initialisierungscode des Workers war, der zwei unabhängige Kopien desselben Moduls im Speicher hinterließ.
Wie sich der Bug manifestiert
Moderne Bundler (Vite, Rollup usw.) ziehen häufig gemeinsam genutzte Utilities in die Entry-Datei des Workers, damit lazy-geladene Chunks diesen Code wieder vom Einstiegspunkt aus importieren können. In Browsern, die dem Standardverhalten des Modul-Loaders folgen, gibt der Loader nach der Instanziierung des Entry-Moduls dasselbe Modul-Objekt an jeden nachfolgenden Import zurück, was eine zweite Ausführung verhindert.
Safari weicht von dieser Erwartung ab. Wenn ein lazy-geladener Chunk die Entry-Datei des Workers importiert, behandelt Safari diesen Import als eine neue Modulanforderung und führt das Entry-Script erneut aus. Das Ergebnis sind zwei separate Instanzen jeder dort definierten Variable, Klasse oder jedes Singletons.
Was kaputt geht, wenn das Entry zweimal läuft
- Singletons und Caches teilen keine Daten mehr; eine Kopie sieht einen leeren Cache, während die andere ihn befüllt.
- Registries (zum Beispiel eine Liste von Message-Handlern) werden auf die beiden Instanzen aufgeteilt, wodurch eine Seite effektiv leer bleibt.
- Event-Listener werden doppelt angehängt, was potenziell zu doppelter Verarbeitung oder Speicheraufblähung führt.
- WebAssembly (WASM)-Module werden zweimal geladen, was Bandbreite und Initialisierungszeit verschwendet.
- Der Fehler tritt lautlos auf: Es wird keine nicht abgefangene Exception geworfen; nur die nachgelagerte Logik, die vom fehlenden Zustand abhängt, verhält sich fehlerhaft.
Das Problem im Projekt erkennen
Ein schnelles Grep über die gebauten Assets kann aufzeigen, ob ein Chunk die Entry-Datei des Workers importiert:
grep -l 'from"./your.worker-' dist/assets/*.js
Wenn der Befehl Dateien auflistet, lösen diese Importe höchstwahrscheinlich den Double-Run-Bug in Safari aus.
Praktische Workarounds
Gemeinsamen Code aus dem Worker-Entry extrahieren.
Konfigurieren Sie den Bundler so, dass gemeinsame Bibliotheken in einen eigenen Chunk ausgelagert werden (z. B. mit RollupsmanualChunks). Sowohl der Worker als auch alle lazy-geladenen Module importieren die Bibliothek dann aus dieser dritten Datei, wodurch der Import des Worker-Entry-Points überflüssig wird.Verwendung einer schlanken Entry-Datei.
Reduzieren Sie das Entry-Script des Workers auf eine einzige Zeile, die die eigentliche Implementierung re-exportiert:// worker-entry.js import("./main.js");Solange kein anderes Bundle
worker-entry.jsimportiert, sieht Safari nie eine zweite Importanforderung, sodass das Entry nur einmal ausgeführt wird.
Beide Ansätze stellen sicher, dass der Initialisierungscode des Workers über die gesamte Anwendung hinweg als Singleton fungiert.
Warum dieser Bug wichtig ist
Web Workers sind ein gängiges Muster, um rechenintensive Aufgaben – Videokodierung, Bildverarbeitung, Kryptografie – vom Main-Thread auszulagern. Eine stille Aufspaltung des Zustands kann ein perfekt funktionierendes Feature in einen sporadischen Fehler verwandeln, der nur in Safari auftritt – dem Standardbrowser für einen großen Anteil an Desktop- und Mobilgeräten. Da der Fehler als generisches Medienlade-Problem erscheint, verbringen Entwickler möglicherweise Stunden damit, dem falschen Symptom nachzujagen.
Der Bug verdeutlicht auch ein breiteres Risiko: sich auf eine Semantik des Modul-Loaders zu verlassen, die nicht einheitlich über alle Browser hinweg implementiert ist. Wenn die Optimierungsstrategie eines Bundlers von einer einzigen, gemeinsam genutzten Modulinstanz ausgeht, kann jede Abweichung diese Annahme zerstören.
Gegenargumente und offene Fragen
Das Verhalten von Safari entspricht seinen eigenen Regeln zur Modulauflösung, die sich in Grenzfällen mit Workern subtil vom Standard unterscheiden. Einige Entwickler argumentieren, dass Bundler es ganz vermeiden sollten, gemeinsamen Code in die Entry-Datei eines Workers zu legen, wodurch das Problem eher eine Frage der Disziplin zur Build-Zeit als ein Browser-Defekt wäre. Andere weisen darauf hin, dass die Abweichung von Safari nicht dokumentiert ist, was Entwickler ohne eine zuverlässige Möglichkeit zur Vorhersage zurücklässt.
Apple hat das Problem bisher nicht öffentlich bestätigt, und es gibt keinen bekannten Zeitplan für eine Behebung. Bis Safari seinen Loader ändert, liegt die Verantwortung bei den Entwicklern, ihre Bundles umzustrukturieren oder eine Erkennungslogik in ihre CI-Pipelines einzubauen.
Worauf man als Nächstes achten sollte
- Browser-Updates – Achten Sie in den Safari-Release-Notes auf Hinweise zur Handhabung von Module-Workern.
- Community-Patches für Bundler – Vite, Rollup und andere könnten Warnungen oder automatische Chunking-Strategien einführen, um das Muster zu vermeiden, das den Bug auslöst.
- Testpraktiken – Die Einbindung von realen Mediendateien und Full-Stack-Worker-Tests auf Safari vor dem Release kann das stille Versagen frühzeitig erkennen.
Fazit
Wenn Ihre Safari-Nutzer unerklärliche Fehler im Zusammenhang mit Workern erleben, prüfen Sie, ob ein Nicht-Worker-Bundle das Entry-Script des Workers importiert. Der Bug durch die doppelte Ausführung zerstört lautlos den Singleton-Zustand, aber das Auslagern von gemeinsam genutztem Code aus dem Entry-Point oder das Reduzieren des Entry-Points auf einen schlanken Re-Export stellt das korrekte Verhalten wieder her, ohne auf einen Browser-Fix warten zu müssen.
