SafariのJavaScriptエンジンには隠れた欠陥があります。モジュール形式のWeb Workerのエントリ・スクリプトがバンドル内の他の場所でインポートされると、Safariはそのエントリ・スクリプトを2回実行してしまいます。この重複実行はシングルトンの状態を破壊し、共有キャッシュや単一インスタンスのオブジェクトに依存するWorkerを密かに機能不全に陥らせます。

この問題は、Web Workerを使用してProResファイルをデコードするブラウザベースのビデオ処理アプリを開発している際に表面化しました。ChromeやFirefoxでは問題なく動作していましたが、Safariではビデオの読み込みに一貫して失敗しました。コンソールには「cannot read video」という汎用的なエラーが表示されるだけでしたが、真の原因はWorkerの初期化コードが2回実行され、メモリ内に同じモジュールの独立したコピーが2つ作成されてしまったことでした。

バグの現れ方

現代的なバンドラー(Vite、Rollupなど)は、遅延読み込み(lazy-loaded)されるチャンクがエントリポイントからそのコードをインポートできるように、共有ユーティリティをWorkerのエントリファイルにまとめることがよくあります。標準的なモジュールローダーの動作に従うブラウザでは、一度エントリモジュールがインスタンス化されると、ローダーはそれ以降のインポートに対して同じモジュールオブジェクトを返すため、2回目の実行は防がれます。

Safariはこの期待とは異なります。遅延読み込みされるチャンクがWorkerのエントリファイルをインポートすると、Safariはそのインポートを新しいモジュールリクエストとして扱い、エントリ・スクリプトを再実行します。その結果、そこで定義されたすべての変数、クラス、またはシングルトンの別々のインスタンスが作成されてしまいます。

エントリが2回実行されると何が壊れるのか

  • シングルトンとキャッシュ: データが共有されなくなります。一方のコピーは空のキャッシュを参照し、もう一方がそれを埋めることになります。
  • レジストリ(例:メッセージハンドラーのリスト): 2つのインスタンスに分散してしまい、片方が実質的に空の状態になります。
  • イベントリスナー: 2回アタッチされるため、重複した処理が発生したり、メモリ使用量が増大したりする可能性があります。
  • WebAssembly (WASM) モジュール: 2回読み込まれるため、帯域幅と初期化時間が浪費されます。
  • 失敗がサイレントである: キャッチされない例外はスローされず、欠落した状態に依存する後続のロジックが誤作動するだけです。

プロジェクト内での問題の検出

ビルドされたアセットに対して簡単なgrepを実行することで、チャンクがWorkerのエントリファイルをインポートしているかどうかを確認できます。

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

コマンドの結果にファイルが表示された場合、それらのインポートがSafariにおける二重実行バグを引き起こしている可能性があります。

実用的な回避策

  1. Workerのエントリから共有コードを抽出する
    共通ライブラリを独自のチャンクに配置するようにバンドラーを設定します(例:Rollupの manualChunks を使用)。これにより、Workerと遅延読み込みされるモジュールの両方がその第3のファイルからライブラリをインポートするようになり、Workerのエントリポイントをインポートする必要がなくなります。

  2. 「薄い」エントリファイルを使用する
    Workerのエントリ・スクリプトを、実際のインプリメンテーションを再エクスポートするだけの1行に削減します。

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

    他のバンドルが worker-entry.js をインポートしない限り、Safariは2回目のインポートリクエストを検知しないため、エントリは一度しか実行されません。

どちらのアプローチも、アプリケーション全体でWorkerの初期化コードをシングルトンとして維持できます。

なぜこのバグが重要なのか

Web Workerは、ビデオエンコーディング、画像処理、暗号化などの重い計算をメインスレッドから切り離すための一般的なパターンです。サイレントな状態の分離は、完全に機能するはずの機能を、デスクトップやモバイルデバイスの大部分でデフォルトブラウザとして使用されているSafariでのみ発生する断続的な失敗へと変えてしまう可能性があります。エラーが汎用的なメディア読み込み失敗として表面化するため、開発者は誤った症状を追って何時間も費やしてしまうかもしれません。

また、このバグはより広範なリスクを浮き彫りにしています。それは、ブラウザ間で一様に実装されていないモジュールローダーのセマンティクスに依存することのリスクです。バンドラーの最適化戦略が単一の共有モジュールインスタンスを前提としている場合、その仕様からのいかなる逸脱も、その前提を崩す可能性があります。

反論と未解決の疑問

Safariの挙動は、Workerが関わるエッジケースにおいて仕様とは微妙に異なる、独自のモジュール解決ルールに基づいています。一部の開発者は、バンドラーはWorkerのエントリファイルに共有コードを配置することを避けるべきであり、この問題はブラウザの欠陥ではなくビルド時の規律の問題であると主張しています。一方で、Safariの逸脱は文書化されていないため、開発者がそれを予測する信頼できる方法がないという指摘もあります。

Appleはこの問題を公に認めておらず、修正の予定も知られていません。Safariのローダーが変更されるまでは、バンドルを再構成するか、CIパイプラインに検出ロジックを追加するかは、開発者の責任となります。

次に注目すべきこと

  • ブラウザのアップデート – Safariのリリースノートで、module-workerの取り扱いに関する記述がないか注意してください。
  • バンドラーのコミュニティパッチ – Vite、Rollupなどは、このバグを引き起こすパターンを回避するために、警告の導入や自動チャンク分割戦略を導入する可能性があります。
  • テストの実践 – リリース前に、実際のメディアファイルを使用したテストやSafari上でのフルスタックなworkerテストを取り入れることで、サイレントな失敗を早期に発見できます。

まとめ

Safariユーザーが原因不明のworker関連の失敗に遭遇している場合は、worker以外のバンドルがworkerのエントリースクリプトをインポートしていないか確認してください。二重実行のバグは、シングルトンの状態を密かに破壊してしまいますが、共有コードをエントリーポイントから切り離すか、エントリーポイントを薄い再エクスポート(re-export)に集約することで、ブラウザの修正を待たずに正しい動作を復元できます。