画像表示のパフォーマンス改善方法
問題
画像(img または iframe)が含まれるページは、その画像をダウンロードして表示する必要があり、その表示タイミングによってはページ全体の表示が遅れてしまう。
いつまでも白いページが表示されている。
何度も再レイアウトが発生して、いつまでもまともに表示されない。
見かけ上ページは表示されても、その後うんともすんともになる。
なぜページの表示(あるいは動作)が遅くなるのか?
画像が多すぎる。
画像データが大きすぎる。
画像のダウンロードに時間が掛かる。
画像のデコードに時間が掛かる。
画像の処理を待つ必要がある。
様々な対策方法(歴史的経緯。善し悪しがある。)
そもそも画像を使う必要があるか考える
必要なのはただの記号でしかないのでは?
画像形式が適切か確認する
PNG 形式にした方が効率的な画像と、JPEG 形式にした方が効率的な画像とがある。
写真や自然画のような画像、グラデーション的な画像の場合は JPEG 形式が適当。
ドット絵的な画像、色が少ない、色の区分けがハッキリしている画像、の場合は PNG 形式が適当。
WebPやAVIFといった最新の画像フォーマットを使用する。
画像そのもののデータサイズを減らす
カメラなどで撮った画像にはノイズが含まれていて、それが画像のファイルサイズが大きくなる原因になっている。(「画像が重い」の多くのケースがこれ)
人間の目には分からない程度のノイズを減らすことで画像のファイルサイズを小さくすることができる。
また、JPEG 等では圧縮率を上げることができる。画質は悪くなるが、ファイルサイズを小さくすることができる。
サムネイルを使う。
サムネイル(thumbneil)とは(主に多数の画像を表示して選択できるようにするため)縮小された画像のこと。
ほとんどの場合、詳細な画像を見たいわけではなく、大まかなイメージが分かれば良い。
サムネイルと元の画像の2本立てにして、サムネイルをクリックしたら元の画像が表示されるようにする。
レスポンシブ画像
ブラウザの解像度に応じた画像を使用する
高い解像度のブラウザでは高い解像度の画像を、低い解像度のブラウザでは低い解像度の画像を表示する。
img タグに width と height を付ける。
HTMLはフロー型のレイアウトシステムで、中の要素のサイズが変わる度に再レイアウトが必要になっている。
このため、画像をダウンロードして、その画像サイズが確定するまでは「デフォルトの画像サイズ」もしくは「サイズ0」として仮埋めしてレイアウトしてしまう。そして、画像のサイズが確定した時点で再レイアウトが発生する。
これを避けるために、width と height を付ける。
現代では、CSSで画面サイズに対する割合等を付けるのが妥当。
プログレッシブ表示
精細な画像には大量のデータが必要になるが、モザイクのような低解像度のデータならば小容量で済む。全体のダウンロードが完了するまでの間、低解像度の画像を表示して、次第に高解像度に上げていくのが「プログレッシブ表示」
JPEG にはプログレッシブ方式が存在する。
JavaScript で力業で切り替える方法がある。(裏でロードが完了したら差し替える。)
投機的読み込みを使う
早く読み込みたいものをブラウザに教える。
遅延読み込みを使う
後から読み込みたいものをブラウザに教える。
loading="lazy"
画像のダウンロードにはとても時間が掛かる。画像をダウンロードして表示しないと画面全体のロードが完了したことにならないため、JavaScript 等の動作が遅れることがある。一般的に、ビューポートに表示されていない画像をわざわざ待つ必要性はないのに、待ってしまう。
img タグに loading="lazy" を付けることで、後からダウンロードしても構わないことをブラウザに教える。
onload が発火するのは(本来は)画像がロードされた後だが、loading="lazy" を付けると、その画像を読まなくてもロードが完了したとみなされる。
Chrome はデフォルトの挙動が loading="auto" になっていて、onload の発火はもはや信用ならない。
decoding="async"
画像のデコード処理を非同期で行うという指示。
画像のデコード処理には時間が掛かる。このため、画像を表示する時にデコードが完了するまで待つとその後の処理が遅れる。
これを避けるためにデコード処理を非同期に(別スレッドなどに回して)処理して構わないことをブラウザに教える。
プレースホルダ表示(遅延ロード)
画面を表示した時の画像の読み込みは諦めて、最初は仮画像(あるいは似たような色の矩形)を表示する。JavaScript で後から差し替える。
見える範囲だけ画像をロードする
JavaScript で現在のスクロール位置を把握して、ピューポートに表示されている部分だけロードする。
Intersection Observer APIを使用することができる。
複数の画像を1つにまとめる。
画像の情報量はほとんど変わらないが、リクエスト回数を減らす効果がある。
キャッシュの利用
通常、一度ダウンロードされたものはキャッシュに残り、再利用される。
動的なページでは、このキャッシュがうまく効いていなくて、毎回ダウンロードされていることがある。
再利用されやすいように HTTP ヘッダを設定する。
CDN(コンテンツデリバリネットワーク)を利用する。
多数の地域で使われる場合、その地域毎にあるCDNでホストすればネットワーク帯域を最適化できる。
画像を仮表示する方法では、どの方法を採っても、処理が完了するまでは仮表示することになるため、どうしてもちらつくことがある。
(ちらつかない方法は本質的に存在しない。)
参考
早く読み込みたいものを指定する
後から読み込みたいものを指定する
Chromeのloading属性は、指定しなくてもデフォルトで"lazy"の挙動となりうる ~loading属性の落とし穴から学ぶ、機能拡張と初期値・デフォルト挙動の話~
decoding="async" について詳しく調べてみる