今回のテーマ
Webパフォーマンスを改善する上で知っておきたい知識をまとめてみました。
前回の記事では使わなかった(使えなかった)技術や方法なども含めて記載します。
また、ブラウザのレンダリングなどについても書きたいと思います。
(2019年5月23日追記)
過去の記事はこちら
How to 速度改善 ー計測・知識編ー
How to 速度改善 ー原因調査編ー
How to 速度改善 ー実装&技術調査編1ー
1. ブラウザレンダリングの仕組み
推測するな、計測せよ
という言葉にあるように、闇雲にチューニングを初めても良い結果は出ません。まずはブラウザレンダリングの仕組みからみていきましょう。
ブラウザレンダリングの流れ
レンダリングの大まかな流れは
Loading→Scripting→Rendering→Painting(これでページが表示される)
となっています。
この処理の内容をフレームと言います。
また、それをもう少し分割すると、下記のようになります。
Loadingの工程では主に
・リソースのダウンロード
・リソースのパース
を行います。HTML,CSSなどをダウンロードして、DOMツリーやCSSOMツリーを作成します。
Scriptingの工程ではJavascriptの実行を行います。
ここでコードの内容を見て、字句解析→構文解析→コンパイル→実行
という流れでJavascriptを実行します。
割と早い段階でJavascriptがあるので、ここで大量の処理が走ってしまうと、後に控えている処理が進みません。
Renderingの工程ではスタイルの計算とレイアウト(配置)が行われます。
ここでは全てのDOM要素に対してCSSセレクタがマッチするかを総当たりで試行します。
総当たり、というところは結構ポイントでして、レイアウトツリーの構築の部分で詳しく触れます。
Paintingの工程ではラスタライズやレイヤーの合成が行われ、最終的なレンダリング結果が生成されます。ここで、ページ全体が見れるようになるということです。
2. RAIL
Googleが2015年の開発者向けのイベントGoogleI/Oにて発表したモバイル時代のパフォーマンス計測に対しての考え方です。
Response,Animation,Idle,Load
この4つの頭文字をとってRAILと呼ばれています。
こちらを参考に簡単に説明します。
Response
ユーザーによるボタンのクリック、フォーム上のコントロールの切り替え、アニメーションの開始などのユーザーの操作に対して100ミリ秒以内にレスポンスを返しましょう、という考え方です。
Animation
現在ある端末のほとんどは、1秒間に60回リフレッシュします。
(スクロールなどを含めて)アニメーションなどを実行している時はブラウザ側で端末のリフレッシュレートに合わせて、画面がリフレッシュされるたびに新しい画像またはフレームを表示する必要があります。
なので、1つのフレームに対して割り当てられる時間は1秒/60回 = 16ミリ秒くらいとなります。
(短い。。)
Idle
Idleはユーザー操作が行われるまでの待機時間(つまり、処理が何も行われていない)状態のことです。
WebページでJavascriptを使って何かをする場合にはメインスレッドにも処理が行えるように余裕を持たせることが推奨されています。Javascriptの処理が重すぎるとメインスレッドが詰まってしまうので、それを避けましょう、ということですね。
Load
Webページの読み込みについては、ユーザーがアクションを起こしてから1000ミリ秒(つまり1秒)以内にDOM生成を行う必要があります。
3. パフォーマンス計測ツールとか
推測するな、計測せよ、という言葉にあるように、Webのパフォーマンスの計測は必須です。どこに問題があるのかを分析し、その問題を解決することでユーザーが快適にアプリやサイトを使えるようになります。
Chrome DevTools
開発者なら誰もが開いたことあると思うので詳細は省きますが、command+alt+iキーで開いて出てくるアレですね。
パフォーマンス計測についてよく使われるのはNetworkタブ、Performanceタブ、Memoryタブの3つです。
Networkタブをクリックすると、ネットワークを通じて行ったリソース取得の結果を細かく見ることができます。かかった時間、ファイル名、どのタイミングで読み込まれたのかなどの情報を見ることができます。
図の青い線がDOMContentLoadedのタイミングになります。
ここでは個別のリソース取得時間を把握することもでき、Waterfallと記述のあるところに出ているバーにマウスを合わせると詳細を見ることができます。
Performanceタブをクリックするとページの読み込みや操作に関するイベントを調査することができます。
例えばこのサイトの場合はScriptingに時間がかかっていることがわかります。
また、下記の図のように、それぞれの処理内容が具体的にどうなっているのかも見ることができます。
Memoryタブをクリックするとメモリの利用状況を見ることができます。
メモリの使いすぎなどを把握することができます。
Navigation Timing API
パフォーマンス計測に使用できるデータを取得できるAPIです。
色々なプロパティが設定されていて
リダイレクトにかかる時間
ドメインの解決にかかる時間
HTTPリクエストの送信からHTTPレスポンスの受信までかかる時間
ドキュメントの解析時間
など計測できます。
上記の例を計測する場合は
var timing = performance.timing;
//リダイレクトにかかる時間
console.log(timing.redirectEnd - timing.redirectStart);
//ドメインの解決にかかる時間
console.log(timing.domainLookupEnd - timing.domainLookupStart);
//HTTPリクエストの送信からHTTPレスポンスの受信までかかる時間
console.log(timing.responseEnd - timing.requestStart);
//ドキュメントの解析時間
console.log(timing.domInteractive - timing.domLoading);
のようにします。
Resource Timing API
こちらは名前の通り、リソースの取得にかかっている時間を計測することができるAPIです。
Navtigation Timing APIと似ていますがその違いは、計測している値がナビゲーション開始時を起点とするミリ秒での値という点です。
//これでオブジェクトを取得
var entry = window.performance.getEntriesByType('resource');
//リダイレクトにかかった時間
console.log(entry.redirectEnd - entry.redirectStart);
//接続の確立にかかった時間
console.log(entry.connectEnd - entry.connectStart);
//ドメイン名解決にかかった時間
console.log(entry.domainLookupEnd - entry.domainLookupStart);
このようにJavascriptを用いても計測をすることができます。
その他APIについてはこちらに詳細にまとまっていますのでご確認ください。
New Relic Browser
パフォーマンスに関しての指標をダッシュボードでみれるようになるツールです。
こちらのサイトで詳細を見ることができます。
4. リソースの読み込み
前回の記事にも書いていますが、リソースの読み込みの最適化については下記の4つが重要です。
・リソースの大きさと数を減らす
・レンダリングブロックを減らす
・ブラウザとサーバーの間の遅延を減らす
・ブラウザキャッシュの活用
HTML/CSS/Javascriptの最小化や、画像サイズの最適化などを行うことでリソースの大きさと数を減らし、Javascriptのasync defer属性、CSSのpreloadなどを使ってレンダリングを妨げる要素を削除するなどの方法で対応していく必要があります。
また、Akamai,Fastly,CloudFrontなどのCDNを使ってリソースファイルを配信することで、1つのサーバーで配信するよりも負荷が分散され、かつ高速なリソース配信を実現することができます。
Lazyload
jQueryにはLazyloadというライブラリがあり、画像の遅延読み込みにはよく使われています。
画像の遅延読み込みもパフォーマンスに良い影響を与えますが、KVなどに使ってしまうと一瞬KVが表示されなくなってしまったりするので、使い所は考えた方が良さそうです。
また、Google I/O 2019ではChromeに限り、imgタグの属性に
<img src="hogehoge.jpg" loading="lazy" alt="hoge">
としてloading属性にlazyを指定すると遅延読み込みするようになりました。
今まではIntersection Observerを使って実装していたところをimgタグ一発で実装できるようになります。便利ですね。
5. HTTP/2
HTTP2はHTTP1.1の後継のプロトコルです。
分かりやすいところで言うと、リクエストとレスポンスの多重化というのがパフォーマンスに良い影響を及ぼします。
具体的には1つのTCP接続で複数のリクエストとレスポンスを扱うことができるようになり、複数のリソースを同時に取得することができます。それによってブラウザの同時接続数の制限が無くなります。
QUICプロトコル
Googleが開発している次世代のプロトコルです。
UDPをベースにして動いているので、普通に考えるとTLS(SSL)が使えないわけですが、そこを使えるようになればいいプロトコル作れるじゃん!といった感じの考え方で開発されているブラウザになります。
速度的にはTLS<UDPであるため、そこをベースに動かせるプロトコルを作ることで、HTTP/2よりも高速なプロトコルを作っているということですね。
6. レイアウトツリーの構築
上記のRenderingの工程においては、CSSスタイルの計算とレイアウト処理が行われます。先ほど総当たりでセレクタマッチングをするという話をしましたが、それについてまずは書きたいと思います。
CSSセレクタのマッチング
CSSスタイルの計算についてはCSSセレクタは右から左に向けて処理されます。
つまり
div > p {
color: red;
}
のようなCSSがあるとして、これはまず全てのpタグを探して、その上にdivタグを親要素にもつものを洗い出してから計算がされます。
なので、ページによってはものすごい量のpタグを探した上で、そこからdivタグを探しにいくような探し方をします。非効率ですね。。
なので、上記の場合であれば、pタグに対してクラスをつけて
.text {
color: red;
}
のようにしてあげると、class属性にtextを持っているものだけが検索されるようになるので、高速になります。
BEM
フロントの開発をしている人であれば耳にしたことはあるかと思いますが、BEMというCSSの設計規約があります。
BEMではドキュメントを構成する要素をBlock, Element, Modifierの3つに切り分けて考えて設計するという手法です。
BEM自体は前から存在する手法なので、それについては色々な記事があります。
BEMによるフロントエンドの設計
一番詳しいCSS設計規則BEMのマニュアル
ここで共有したかったのは、BEMの記法によりCSSセレクタのマッチングの速度が上がるということです。
先ほどの
div > p {
color: red;
}
というCSSの記述の部分について
pタグのclass属性がcontents_area__text--redと設定されているという前提で
.contents_area__text--red {
color: red;
}
とすることができます。これによって
・大量のpタグを検索しなくてもよくなる
・その上のdivタグも検索しなくてよくなる
といったメリットがあります。BEMの他のメリットとしてはCSSの管理が楽になることですが、こういった副次的な効果もあるんです。
なので新規でプロダクトを作っていく段階なのであればBEM記法をおすすめします。
UNCSS
CSSセレクタのマッチングの処理を上記のBEMで減らすことも大事ですが、プロジェクトが大きくなってくると使っていないCSSが出てくるので、そのCSSを減らすということも重要ですね。
とはいえ、大量のCSSをいちいち目視していたら1年くらいかかりそうなので、適用されていないCSSルールセットを検出してくれるツールを使ってみてもいいかもしれません。
使い方は割とシンプルなようで
Node.jsをインストールして
$ brew install node
$ node -v
uncssをインストールします
$ npm install -g uncss
そこから
$ uncss https://hoge.com > retain.css
とすると、実際に使っているCSSルールセットのみを書き出してくれます。
CCSS
Critical CSSの略で、ファーストビューの領域の表示を速くするためのCSSの記述方法の1つです。
ファーストビューについてのCSSはインラインで書いて、使わないCSSは遅延読み込みなどをしてレンダリングブロックをしないように調整するといった形になります。
日経新聞さんが先日のDeveloppers Summitでこの辺りを話されていたので資料を読んでみるのもいいかもしれません。
7. Service Worker
数年前から注目されているProgressive Web Appsという概念を支える技術の1つです。
ものすごくざっくりいうとWebのライフサイクルとは別のプロキシのようなものでして、リクエストを傍受してキャッシュを返したり、Push通知を送ったりすることができたりします。
キャッシュを返す仕組みをService Workerで実装することで、キャッシュヒットした場合はそれを返し、ヒットしない場合はApp Shellを返すようにすることで、First Paintが高速になったり、オフライン時でもそれなりにアプリ感が出せたりします。
実装方法についてはこの辺りを参考にされると良いかと思います。
PWA: ServiceWorkerを使って、キャッシュをコントロールする(オフラインハンドリング)
-15分PWAクッキング -オフラインで動くページを作ってみよう-
quicklink
日経さんの資料にも書かれていますが、こちらを最後に紹介します。
Idle時間中にViewport内のリンクをprefetchすることで後続のページロードを高速化するというもので、Intersection Observerを利用しているようです。
Intersection Observerとは、特定のDOM要素が画面内に入っているかどうか、さらにその位置までも容易に得ることができるのAPIで、それを使って画面領域外のコンテンツを、その領域に近づいたら読み込む、といったことができるようになります。
現時点ではIEやSafariで使えないので、Poryfillが必要になります。
以上、パフォーマンスチューニングをする上で必要な知識の共有でした。