コンテンツにスキップする

「超速!Webページ速度改善ガイド」を読んだ

投稿時刻2024年1月28日 11:23

超速! Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる」を 2,024 年 01 月 28 日に読んだ。

目次

メモ

推測するな、計測せよ p16

ソフトウェア開発の現場では、しばしば「Measure, Don't Guess」(推測するな、計測せよ)という教訓が用いられます。
Webページの速度改善においても、この教訓が非常に当てはまります。

RAILモデル アプリケーションの各タイミングにおける応答時間の基準 p21

RAIL モデルは、 Web ページの速度の重要性について啓蒙する Google のエヴァンジェリストたちによって紹介されているモデルです。
これは Response、Animation、Idle、Load の頭文字をとったもので、それぞれのタイミングの応答時間について表 1.2 の目標値を掲げています。
表1.1 と一部共通する目標値としては、「遅延を伴うが一連のナビゲーションが間断なく進んでいると感じられる限界」が Load に相当し、「瞬時に応答があって自分が UI を直接コントロールしていると感じられる限界」は Response に相当します。

データの転送距離を短くする p42

転送距離は、通信するデバイスと実際にデータをやりとりしているサーバの物理的な距離を指します。
データの転送距離が短いほどラウンドトリップタイムが短くなります。
具体的には、日本国内のサーバへのアクセスのラウンドトリップタイムはおよそ100ミリ秒であるのに対し、日本からアメリカ西海岸へは200~300ミリ秒程度かかります。
これに関してはフロントエンドだけでは対処できませんが、よりシビアな性能要件を満たす場合は無視できない要素です。

クリティカルレンダリングパス p44

Webページがロードされるときのブラウザの内部処理を理解するうえで重要なモデルに、「クリティカルレンダリングパス」があります (図2.6)。
Webページの最初のレンダリング処理が行われるまでに必要な一連の処理であり、これが最適化できればユーザーがURLを開いてからコンテンツが見えるようになるまでのページロードの速度改善につながります。

このクリティカルレンダリングパスという用語は、レンダリング処理をのものを最適化するというよりも、ページロード時にレンダリング処理に至るまでの時間を最適化するという文脈で使われることに注意してください。

1 HTML ドキュメントのダウンロードと評価
2 サブリソースのダウンロードと評価
3 レンダーツリーの構築とレンダリング

ページロードに関わるブラウザイベント p68

ブラウザのページロードには、 HTML ドキュメントのロード完了 (DOMContentLoaded) とサブリソースのダウンロードと評価も含めたページロード完了 (load) の2つのイベントが含まれます。
ナビゲーションを開始してから、これらのイベントが発生するまでの所要時間は指標の一つになり得ます。

DOMContentLoadedイベントはブラウザによるHTMLドキュメントのロード完了を意味しますが、途中で発生する CSS や画像のようなサブリソースのロードを持ちません。
これに対し load イベントはページを構成するすべての要素がそろったことを意味します。
2.2節でクリティカルレンダリングパスについて説明したとおり、ページのレンダリング開始はリソースがいかに早くそろうかが左右します。
loadイベントはWebサイトのネットワーク性能を表す大まかな指標として、 DOMContentLoaded イベントとの差分はサブリソースによって発生するオーバーヘッドとしてとらえてください。

スタイリングされずに表示されるコンテンツ p98

一部のCSSを非同期でロードするなどして CSSOM ツリーが段階的に準備されると、ブラウザは完成しているレンダーツリーからレンダリングし始めるため、スタイルが適用されていないコンテンツが表示される場合があります。
この事象を FOUC (Flash of Unstyled Content) と言います。

フォントの切り替えによるチラつき p105

Webフォントが適用されたテキストのレンダリングを行う過程は、ブラウザによって異なります。
Chrome、Firefox、Safari、Edgeは、ダウンロードが完了するまでレンダリングを保留し、完了したタイミングでレンダリングします。
Internet Explorerは、ダウンロード完了まで即座に代替フォントでレンダリングし、ダウンロードが完了したらそのフォントで再レンダリングします。
このフォントを適用するタイミングで再レンダリングが発生することを FOUT (Flash of Unstyled Text) と呼びます。

FOUTについては賛否両論あり、チラつきが気になる人もいれば、代替フォントで早く表示されているべきという意見もあります。
いずれにせよ、フォントのロード処理の最適化によって最小化するべき要素であることには変わりません。
WebページにおいてFOUTが発生していないかどうかもチェックしてみてください。

