はじめに
フロントエンド周りのパフォーマンスについて抑えたく以下の書籍で学習中なので、まとめつつ共有しようかと思います。
「Webフロントエンド ハイパフォーマンス チューニング」
なぜパフォーマンスチューニングが必要か
現在正常稼働しているWebアプリケーションでも、利用ユーザ数の増加によりコンピューティング、ネットワーク、ストレージ等々の利用可能なリソース不足により応答に時間がかかったりと操作性に悪影響を与えることがあります。ユーザとしてはある操作で数秒待たされるのにもストレスを感じる状況でなかなか処理が終わらないといった体験はWebアプリケーション自体に悪い印象を与えることに繋がります。
つまりアプリケーションの顧客体験を向上させるためにパフォーマンスチューニングが必要となってきます。
フロントエンドにおけるパフォーマンスチューニングの重要性
パフォーマンスチューニングをする対象としてネットワークのレイヤなども考えられますが、フロントエンドにおけるパフォーマンスチューニングの重要性を考えます。
昔に比べて現在はマシンの性能が上がってきていてリソース不足となる心配は減ってきているものの、モバイル機器によるアクセスが増えてきている状況でネットワーク環境が必ずしも安定した環境で利用されているとは限りません。さらにモバイル機器自体もデスクトップマシンに比べるとスペック面で劣る部分もあり、フロントエンドの観点では描画面のチューニングや初期読み込みの最適化をする必要が出てきます。
またWebページ内で複雑な処理を行うニーズも増えてきており、よりフロントエンドのパフォーマンスチューニングの重要性が増しています。
フロントエンドのパフォーマンスチューニングをするための前提知識
フロントエンドのパフォーマンスチューニングを実施するために、ブラウザの仕組みを理解することが必要になります。
開発をおこなっている際はHTMLやCSSを書くだけでブラウザ上簡単に意図する情報を表示することができますが、実際にブラウザの中でどのように解釈されてどんな処理が行われて描画されているか、といったところはあやふやな部分が多いかと思います。
この理解が曖昧だとパフォーマンスチューニングを行う際の障壁※となるので、まずはこの記事ではブラウザの仕組みについてまとめていこうかと思います。
※チューニングのベストプラクティスと言われている操作をすることでなぜパフォーマンスが上がるかといったことが理解できない
※工数や可読性とのトレードオフを無視したチューニングにつながる
※かえって動作が遅くなるといった問題を引き起こす
ブラウザでのレンダリングの仕組み
ブラウザ内のコンポーネント
レンダリングの仕組みを押さえる前に、ブラウザにて構成されているチューニングに関わる重要な2つのコンポーネントを確認します。
- レンダリングエンジン
- JavaScriptエンジン
レンダリングエンジン
HTMLの描画エンジンを指します。ブラウザそのものではなく、純粋にHTMLを解釈してページを描画するためのコンポーネントになります。(URLを入力するアドレスバーやブックマーク機能等のブラウザ機能に対しては責任を持っていません。)
レンダリングエンジンはHTMLやCSS、その他画像ファイルやJavaScriptなどの各種リソースを読み取って、それをページ上にピクセルとして描画します。
JavaScriptエンジン
その名の通りJavaScriptの実行環境を提供するコンポーネントです。つまりこのコンポーネントがいるおかげてscriptタグに記載しているJavaScriptが動作します。
レンダリングエンジンと併用することで、DOMツリーなどの内部のオブジェクトやAPIに対してJavaScriptからアクセスすることでページ上インタラクティブな動作を実現することができます。(HTML要素の変更とかとか)
レンダリングの流れ
ブラウザのアドレスバーにURLを打ち込んでからページが表示されるまでにどういったプロセスがあるのか見ていきます。
レンダリングの処理を大まかに見ていくと4つの処理に分けられます。
Loading -> Scripting -> Rendering -> Painting
↑の処理をより詳細にみると以下のようになります。
Loading - リソース読み込み
最初に行われるのがLoadingと呼ばれるリソース読み込みの処理になります。ブラウザは入力されたURLからHTMLを読み込んで、そこからさらに付随するリソース(CSS/img...)を読み込んで解釈を進めます。
このフェースでは以下の2つの処理が行われます。
- Download
- Webページのリソースをサーバからダウンロードする
- Parse
- ダウンロードしたリソースをパース(構文解析)してレンダリングエンジンの内部表現に落とし込む
- -> HTMLやCSSをDOMツリーオブジェクトに変換する
一番最初に取得されるリソースはHTMLファイルです。HTMLファイルに記載されているリソース参照をもとにそのリソースを読み込み、そこにもリソース参照があれば再起的に次々読み込みます。
読み込んだリソースはパースされて内部表現に利用するDOMツリーに変換されます。ここで用意されたDOMツリーは後続のRenderingやPainting処理に利用されます。
DOMツリー
DOM(Document Object Model)はブラウザ上(レンダリングエンジン)でHTMLを表現するためのオブジェクトです。
パース処理にてHTMLは以下の工程でDOMツリーに変換されます。
- 字句解析によるトークンのリスト化
- 構文解析による構文木の構築
- 構文木内にあるJavaScriptを実行しつつDOMツリーの構築
レンダリングエンジンはHTMLを受け取りつつ字句解析によるトークン(意味的に1つの塊になっている文字列)化を行います。
その後トークンをもとに木構造のデータである構文木を構築します。
構築された構文木内に含まれているJavaScriptを同期的に実行しながら画面の描画を進めます。※
※例えば以下のような描画内容がJavaScriptの実行結果に遺存するものをあるため、同期的にJavaScriptを実行します。
<p>
<script>
document.write('some text');
</script>
</p>
このような構文木があることで、JavaScript上でHTMLの特定の要素を指定することができ、かつDOM APIをJavaScriptから利用することでページ上でインタラクティブな操作を実施することができます。
const text = document.getElementById("text");
const changeText =()=>{
text.textContent="text changed!"
}
こんな処理もDOMがないとできない。
CSSOM ツリー
HTMLは前述のDOMツリーに変換されますが、CSSはCSSOM(CSS Object Model)ツリーへと変換されます。
CSSOMツリーはDOMツリーと同様レンダリングで扱うための内部表現になりますが、JavaSctiptから操作することができます。
後続の処理であるLayoutやPaintingフェーズでDOM要素に適用するスタイルを解釈するために利用されます。
Scripting - JavaScript実行
リソースの読み込みが完了するとScriptingフェーズが始まります。レンダリングエンジンはJavaScriptのコードをJavaScriptエンジンに引き渡して実行させます。
JavaScriptエンジンがJavaScriptを実行する流れは以下のようになります。
- JavaSctiptコードの字句解析
- 構文解析
- コンパイル
- 実行
字句解析と構文解析
JavaScriptエンジンは与えられたJavaScriptコードを実行可能な形式にコンパイルした上で実行します。
コンパイルを行うためには抽象構文木(AST)と呼ばれるコンパイル可能な形式に変換する必要があります。
以下のJavaScriptコードを例に示します。
console.log("Hello World");
字句解析
上記のコードを字句解析すると以下のようなトークンに変換できます。
Identifier -> console
Operator -> .
Identifier -> log
Parenthesis-> (
String -> "Hello World"
Parenthesis-> )
Separator. -> ;
構文解析
Call Expression
- String: "Hello World"
- Menber Expression
- Identifier: console
- Identifier: log
ざっくりの理解ですが、コード実行に必要のない情報を取り除いて、コンパイルしやすい形式にする認識です。。
抽象構文木が作成されたため、それをコンパイルして実行可能なコードが生成できます。
コードはCPU上で実行され、JavaScript内でDOMツリーを操作することができます。
Rendering - レイアウトツリー構築
JavaScriptの実行が終わるとRenderingフェーズに移行します。
ここでは大別して以下の処理が行われます。
- Calculate Style(スタイルの計算)
- Layout(レイアウト)
Calculate Style(スタイルの計算)
この処理ではDOMツリー内のDOM要素に対してどのようなCSSプロパティが割り当てられるかを計算します。先述のCSSOMツリーを参照してCSSセレクタのマッチング処理が行われます。この時、CSSセレクタがマッチするかを全てのDOM要素に対して試行します。
以下のCSSルールセットを例にDOM要素にCSSを割り当てる流れを示します。
div.my-button, button{
background-color:green;
color;white;
}
この場合、CSSセレクタは「div.my-button」と「button」になります。
レンダリングエンジンはDOMツリーから全てのDOM要素一つ一つに対して、どのCSSルールセットが割り当てられるかを総当たりで計算します。単純にDOM要素が100個ありCSSルールセットが50個あった場合、この計算量は 100 * 50 = 5000 回のマッチング処理が行われます。
CSSセレクタのマッチング処理はセレクタの右側から行われます。例示の場合以下の流れで解釈されます。
- DOM要素のclass属性に「button」が含まれているか
- その親要素のclass属性に「container」が含まれているか
- その親要素のDOM要素名が「body」であるか
ここでマッチしたDOM要素に対して該当のCSSルールセットを適用します。
Layout(レイアウト)
Calculate StyleにてDOM要素に当たるCSSプロパティを算出したあと、レンダリングエンジンはDOMツリー内の全ての要素のレイアウト情報を計算します。
レイアウト情報とは次のようなものを指します。
- 要素の大きさ
- 要素のマージン
- 要素のパディング
- 要素の位置
- ...
HTMLやCSSの読み込みからここまでの処理をまとめると以下のようになります。
ここまでの処理でLayoutTreeの作成が完了し後続のPainting処理で描画を行います。
Painting - レンダリング結果の描画
先のフェーズで作成した LayoutTreeをもとに描画を行います。Paintingフェーズではブラウザ上でユーザが実際に見ることのできるピクセルを描画します。
このフェーズでは大別して以下の処理が行われます。
- Paint
- Rasterize
- Composiote Layers
Paint
LayoutTreeをもとに、内部の低レベルな2Dグラフィックエンジン向けの命令の列を生成します。
Rasterize
Paintにて生成された命令を用いて、実際にピクセル(ビットマップ)へと描画します。この時、CSSのz軸で上下関係があるようなオーバラップするコンテンツがある場合、レイヤーという単位で一枚一枚描画されます。
Composiote Layers
Rasterizeにて生成されたレイヤーを合成して最終的なレンダリング結果をページに描画します。
ここまでの処理で与えられたURLから取得したリソースをもとに、ブラウザ上にコンテンツを表示することができます。
再レンダリング
ユーザやブラウザからのアクション、もしくはJavaScriptコードの実行によって再レンダリングが引き起こされることがあります。
この場合、前述の全ての処理をもう一度やり直すわけではなく、以下のようにScriptingフェーズから再度実行されます。
多くのレンダリングエンジンは既に用意されたオブジェクトを再利用しようとするため、また全てのDOMツリーを作り直したりCSSを当て直したりといったことはせず、部分的に実行する処理となります。ただ実施されたイベントによって再度実行される処理も変わってくるため、JavaScript上でどのようなコードが再レンダリングを引き起こすかといったことを把握しておくとパフォーマンス上優れたコードを意識できるかと思います。
終わりに
フロントエンドのパフォーマンスチューニングを考える上で前提知識となるブラウザの仕組みを紹介しました。
正直抽象構文木(AST)とかその辺りの理解はめちゃくちゃ曖昧です、、
書籍からは端折っている部分も多いのでもっと細かい情報を知りたい方は購入してみるといいかもしれません!
「Webフロントエンド ハイパフォーマンス チューニング」
「フロントエンドのパフォーマンスを考える」の続きはまた書いていこうかと思います ->
(次はおそらくこの記事を踏まえたチューニングの基礎編?)