序文
初めにフロントエンドの知識を学び始めたとき、一つ一つの知識を積み重ね、一つ一つの知識点を克服していました。そうして、長い時間が経過してもかなりの知識が蓄積されましたが、それを連携して理解することは常にできませんでした。毎回の整理は非常に分散しており、思考の連続性を保つことができませんでした。最近になって、DNSドメイン名解析、TCP接続の確立、HTTPリクエストの構築、ブラウザのレンダリングプロセスを一連の流れとして整理した後、まるで任督二脈を通したような感覚があり、全体的な構造ができました。以前の知識点がすべてつながり、少なくともそのほとんどの骨組みを知ることができました。知識体系を整理することで、これから新しい知識を学んでも、この体系に沿って学び、相互に関連し合い、理解しやすく、忘れにくくなります。これが本文の目的です。
外国人なので、ChatGPTを翻訳ツールとして使ってます
1. 主要流程の整理
- ブラウザはURLを受信し、新しいプロセスを開始します(この部分ではブラウザのプロセスとスレッドの関係を展開できます)
- ブラウザは入力されたURLを解析し、プロトコル、ドメイン名、パスなどの情報を抽出します(この部分はURLの構成要素に関連しています)
- ブラウザはDNSサーバーにリクエストを送信し、DNSサーバーは多層クエリを通じて該当ドメイン名を対応するIPアドレスに解析し、そのIPアドレスにリクエストを送信してサーバーとの接続とデータ交換を行います(この部分はDNSクエリに関連しています)
- ブラウザはサーバーとTCP接続を確立します(この部分はTCPの三方向ハンドシェイク/四方向ウェーブ/5層ネットワークプロトコルに関連しています)
- ブラウザはサーバーにHTTPリクエストを送信し、リクエストヘッダーとリクエストボディが含まれます(4,5,6,7はHTTPヘッダー、応答コード、メッセージ構造、cookieなどの知識を含みます)
- サーバーはリクエストを受信して処理し、状態コード、応答ヘッダー、応答ボディを含む応答データを返します
- ブラウザは応答データを受信し、応答ヘッダーと応答ボディを解析し、状態コードに基づいて成功を判断します
- 応答が成功した場合、ブラウザはHTTPデータパケットを受信した後の解析プロセス(この部分はHTMLを字句解析し、DOMツリーに解析し、CSSを解析してCSSOMツリー(スタイルツリー)を生成し、レンダリングツリー(スタイル計算)を生成します。その後、レイアウト、層分け、GPU描画の呼び出し等が行われ、最終的に描画結果が合成され、画面に最終ページ画像として表示されます。このプロセスではリフローと再描画が発生します)
- 接続終了 -> TCP接続を四方向ウェーブで切断します
2. ブラウザがURLを受信し、新しいプロセスを開始する
ブラウザはマルチプロセスです
ブラウザはマルチプロセスであり、メインプロセスがあり、新しいタブページを開くたびに新しいプロセスが開始されます(ある状況では複数のタブが1つのプロセスに統合されることがあります)。
注:ここでブラウザには独自の最適化メカニズムがあると思われます。たとえば、複数の空白タブページを開いた後に、Chromeのタスクマネージャーで見ると、プロセスが統合されていることがわかります。
プロセスにはメインプロセス、プラグインプロセス、GPU、タブページ(ブラウザコア)などが含まれます。
- Browserプロセス:ブラウザのメインプロセス(調整および主制御を担当)、ただ1つのプロセスです
- 第三者プラグインプロセス:各種類のプラグインに対応するプロセスで、そのプラグインを使用するときのみ作成されます
- GPUプロセス:最大1つ、3D描画などに使用されます
- ブラウザレンダリングプロセス(ブラウザコア)(内部はマルチスレッド):デフォルトでは各Tabページに1つの* プロセスがあり、互いに影響しません。ページレンダリング、スクリプト実行、イベント処理などが行われます(ブラウザは時々最適化し、複数の空白ページを1つのプロセスに統合することがあります)
記憶の強化:ブラウザでウェブページを開くことは、新しいプロセスを開始することに相当します(プロセス内には独自のマルチスレッドがあります)。
以下の図はChromeブラウザを例にしています。自分でChromeの「さらに多くのツール」>「タスクマネージャー」を通じて確認し、Chromeのタスクマネージャーには複数のプロセスがあることが見えます(それぞれのタブページが独立したプロセスであり、1つのメインプロセスが存在します)。
また、各プロセスのメモリリソース情報とCPU使用率を見ることができます。
JSエンジンがシングルスレッドである
JSエンジンがシングルスレッドである理由は、JavaScriptが最初にブラウザのスクリプト言語として開発されたことに起因します。JavaScriptはDOMなどのブラウザのAPIを操作する必要があります。複数のスレッドが同時にDOM更新などの操作を行うと、競合状態やデータ同期の難しさ、複雑なロックロジックなど、さまざまな問題が発生する可能性があります。したがって、JSエンジンをシングルスレッドに設計することで、これらの問題を避けることができます。JSエンジンはシングルスレッドですが、非同期プログラミングモデルとイベントループメカニズムを使用することで、高い並行処理を実現することができます。
JSがマルチスレッドだった場合のシナリオを説明すると、2つのスレッド、process1とprocess2があります。マルチスレッドのJSを使用しているため、これらは同じDOMに対して同時に操作を行います。process1がそのDOMを削除し、process2がそのDOMを編集すると、2つの矛盾する命令が同時に出され、ブラウザはどのように実行すべきか問題が生じる可能性があります。
レンダリングプロセス中にJavaScriptファイルに遭遇するときどう対応する?
レンダリングプロセス中にJavaScriptファイルに遭遇すると、JSのロード、解析、実行は文書の解析をブロックします。つまり、HTMLパーサーがJavaScriptに遭遇すると、文書の解析を一時停止し、制御をJSエンジンに移譲します。JSエンジンの実行が完了すると、ブラウザは中断された場所から文書の解析を再開します。そのため、最初の画面のレンダリングを速くしたい場合は、最初の画面でJSファイルをロードするべきではなく、これがscriptタグをbodyタグの末尾に配置することが推奨される理由です。現代では、scriptタグを必ずしも末尾に配置する必要はありません。scriptタグにdeferやasync属性を追加することができます。
deferとasyncの違いは次のとおりです。
- deferはページ全体がメモリ内で正常にレンダリングされて終了し(DOM構造が完全に生成され、他のスクリプトの実行が完了する)た後で実行されます
- asyncはダウンロード完了後すぐにレンダリングエンジンがレンダリングを中断し、このスクリプトを実行した後、レンダリングを続行します
- 簡単に言えば、deferは「レンダリング完了後に実行」、asyncは「ダウンロード完了後すぐに実行」です。また、複数のdeferスクリプトがある場合、ページ内に現れる順序でロードされます
- 一方、複数のasyncスクリプトのロード順序は保証されません(モジュールがロードされ次第、そのモジュールは実行されるため、モジュールがいつ完了するかは不確定です)
3. URL解析
URLを入力した後、解析が行われます(URLの本質は統一資源ロケータです)。URLは一般的に以下の部分を含みます:
- プロトコル(Protocol):リソースにアクセスする際に使用されるプロトコルで、一般的にはHTTP、HTTPS、FTPなどがあります
- ホスト名(Host):サーバーのドメイン名またはIPアドレスで、サーバーを一意に識別するために使用されます
- ポート番号(Port):サーバー上のサービスを提供するポート番号で、省略可能です。例えば、HTTPのデフォルトポートは80、HTTPSのデフォルトポートは443です
- パス(Path):サーバー上のリソースへのパスで、アクセスするリソースがあるディレクトリの階層とリソースの名前を表します
- クエリパラメータ(Query):リソースへのリクエストパラメータで、形式はkey=valueで、複数のパラメータは&で接続されます
- フラグメント(Fragment):# 以降のハッシュ値で、通常は特定の位置への移動に使用されます
例示したURL www.example.com/index.html?key1=value1&key2=value2#section の各部分は以下の通りです:
- プロトコル : ここでは明示的に示されていませんが、www.example.com が使われているため、通常のWebアクセスで使用されるHTTPと考えられます。
- ホスト名(Host): www.example.com
- パス(Path): /index.html
- クエリパラメータ(Query): key1=value1&key2=value2
- フラグメント(Fragment): section
したがって、このURLはHTTPプロトコルを使用し、www.example.com というホストにアクセスし、/index.html というパスのリソースに対するリクエストを行い、key1=value1 と key2=value2 の2つのクエリパラメータを持ち、ドキュメント内のsection というIDを持つセクションに直接ジャンプすることを示しています。