動きの滑らかさ 1フレームあたり10ミリ秒以内 p112

スクロール操作やアニメーションが実行されたときに、ブラウザの中では表示内容を更新するためにレンダリング処理が発生します。
スクロール操作は、PC、モバイルを問わず頻繁に発生します。
アニメーションも、UIの応答を表現するためのCSSアニメーションなど、最近のWebページでは随所で使われるようになっています。
これらすべての動きが滑らかであることがスムーズなUIの基本です。

動きの滑らかさは1.4節で紹介したRAILモデルにおける Animation に該当し、1フレームあたりの処理は10ミリ秒以内に収めることが理想です。
アニメーションとは言いますが、画面のレンダリング処理すべてに共通します。
レンダリング処理によって画面がどれだけ滑らかに動いているかは、 FPS (Frames Per Second) という単位で測ることができます。
FPSとレンダリング処理の関係は、次節で詳しく説明します。

UIの応答速度 100ミリ秒以内 p113

アクションに対するUIによる応答の典型例としては、マウスカーソルを <a> 要素の上に持っていくと、 :hover 疑似クラスによってテキストに下線が付いたり色が変化したりするといったエフェクトが挙げられます。
ほかにも、ボタンをクリックしたら押下したような見た目に変化したり、通信処理などでユーザーを待たせるときはインジケータを表示したりなど、ユーザーが自身の操作の連続性を見失わないようUIがすばやく何らかの応答を示すことが重要です。

UIの応答速度はRAILモデルにおける Response に該当し、ユーザーアクションが発生してから100ミリ秒以内に応答を返すことが理想です。
UIの応答速度については、ビジュアル的によほどリッチな表現をしなければレンダリング処理自体がボトルネックになることはあまりありません。
しかしUIの応答として多量のレンダリング処理を必要とする表現をしたり、
パララックススクロールのようにscroll、touchmove、mouseoverなどの高頻度で発生するイベントをトリガとした処理をしたりすると、応答速度どころか画面全体のレンダリング処理を阻害してしまう可能性はあります。

スクリプトの処理 p118

レンダリング処理に関わる JavaScript の役割としては、 HTML 文字列のテンプレート処理や、イベントの発生をとらえて表示要素の状態を変化させる処理などがあります。
scroll や mousemove のように高頻度で発生するイベントにおいて、 DOM の操作や更新をするような処理や、計算量を必要とする処理が発生すると、レンダリング処理に大きな影響を与えることがあります。

スクリプト処理中はメインスレッドが占有され、ほかの処理は行われません。
極端な例ですが、特定の重い処理に1,000ミリ秒かかるとすれば、その1,000ミリ秒の間は、レンダリングなどのほかの処理も停止してしまいます。
React に代表される VirtualDOM の実装は有用ですが、更新対象の構造体が大きくなるにつれて相応の計算量が必要になりますし、高頻度で発生すればメインスレッドを占有してUIのスムーズさを損なうことがあります。

will-change プロパティによるCompositingの有効化 p126

CSS Animations や position: fixed のようにブラウザが自動で Compositing を適用するスタイルもありますが、確実に適用したい場合は開発者が任意の要素に Compositing が有効になるような記述を加える必要があります。

will-change プロパティは、ほかのプロパティに変更が生じる可能性があることをあらかじめブラウザに伝え、 Compositing をはじめとした最適化の準備を促します。
先に紹介したCSSハックよりも、ブラウザにGPUアクセラレーションに関する予測と最適化を適切な方法で促すことができるプロパティです。

CSSハックによる Compositing の有効化 p128

Compositing を有効化する方法として、 will-change プロパティが登場する前からChromeやSafari、Android Browserを対象として使われているのは、transform: translateZ(0) のような指定です。
これはtransformプロパティに指定できる関数のうち関数名の末尾にZや3dが付くものは、Z軸の操作を前提とした3D操作になるため Compositing が有効になることを利用したCSSハックです。
実際にZ軸の操作を必要としなくても0を指定すれば見た目は変更されないので、このような指定が使われます。

メモリリークを回避し、メモリを節約する p172

詳細は後述しますが、メモリリークがある状態は望ましくありません。
Webの場合はページを遷移するごとにメモリが解放されるため、問題にならないこともあります。
しかし、SPAのようにメモリを解放しないまま同一プロセスで動作し続ける場合は確実に問題になるうえに、SPAのようにJavaScriptによる実装の複雑性が高いほどメモリリークは発生しやすいので注意が必要です。

