プロセスとスレッド#
プロセスとスレッドは工場と労働者に例えることができます。
- プロセスは工場であり、工場には独立したリソース(システムが割り当てた独立したメモリ領域)があります。
- 工場は相互に独立しています(プロセスは相互に独立しています)。
- スレッドは工場内の労働者であり、複数の労働者が協力してタスクを完了します(複数のスレッドがプロセス内で協力してタスクを完了します)。
- 工場には 1 人以上の労働者がいます(1 つのプロセスは 1 つ以上のスレッドで構成されています)。
- 労働者は空間を共有します(同じプロセス内の各スレッドはプログラムのメモリ空間を共有します)。
プロセスはリソースを持ち独立して実行できる最小単位であり、スレッドはプロセスの基盤の上に構築されたプログラムの実行単位です。1 つのプロセスには複数のスレッドが存在できます。公式用語:
- プロセスは CPU リソース割り当ての最小単位です。
- スレッドは CPU スケジューリングの最小単位です。
異なるプロセス間でも通信は可能ですが、コストが高くなります。現在一般的に言われるシングルスレッドとマルチスレッドは、すべて 1 つのプロセス内の単一と複数を指します。
ブラウザのマルチプロセス#
ブラウザの 3 つの概念を理解する必要があります:
- ブラウザはマルチプロセスです。
- ブラウザが実行できるのは、システムがそのプロセスにリソース(CPU、メモリ)を割り当てているからです。
- タブを 1 つ開くごとに、独立したブラウザプロセスが作成されます。
ブラウザのマルチプロセスの紹介#
ブラウザに含まれるプロセス:
- ブラウザプロセス:ブラウザのメインプロセス(調整、主制御を担当)、数は 1 つだけで、役割は:
- ブラウザのインターフェース表示とユーザーとのインタラクションを担当(前進、後退など)。
- 各ページの管理、他のプロセスの作成と破棄を担当。
- Renderer プロセスから得たメモリ内の Bitmap をユーザーインターフェースに描画します。
- ネットワークリソースの管理、ダウンロードなど。
- サードパーティプラグインプロセス:各タイプのプラグインに対応するプロセスで、使用する時にのみ作成されます。
- GPU プロセス:最大で 1 つだけで、3D 描画などに使用されます。
- Renderer プロセス(ブラウザレンダリングプロセス、内部はマルチスレッドです):Renderer プロセスはブラウザのコアを指し、デフォルトでは各タブページに 1 つのプロセスがあり、互いに影響を与えません。主な役割はページのレンダリング、スクリプトの実行、イベント処理などです。
ブラウザのマルチプロセスの利点:
- 単一のページのクラッシュがブラウザ全体に影響を与えるのを避ける。
- サードパーティプラグインのクラッシュがブラウザ全体に影響を与えるのを避ける。
- マルチプロセスはマルチコアの利点を十分に活用します。
- サンドボックスモデルを使用してプラグインなどのプロセスを隔離し、ブラウザの安定性を向上させることが容易です。
Renderer プロセス#
私たちが重点的に議論する必要があるのは Renderer プロセス、つまりブラウザのレンダリングプロセスであり、このプロセスに含まれる主なスレッドは:
- GUI レンダリングスレッド
- ブラウザインターフェースをレンダリングし、HTML、CSS を解析し、DOM ツリーと RenderObject ツリーを構築し、レイアウトと描画を行います。
- インターフェースが再描画(Repaint)を必要とする場合や、何らかの操作によってフローが引き起こされる場合(reflow)、このスレッドが実行されます。
- 注意、GUI レンダリングスレッドと JS エンジンスレッドは排他的です。JS エンジンが実行されているとき、GUI スレッドは一時停止し、GUI の更新はキューに保存され、JS エンジンが空いているときに即座に実行されます。
- JS エンジンスレッド
- JS コア(例えば V8 エンジンとも呼ばれます)で、Javascript スクリプトを解析し、コードを実行します。
- JS エンジンは常にタスクキューにあるタスクの到来を待ち、処理します。1 つのタブページ(renderer プロセス)内では、いつでも 1 つの JS スレッドが JS プログラムを実行しています。
- 同様に注意、GUI レンダリングスレッドと JS エンジンスレッドは排他的です。したがって、JS の実行時間が長すぎると、ページのレンダリングが不連続になり、ページのレンダリングがブロックされることになります。
- イベントトリガースレッド
- ブラウザに属し、JS エンジンには属しません。イベントループを制御するために使用されます(理解できるように、JS エンジン自体が忙しいため、ブラウザが別のスレッドを開いて支援する必要があります)。
- JS エンジンが setTimeout のようなコードブロックを実行するとき(マウスクリック、AJAX 非同期リクエストなど、ブラウザコアの他のスレッドからも来ることがあります)、対応するタスクがイベントスレッドに追加されます。
- 対応するイベントがトリガー条件を満たすと、そのスレッドはイベントを待機中の処理キューの末尾に追加し、JS エンジンの処理を待ちます。
- 注意、JS の単一スレッドの関係により、これらの待機中のキュー内のイベントはすべて、JS エンジンが空いているのを待って処理される必要があります。
- タイマー起動スレッド
- setInterval と setTimeout が存在するスレッド。
- ブラウザのタイマーは JavaScript エンジンによってカウントされるわけではありません。JavaScript エンジンは単一スレッドであり、ブロック状態にあるときはカウントの正確性に影響を与えるため、別のスレッドでカウントし、タイマーをトリガーします。カウントが完了した後、イベントキューに追加され、JS エンジンが空いてから実行されます。
- 注意、W3C は HTML 標準で、setTimeout の時間間隔が 4ms 未満の場合は 4ms と見なすことを規定しています。
- 非同期 HTTP リクエストスレッド
- XMLHttpRequest が接続後にブラウザによって新しいスレッドを開いてリクエストを行います。
- 状態の変化が検出されると、コールバック関数が設定されている場合、非同期スレッドは状態変化イベントを生成し、このコールバックをイベントキューに再度追加し、JavaScript エンジンが実行します。
Browser プロセスと Renderer プロセスの通信#
次に、Browser プロセスと Renderer プロセスの通信方法を分析します:
- Browser プロセスはユーザーのリクエストを受け取ると、まずページの内容を取得する必要があります(例えば、ネットワークを介してリソースをダウンロードするなど)。その後、このタスクを RendererHost インターフェースを介して Renderer プロセスに渡します。
- Renderer プロセスの Renderer インターフェースはメッセージを受け取り、解釈してレンダリングスレッドに渡し、レンダリングを開始します。
- レンダリングスレッドはリクエストを受け取り、ウェブページを読み込み、レンダリングします。この際、Browser プロセスがリソースを取得し、GPU プロセスがレンダリングを支援する必要がある場合があります。
- JS スレッドが DOM を操作する可能性があります(これによりフローが引き起こされ、再描画される可能性があります)。
- 最後に、Renderer プロセスは結果を Browser プロセスに渡します。
- Browser プロセスは結果を受け取り、結果を描画します。
Browser プロセスと Renderer プロセスの通信
Renderer プロセスのマルチスレッド#
上記の分析から、Renderer プロセスはマルチスレッドであることがわかります。主に含まれるのは:GUI レンダリングスレッド、JS エンジンスレッド、イベントトリガースレッド、タイマー起動スレッド、非同期 HTTP リクエストスレッド。これらのスレッド間のいくつかの概念を理解する必要があります。
GUI レンダリングスレッドと JS エンジンスレッドの排他性#
上記で述べたように、GUI レンダリングスレッドと JS エンジンスレッドは排他的です。JavaScript は DOM を操作できるため、これらの要素の属性を変更しながらインターフェースをレンダリングすると(つまり、JS スレッドと UI スレッドが同時に実行される場合)、レンダリングスレッドが前後で取得する要素データが一致しない可能性があります。
したがって、予期しない結果を防ぐために、ブラウザは GUI レンダリングスレッドと JS エンジンスレッドを排他的な関係に設定しています。JS エンジンが実行されているとき、GUI スレッドは一時停止し、GUI の更新はキューに保存され、JS エンジンスレッドが空いているときに即座に実行されます。
この排他関係から導き出されるのは、JS の実行時間が長すぎるとページがブロックされるということです。例えば、JS エンジンが大量の計算を行っている場合、GUI に更新があっても、それはキューに保存され、JS エンジンが空いているときに実行されるのを待つことになります。そのため、大量の計算のために JS エンジンが空くまでに非常に長い時間がかかる可能性があり、非常に遅く感じることになります。したがって、JS の実行時間が長すぎることを避けるようにし、ページのレンダリングが不連続になり、ページのレンダリングがブロックされる感覚を引き起こさないようにする必要があります。
Web Worker#
Web Worker についての MDN の紹介は以下の通りです:
Web Worker は、Web コンテンツがバックグラウンドスレッドでスクリプトを実行するための簡単な方法を提供します。スレッドはユーザーインターフェースを妨げることなくタスクを実行できます。さらに、XMLHttpRequest を使用して I/O を実行できます(ただし、responseXML および channel プロパティは常に空です)。一度作成されると、ワーカーは作成した JavaScript コードにメッセージを送信でき、指定されたイベントハンドラーにメッセージを発行します(その逆も同様です)。
Web Worker の役割は、JavaScript にマルチスレッド環境を提供し、メインスレッドが Worker スレッドを作成し、いくつかのタスクを後者に実行させることを許可します。メインスレッドが実行されている間、Worker スレッドはバックグラウンドで実行され、両者は互いに干渉しません。Worker スレッドが計算タスクを完了すると、結果をメインスレッドに返します。この利点は、計算集約型または高遅延のタスクが Worker スレッドによって処理され、メインスレッド(通常は UI インタラクションを担当)がスムーズに動作し、ブロックされたり遅くなったりしないことです。
Worker スレッドが一度成功裏に作成されると、常に実行され、メインスレッド上のアクティビティ(ボタンのクリック、フォームの送信など)によって中断されることはありません。これにより、メインスレッドとの通信に即座に応答できます。しかし、これにより Worker はリソースを多く消費しすぎる可能性があるため、過度に使用すべきではなく、一度使用が完了したら閉じるべきです。
ブラウザのレンダリングプロセス#
ブラウザのコンテンツレンダリングは、以下のいくつかのステップに分けることができます:
- HTML を解析して DOM ツリーを構築します。
- CSS を解析してレンダーツリーを構築します(CSS コードをツリー状のデータ構造に解析し、DOM と結合してレンダーツリーを作成します)。
- レンダーツリーのレイアウト(Layout/reflow)を行い、各要素のサイズと位置を計算します。
- レンダーツリーを描画(paint)し、ページのピクセル情報を描画します。
- ブラウザは各レイヤーの情報を GPU に送信し、GPU は各レイヤーを合成(composite)して画面に表示します。
レンダリングが完了した後、load イベントが実行され、そのフローチャートは以下の通りです:
ブラウザのレンダリングプロセス
load イベントと DOMContentLoaded イベント#
load イベントと DOMContentLoaded イベントの実行順序を比較する前に、それぞれのトリガータイミングを理解する必要があります:
- DOMContentLoaded イベントがトリガーされるとき、DOM が完全に読み込まれたときのみ発生し、スタイルシートや画像は含まれません(例えば、async で読み込まれるスクリプトがある場合は必ずしも完了しているわけではありません)。
- onload イベントがトリガーされるとき、ページ上のすべての DOM、スタイルシート、スクリプト、画像がすでに読み込まれています。
したがって、実行順序はDOMContentLoaded -> load
であることがわかります。
CSS の読み込みは DOM ツリーのレンダリングをブロックしますか?#
この質問に答える前に、重要な概念を知っておく必要があります:CSS は別のダウンロードスレッドによって非同期にダウンロードされます。
それから答えを得ることができます:CSS の読み込みは DOM ツリーの解析をブロックしません(非同期読み込み時に DOM は通常通り構築されます)が、レンダーツリーのレンダリングをブロックします(レンダリング時には CSS の読み込みが完了するのを待つ必要があります。レンダーツリーは CSS 情報を必要とします)。
これはブラウザの最適化メカニズムの一つです。CSS を読み込んでいるとき、下の DOM ノードのスタイルが変更される可能性があるため、CSS の読み込みがレンダーツリーのレンダリングをブロックしない場合、CSS の読み込みが完了した後、レンダーツリーが再度再描画またはフローを引き起こす必要があるため、不要な損失が発生します。したがって、まず DOM ツリーの構造を解析し、できる作業を完了させ、その後 CSS が読み込まれた後、最終的なスタイルに基づいてレンダーツリーをレンダリングするという方法が、パフォーマンス面で確かに良い結果をもたらします。
通常のレイヤーと合成レイヤー#
ブラウザのレンダリングプロセスの第 5 ステップで述べたように、ブラウザは各レイヤーの情報を GPU に送信し、GPU は各レイヤーを合成(composite)して画面に表示します。ここでcomposite
の概念が関与します。ブラウザのレンダリングレイヤーは一般的に 2 つの大きなカテゴリに分けられます:通常のレイヤーと合成レイヤー。
まず、通常の文書フローは合成レイヤーと理解できます(ここではデフォルトの合成レイヤーと呼ばれ、内部にどれだけの要素が追加されても、実際には同じ合成レイヤー内にあります)。absolute および fixed レイアウトは通常の文書フローから外れることができますが、それでもデフォルトの合成レイヤーに属します。
次に、ハードウェアアクセラレーションの方法で新しい合成レイヤーを宣言できます。これにより、リソースが個別に割り当てられ、もちろん通常の文書フローから外れます。こうすることで、この合成レイヤー内でどのように変化しても、デフォルトの合成レイヤー内のフローの再描画には影響しません。
こう理解できます:GPU 内の各合成レイヤーは個別に描画されるため、互いに影響を与えません。これが、特定のシーンでハードウェアアクセラレーションの効果が素晴らしい理由です。
DOM 要素を合成レイヤー(ハードウェアアクセラレーション)に変える方法は:
translate3d、translateZ
opacity
プロパティ / 遷移アニメーション(アニメーションが実行されている間に合成レイヤーが作成され、アニメーションが開始または終了した後、要素は以前の状態に戻ります)。will-change
プロパティを使用して、ブラウザに変化を事前に通知し、ブラウザが最適化作業を開始します(これを使用した後は解放するのが最良です)。<video><iframe><canvas><webgl>
などの要素。
上記の分析から、absolute とハードウェアアクセラレーションの違いがわかります:absolute は通常の文書フローから外れることができますが、デフォルトの合成レイヤーからは外れることができません。したがって、absolute 内の情報が変更されても通常の文書フロー内のレンダーツリーは変更されませんが、ブラウザが最終的に描画する際には全体の合成レイヤーが描画されるため、absolute 内の情報の変更は依然として全体の合成レイヤーの描画に影響を与えます。ブラウザはそれを再描画します。合成レイヤー内の内容が多い場合、absolute による描画情報の変化が大きすぎると、リソース消費が非常に深刻になります。
一方、ハードウェアアクセラレーションは別の合成レイヤーであるため、その情報の変更はデフォルトの合成レイヤーには影響を与えず、自分自身の合成レイヤーにのみ影響を与え、最終的な合成(出力ビュー)を引き起こします。
ハードウェアアクセラレーションは非常に魅力的に見えますが、使用には注意が必要です。合成レイヤーを大量に使用しないようにし、リソース消費が過度になると、ページが逆に遅くなる可能性があります。
ハードウェアアクセラレーションを使用する際は、できるだけインデックスを使用し、ブラウザが後続の要素に合成レイヤーを作成するのを防ぎます。理由は、webkit CSS3 では、要素にハードウェアアクセラレーションが追加され、インデックスのレベルが比較的低い場合、その要素の後ろにある他の要素はデフォルトで合成レイヤーとしてレンダリングされるため、適切に処理しないとパフォーマンスに大きな影響を与える可能性があるからです。
イベントループ#
ここで、この記事の最も重要な部分、JS の実行メカニズムにやっと入ります。Renderer プロセスの 5 つのスレッドを振り返ります:GUI レンダリングスレッド、JS エンジンスレッド、イベントトリガースレッド、タイマー起動スレッド、非同期 HTTP リクエストスレッド。
次に、1 つの概念を理解します:
- JS は同期タスクと非同期タスクに分かれます。
- 同期タスクはすべてメインスレッド(JS エンジンスレッド)上で実行され、
実行スタック
を形成します。 - メインスレッドの外で、イベントトリガースレッドは
タスクキュー
を管理し、非同期タスクに実行結果があると、タスクキューにイベントを配置します。 - 実行スタック内のすべての同期タスクが実行されると、システムはタスクキューを読み取り、実行可能な非同期タスクを実行スタックに追加し、実行を開始します。
実行スタックとタスクキュー
以下のように説明できます:
- メインスレッドは実行スタックを実行し、スタック内のコードが API を呼び出す(例えば ajax リクエスト)と、イベントを生成し、タスクキューに追加します。
- 実行スタック内のコードが実行されると、タスクキュー内のコードを読み取り、このようにループします。
マクロタスクとマイクロタスク#
JS には 2 種類のタスクタイプがあります:マクロタスク
とマイクロタスク
、つまりマクロタスクとマイクロタスクです。ECMAScript では、マクロタスクはtask
、マイクロタスクはjobs
と呼ばれます。
- マクロタスク:実行スタックで実行されるコードは 1 つのマクロタスクと理解できます(イベントキューからイベントコールバックを取得して実行スタックに追加するたびに含まれます)。これはイベントトリガースレッドによって管理されます。
- 各タスクは最初から最後までこのタスクを完了し、他のタスクは実行しません。
- ブラウザは JS 内部のタスクと DOM タスクが順序よく実行されるように、1 つのタスクが終了した後、次のタスクが開始される前にページを再描画します(
task->レンダリング->task->...
)。 - マイクロタスク:現在のタスクが終了した後に即座に実行されるタスクで、現在のタスクの後、次のタスクの前、レンダリングの前にJS エンジンスレッドによって管理されます。
- そのため、setTimeout(setTimeout はタスク)よりも応答速度が速くなります。レンダリングを待つ必要がないからです。
- 特定のマクロタスクが完了すると、その実行中に生成されたすべてのマイクロタスクが完了します(レンダリングの前に)。
マクロタスクとマイクロタスクにはどのようなものがありますか?
- マクロタスク:メインコードブロック、setTimeout、setInterval。
- マイクロタスク:Promise、process.nextTick(Node 環境では、process.nextTick の優先度が Promise よりも高い)。
最後に、マクロタスクとマイクロタスクの実行メカニズムをまとめます:
マクロタスクとマイクロタスクの実行メカニズム
- マクロタスクを実行します(スタックにない場合はイベントキューから取得します)。
- 実行中にマイクロタスクに出会った場合、それをマイクロタスクのタスクキューに追加します。
- マクロタスクが完了した後、現在のマイクロタスクキュー内のすべてのマイクロタスクを即座に実行します(順次実行)。
- 現在のマクロタスクが完了した後、レンダリングをチェックし、GUI スレッドがレンダリングを引き継ぎます。
- レンダリングが完了した後、JS スレッドが引き続き引き継ぎ、次のマクロタスクを開始します(イベントキューから取得します)。
参考記事: