概要
フロントエンドエンジニアになってからそこそこの時間がたったものの、パフォーマンスについてイチからしっかりと学んだことなかったなと思う今日この頃。
流石にそろそろエンジニアとして提供できる価値を増やしていかないとキャリア的にも危ぶまれてきました(汗
危機感半分、興味半分、新しい学びのアウトプットとしてこの記事を書いてみます。
まずは基礎編①ということで、ブラウザのレンダリングの仕組みのうち、Loadingと呼ばれるフェーズをおさえていきます。
ゴール
この記事を読めば、パフォーマンスチューニングを行う上で最低限必要な、「Loading」についての基礎知識が得られること
参考書籍
ブラウザ触った方がより理解できるけど、ハンズオンなしでも読めるのがイイ
(この記事よりこの本を読んだ方が絶対にイイ)
基礎編① 〜 Loading 〜
パフォーマンスの定義
色々な文脈で語られる「パフォーマンス」という言葉ですが、参考書籍では「ユーザーのさまざまな振る舞いに対して、Webページが応答を返す速さ」と定義していました。
この記事でもそれに倣い、同じ定義を使います。
エンジン
ブラウザを構成するソフトウェアコンポーネントのうち、パフォーマンスチューニングを行う上でおさえておきたい重要なコンポーネントは以下二つです。
レンダリングエンジン
WebページのHTMLやCSSなどのコードを読み込み、画面上に私たちが普段見ているWebサイトの見た目として表示する役割を持つプログラム。
ブラウザによって搭載しているエンジンが違う。
| レンダリングエンジン | ブラウザ |
|---|---|
| Blink | Google Chrome、Microsoft Edge、Opera、Brave |
| WebKit | Safari |
| Gecko | Firefox |
JavaScriptエンジン
JavaScriptのコードを読み込み、コンピューターが理解できる機械語に変換して実行するプログラム。
これもブラウザによって搭載しているエンジンが違う。
| JavaScriptエンジン | ブラウザ |
|---|---|
| V8 | Google Chrome、Microsoft Edge1、Opera、Brave |
| Nitro(JavaScriptCore) | Safari |
| SpiderMonkey | Firefox |
ブラウザにおけるレンダリングの流れ
ブラウザがレンダリングを完了するまでにはさまざまな要素が相互に関連し合うためとても複雑ですが、一つひとつの工程に分解すればなんとか理解できるはずです。
大まかな流れ
レンダリングの流れは、大きく4つの工程に分けられます。
- Loading:リソースの読み込み
- Scripting:JavaScriptの実行
- Rendering:レイアウトツリー構築(今までの文脈のレンダリングとは別)
- Painting:Rendering結果の描画
①~④までの処理の流れをレンダリングパイプライン(Rendering Pipeline)と呼び、レンダリングによって生成された成果物(1コマ)をフレームと呼びます。
流れの詳細
今回は①Loadingについて詳しく見ていきます。
①Loading
Loadingの中ではさらに2つの工程に分かれています。
- Download:ネットワークを介してサーバーからHTML、CSS、JavaScript、画像などのファイルをパソコン(ブラウザ)の手元に持ってくる工程
- Parse:ダウンロードした生データ(テキストやバイト列)を、リソースの種類に応じてブラウザが扱えるデータ構造へ変換する工程
①-1 Download
ブラウザはURLをもとに、レンダリングに必要なリソースを様々なネットワークプロトコルを介して取得します。
ネットワークプロトコルとは、コンピューター同士がネットワーク経由でスムーズに通信(データのやり取り)を行うための「共通のルールや約束事」のことです。
ブラウザではHTTPが最もよく利用されるネットワークプロトコルですが、これだけではサーバーからリソースを取得することはできません。
HTTPはOSI参照モデルでいう、アプリケーション層で使用されるプロトコルです。
そこからプレゼンテーション層、セッション層、トランスポート層、ネットワーク層、データリンク層、物理層を通ってサーバーと通信し、リソースを取得する必要があります。
今回はハードウェアまでは扱わないので、データリンク層、物理層の話は割愛します。
HTTPのプロトコルスタックをまとめると以下になります。
| ネットワーク階層 | プロトコル |
|---|---|
| アプリケーション層 | HTTP |
| プレゼンテーション層 / セッション層 | TLS(httpsのみ) |
| トランスポート層 | TCP |
| ネットワーク層 | IP |
また、URLに含まれるホスト名の解決にはDNSが利用されます。
ホスト名の解決とは、URLに含まれるホスト名をIPアドレスに変換すること、です。なぜなら、HTTP通信をするにはIPアドレスが必要だからです。HTTP通信を行う前にまずDNSプロトコルを使用してホスト名のIPアドレスを取得するステップが入ります。
DNSのプロトコルスタックは以下になります。
| ネットワーク階層 | プロトコル |
|---|---|
| アプリケーション層 | DNS |
| トランスポート層 | 基本的にUDP、データが大きい場合はTCP |
| ネットワーク層 | IP |
IP(Internet Protocol)
IPは、インターネットに接続されたコンピューター同士が、データを正しい相手に届けるための「住所(IPアドレス)の割り当て」と「配送(ルーティング)」を定めた共通のルールです。
ネットワーク上でデータをやり取りする際に、扱いやすいように小さく細切れにした「データの小包」のことをパケットと呼びますが、IPはこのパケットを単位として、指定されたIPアドレス(住所)へとデータを運びます。
なぜデータをパケットに分けるのか?
ブラウザがサーバーからWebページのリソース(HTMLや画像など)を取得するとき、大きなデータを一塊のまま送ることはしません。必ずパケットに分割して送信します。理由は主に2つあります。
- ネットワークの回線を独占させないため:大容量のデータを一塊で送ると、その送信が終わるまで他の人が通信できなくなってしまいます。パケットに分けて少しずつ流すことで、みんなが同時に回線をシェアすることができます
- エラーが起きたときの負担を減らすため:通信の途中でデータが壊れたり消えたりした場合、一塊のままだと「最初から全て再送」になってしまいます。パケットに分かれていれば、壊れたその1コマ(1パケット)だけを再送すれば済むため、効率的です
IPについて理解しておくべきことは、このプロトコルは信用ならないプロトコルだということです。
どういうことかというと、以下のような特性があるからです。
- パケットの内容は壊れる可能性がある
- 送信したパケットは消失する可能性がある
- パケットの送信順は保証されない
- パケットが複製されて届くことがある
このような弱点を補うために利用されるのが、IPの上位プロトコルに当たるTCPになります。
TCP(Transmission Control Protocol)
TCPは、IPの弱点を補う機能を持った上位プロトコルです
- 相手先にデータが届いているかどうかを確認する
- データの欠損や破壊を検知して再送する
- データの送信順を担保する
TCPにはコネクションという概念があり、IPと違ってデータを送る前に送信元と送信先の間で仮想的な専用回線を繋ぐことができます。
IPは手紙のイメージで、TCPは電話のイメージを持つと良いかもしれません。
これにより、一度パイプを繋いでしまえば相手が確実にそこにいることがわかっているため、データが途中で消えたり順番が入れ替わったりしても、お互いに確認し合いながら確実にデータを届けることができます。
TCPでコネクションを確立するには、クライアントとサーバー間で3ウェイハンドシェイクと呼ばれる手続きを行う必要があります。
3ウェイという名前の通り、3回のやり取りが発生します。
パフォーマンスの観点で抑えておかなければならないのは、TCP接続の確立にはSYNなどのパケットをやりとりする必要があるという点です。
接続の確立にパケットをやり取りするということは、そのやり取りの時間よりも早くリソースを取得することはできない、ということです。
TLS(Transport Layer Security)
ブラウザに与えたURLのプロトコルがhttpsだった場合、ブラウザはTLSプロトコルを利用します。httpとhttpsの違いは、このTLSプロトコルを使用するかどうか、です。
TLSは一般的にはSSL(Secure Sockets Layer)と呼ばれます。よく、「WebサイトをSSL化する」という言葉を耳にすることがありますが、そのSSLです。
SSLというのはTLSの前身となった技術で、SSLを元に標準化し、セキュリティ等を向上させたのがTLSです。
TLSはTCPで開通したコネクションをセキュアにするプロトコルです。
主に以下のセキュリティ機能を提供します。
- クライアントとサーバーの認証機能
- 通信データの暗号化
- 通信データの改ざん検出
TLSでもTCPと同様にクライアントとサーバー間で手続きを行う必要があり、それをTLSハンドシェイクと呼びます
TLSハンドシェイクも、TCPと同様にパケットのやりとりが発生するため、リソース取得までに追加でディレイが発生することを覚えておく必要があります。
「TCPの3ウェイハンドシェイク + TLSハンドシェイク」分の時間がかかります。
また、サーバーの証明書の検証では無視できないオーバーヘッドを起こす可能性があることも覚えておきたい特徴になります。
UDP(User Datagram Protocol)
UDPはトランスポート層(TCPと同じ階層)に属するプロトコルで、一言で言うと「スピードと軽さを最優先した、投げっぱなしの通信ルール」です。
IPとほぼ同じですが、UDPではポート番号を指定する機能が備わっています。住所における部屋番号的な部分を指定する機能があるというイメージです。
UDPはTCP/TLSで説明したパケットのやり取りは存在しないため、とにかく爆速で軽いというメリットがあります。もちろんデータが消失する等のデメリットがあるため、DNSへの問い合わせ(ホストのIPアドレス教えてもらうだけ)などの「やり取りするデータサイズが非常に小さく、1回の往復(1往復)で終わる」通信で使われることが多いです。
HTTP(Hyper Text Transfer Protocol)
HTTPは、ブラウザとサーバーがWebページのコンテンツをやり取りするためのルールです。
HTTPはとてもシンプルに設計されており、必ずブラウザからの「リクエスト」で始まり、サーバーからの「レスポンス」で終わるという、一問一答のキャッチボールです。
• ブラウザ(リクエスト):「index.htmlをください!」
• サーバー(レスポンス):「了解、これが要望のHTMLです!」
KeepAliveについて
HTTPにはHTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3などのバージョンがあります。現在はブラウザとサーバーの対応状況に応じて、HTTP/1.1、HTTP/2、HTTP/3などが使われます。
HTTP/1.0の時代は、「1回のリクエスト・レスポンスごとに、毎回TCPコネクションを完全に切断する」のが基本ルールでした。これにより、「画像が50枚あるページ」などを読み込む場合、50回以上も3ウェイ・ハンドシェイク(+TLSハンドシェイク)を繰り返すことになり、ディレイが膨大になって画面の表示が非常に遅くなってしまうという問題が発生しました。
この問題を解決するために登場したHTTP/1.1では、「一度繋いだTCPコネクションは、明示的に切らない限りそのまま維持して使い回す(持続接続)」が標準(デフォルト)ルールになりました。
この機能をKeepAliveと言います。
DNS(Domain Name System)
DNSは、インターネット上の「電話帳」のような役割を果たすシステムです。
「ホスト名の解決」について先述した通り、人間が覚えやすい文字列(ホスト名:example.com)を、コンピューターが通信で実際に使用する数字の羅列(IPアドレス:93.184.215.14)に翻訳・変換する仕組みそのものを指します。
ただ、ブラウザがURLを入力するたびに毎回世界中のサーバーをたらい回し(フルルックアップ)にしていると、それだけでLoadingが遅くなってしまいます。
そこで活躍するのが「DNSキャッシュ」です。
- 仕組み:一度調べたドメインとIPアドレスのペアは、パソコンやブラウザ、身近なルーター、プロバイダのサーバー(キャッシュDNSサーバー)が一時的に記憶(キャッシュ)しておきます。
- 効果:2回目以降のアクセスでは、世界中のサーバーに聞きに行かず、手元の記憶から一瞬でIPアドレスを引っ張り出せるため、DNSのディレイ(遅延)をほぼゼロにすることができます。
①-2 Parse
Downloadフェーズを通じて無事にブラウザに届いたリソース(HTML,CSS,JS,画像など)は、この時点ではまだただのテキストやバイナリデータに過ぎず、このままではブラウザが画面のどこに何を配置すればいいのか理解できません。
Parse(構文解析)とは、Downloadしたテキストデータを上から順に読み解き、ブラウザがプログラムで扱いやすい「ツリー構造のデータ(オブジェクト)」へと変換する工程です。
一番最初に取得するリソースはHTMLで、HTMLファイル内に記述されているリソースの参照を元に再帰的に関連リソースを取得していきます。
HTMLの読み込み
HTMLのパースは、ブラウザの内部で以下のようなステップを踏んで高速に行われます。
- バイト列から文字列への変換:サーバーからパケットで届いた生データ(0と1のバイト列)を、指定された文字コード(UTF-8など)を基に、人間が読める「HTMLテキスト」に変換する
- トークン化:HTMLテキストを上から1文字ずつ読み進め、
<html>、<body>、<h1>といった「タグの塊(トークン)」に細かく分解する - ツリーの構築(DOMツリーの完成):分解したトークンを読みながら、タグの「親子関係」を解析し、親、子、孫へと枝分かれする木構造(DOMツリー)を組み立てる
HTMLのパースは、ファイルがすべてダウンロードし終わるのを待たずに、データが届いた順からストリーミング形式で上から順に開始されるのが大きな特徴です。
CSSの読み込み
HTMLをパースしている途中で <link rel="stylesheet"> や <style> タグが見つかると、ブラウザはスタイルシートの解析を開始します。
CSSを解析すると、スタイルシート内のセレクターや宣言を表現したCSSOM(CSS Object Model)が構築されます。
その後のスタイル計算で、ブラウザはCSSのセレクターとDOM要素を照合し、カスケードや継承を考慮して、各要素に適用される最終的なスタイルを計算します。
まとめ
さて、今回はブラウザのレンダリング4工程のうち、最初の「Loading」工程について詳しく見てきました。
- リソースをDownloadするには、いくつかのプロトコルを使用する
- IPはパケットを渡す
- TCPは送信先と送信元をコネクションする
- TLSはTCPで繋いだトンネルをセキュアに強化
- UDPはリクエスト投げっぱなしだけど爆速
- HTTPは1対1のシンプルなキャッチボール
- DNSは名前解決とキャッシュ
- リソースはそのままだとブラウザが理解できない
- HTMLやCSSをParseしてブラウザが理解できるDOM,CSSOMに変換する
どのようなルールで通信が行われ、リソースをダウンロードし、リソースがパースされるのか、少し理解が進んだ気がします。
まだ序盤も序盤で、最後まで記事を書き切れるのか不安が残るところではありますが、勉強を進めたいと思います。(誰か一緒に勉強してほしい)
-
Microsoft Edgeは、2020/1以前は独自のChakraというエンジンを使っていたが、それ以降はV8に切り替えたとのこと ↩