また、メモリリークがなくても、メモリを使いすぎてWebページの動作に支障が生じているようであれば、メモリの使用量自体を節約することも検討しなければならないでしょう。

GCによって解放されないメモリ p173

JavaScriptのメモリはJavaScriptエンジンによって自動で管理されます。
実行時に自動でメモリが確保され、必要なくなると自動で解放されますが、JavaScriptの記述によっては解放されずにメモリリークしてしまうことがあります。
これはメモリ容量の低下を招き、実行速度の低下はもちろん、ブラウザのクラッシュまで引き起こす可能性があります。

throttle(), debounce () 関数による実行間隔の間引き p190

scrollやresize、keydownなどの短い時間で大量に発生するイベントや、間隔の短いタイマー内で重い処理を実行しているケースにも注意が必要です。
特にDOMの操作を伴う場合は、画面のチラつきやレイアウト処理を招いていることも多いでしょう。

まずは、その処理を高頻度に実行する必要があるかどうかを見なおしてください。
間引いても差し支えなければ、throttle()やdebounce()関数で実行間隔を調整できます。
throttle()関数は連続する処理を一定時間のうち実行を一度までに抑え、debounce()関数は連続している処理が終了してから一定時間が経つと一度だけ実行します(図7.3)。
これらを実装しているライブラリとしてはLodashが代表的です。
throttle() と debounce () 関数を使った処理の間引き

import { throttle, debounce } from 'lodash';

const textarea = document.querySelector('textarea');
textarea.addEventListener('input', throttle(() => {
	// 連続してinputイベントが発生しても
	// 100ミリ秒の間に一度まで実行頻度が調整される
}, 100));

window.addEventListener('scroll', debounce(() => {
	// scrollイベントが発生しなくなってから
	// 200ミリ秒経つと一度だけ実行される
}, 200));

オブジェクトへの参照の消去 p199

DOMツリーを参照しているオブジェクトを消去することで、Detached DOM treeをGCの回収対象とさせます。
先のコード例では window.detached という変数からDOMツリーを参照していることが原因ですので、 detached = null; としてやることで、GCの回収対象となります。
実際にConsoleパネルで実行して再度ヒープのスナップショットを撮り、比較してみてください。

このように一度変数に代入しておくなど、参照が発生するとDOMツリーから削除しても、インスタンスが残るケースがあります。
ヒープのスナップショットを確認し、Detached DOM treeが残っていないかチェックしましょう。

ベースライン画像上部からレンダリング p211

ベースラインは、画像データを1つのブロックに保持する標準的な形式です。
ベースライン形式の画像はロードが進むにつれて、画像の上部から段階的にレンダリングが始まります。

ベースライン形式の画像を<img>要素の縦幅と横幅を指定しないで表示してしまうと、ブラウザはファイルを完全にロードするまで横幅と縦幅がわからないため、ロードに応じて<img>要素のサイズ更新を繰り返します。
これによってWebページのレイアウト算出が断続的に行われ、Webページ表示時のガタつきを引き起こします。

<img>要素の width と height 属性か、 CSS の width と height プロパティ で縦幅と横幅を指定していれば、画像のロード状況にかかわらず<img>要素のサイズが固定されるので、画像のロードに起因するWebページ表示時のガタつきを防げます。

Prerenderによる事前レンダリング p253

Prefetchでリソースを取得しても、それらを評価してレンダリングするには別途処理が必要です。
ナビゲーション先のリソースをすべて取得し、レンダリング処理まであらかじめやっておくことで、ページ遷移そのものを高速化してしまおうというのがPrerenderです。

Prerender は <link rel="prerender">のように宣言し、ブラウザはhref属性で指定されたURLのコンテンツを事前にロードしレンダリングします。
対象ページのサブリソースのロードから、レイアウトやレンダリングといったページロードにおける一連の処理を行います。
現在のページにいながら対象ページがバックグラウンドでレンダリングされるため、ナビゲーション時のレンダリングはとても高速に行われます。

Prerenderを使った投機的なページレンダリング
<link rel="prerender" href="https://example.com/next.html">

ユーザー体験については効果的な反面、ほかの Resource Hints に比べて高コストであり、ブラウザへの負担は小さくありません。
そのため、ログインページやキャンペーンページの遷移先のような高い確率で発生するナビゲーションを選ぶなど、慎重に取り扱うべきです。