2024年1月9日

イベントループ(event loop): microtask と macrotask

ブラウザの JavaScript 実行フローは、Node.js 同様 event loop に基づいています。

event loop の動作を理解することは最適化のためには重要であり、適切なアーキテクチャにとっても重要である場合があります。

このチャプターでは、最初にそれがどのように動作するかについて理論的な詳細を説明し、次にその知識の実践的な使用例を見ていきます。

Event Loop

event loop のコンセプトは非常にシンプルです。無限ループで JavaScript エンジンはタスクを待機し、それらを実行し、また次のタスクを待機します。

エンジンの一般的なアルゴリズムは次の通りです:

  1. タスクがある間:
    • 最も古いタスクから開始し、それらを実行します。
  2. タスクが現れるまでスリープし、現れると 1. に進みます。

これは、ページを閲覧するときに見られることの形式化です。JavaScript エンジンはスクリプト/ハンドラ/イベントがアクティブになった場合にのみ実行され、ほとんどの時間何もしません。

タスクの例:

  • 外部スクリプト <script src="..."> が読み込まれるとき、“タスク” はそれを実行することです。
  • ユーザがマウスを動かすとき、“タスク” は mousemove イベントをディスパッチし、ハンドラを実行することです。
  • setTimeout でスケジュールされた期限がくるとき、“タスク” はそのコールバックを実行することです。
  • …等

タスクが設定され、エンジンがそれらを処理したあと、他のタスクを待機します(スリープ状態で CPU の消費はほぼゼロです)。

タスクはエンジンがビジーなときに来ることもあり、その時はキューに入れられます。

タスクはキュー、いわゆる “macrotask queue(マクロタスクキュー) (v8用語)” を形成します。

例えば、エンジンが script の実行でビジーである間にユーザがマウスを移動させて mousemove を引き起こしたり、setTimeout の実行予定が来たりすると、これらのタスクは上の図に示すようにキューを形成します。

キューのタスクは “先着順” で処理されます。エンジンが script を完了させると、mousemove イベントを処理し、次に setTimeout ハンドラを実行していきます。

ここまではとても簡単ですね。

あと2つ詳細です:

  1. エンジンがタスクを実行している間、レンダリングは発生しません。タスクが時間がかかるかどうかは関係ありません。DOM への変更はタスクが完了した後にのみ描画されます。
  2. タスクに時間がかかりすぎる場合、ブラウザは他のタスクの実行やユーザイベントの処理ができないため、しばらくすると “ページが応答していません” といった警告を表示し、ページ全体のタスクを強制終了するかどうかを訪ねます。これは複雑な計算が多数ある場合や、無限ループに陥るようなプログラムミスにより引き起こされます。

ここまでは理論でした。次からこの知識をどうのように適用できるか見ていきましょう。

ユースケース1: CPUを大量に消費するタスクの分割

大量にCPUを食うタスクがあるとしましょう。

例えば、シンタックスハイライト(このページのコード例を色付けするために使用しています)は、かなりCPU負荷がかかります。コードをハイライトするために分析を行い、多くの色付けされた要素を生成し、ドキュメントに追加します。テキスト量が多い場合には多くの時間が必要です。

エンジンがシンタックスハイライトをするのに忙しい間は、他のDOM関連の処理やユーザイベントの処理などを行うことはできません。また、ブラウザが少しの間 “一時停止” したり “ハング” する可能性もありますが、これは受け入れられません。

この問題に関しては、大きなタスクを細かく分割することで回避が可能です。最初に 100 行をハイライト処理し、次に setTimeout (遅延ゼロで)で次の100行を処理するようスケジュールしていきます。

このアプローチのデモについては、簡単にするためにシンタックスハイライトではなく、1 から 1000000000 までをカウントする関数を取り上げます。

以下のコードを実行すると、エンジンはしばらく “ハング” します。サーバサイドの JS の場合は顕著です。ブラウザで実行している場合には、ページ上の他のボタンをクリックしようとしてみてください。カウントが終了するまで、他のイベントが処理されないことが確認できます。