🛰️

Lazyloadの実装方法の違いによるネットワーク負荷を検証した

2021/11/07に公開

こんにちは。とあるフロントエンドエンジニアです。

同一ページに大量に画像を要するサイトの構築や、高解像度の画像をいくつも埋め込むサイトの場合、コーディングの際に画像の遅延読み込み、いわゆるLazyloadを実装しておくとSEO的にもUX的にも有益であることは有名です。

現在、Lazyloadの実装には様々なパターンがありますが、それぞれ裏側 (ネットワークのリクエスト面)ではどのような違いがあるのだろうと気になったので検証してみました。

検証実験

実験概要

今回は割と新しめのHTMLの属性であるloading属性を使用した実装方法とjsのIntersection Observer APIを使用した実装方法を比較してみました。

またその中でもimg要素にwidth・height属性を指定し、レイアウトシフトの抑制処理をしたケースとそうでないケースなど、いろいろ試してみました。

比較はchromeの開発者ツールのNetworkタブとLighthouseタブから得られるデータを参照しました。

ちなみにloading属性の各ブラウザの対応状況は以下の通り。
https://caniuse.com/?search=loading

safariでは(11/2021時点で)まだ対応していないので、今すぐではなく近い将来どのような実装方法がいいのだろうか?という疑問の検証だと思って読み進めてください。

あくまで参考までに。

Width, Height属性の指定によるレイアウトシフト抑制

SEO対策では欠かせないポイントになってきているCLS(Cumulative Layout Shift)。

レイアウトシフトとは簡単にいうと画像のような要素の表示領域を確保していないが為に起こるレイアウトのズレです。

この記事では詳しくは解説しないので以下のサイトなどを参照してみてください。
https://seopack.jp/seoblog/img_width_height/

もうちょい具体的にいうと、ある領域に予め300px × 300pxの画像が入りますよ〜とわかっていたらブラウザは事前にその領域を確保します。もしその画像サイズが伝わっていなければブラウザは土壇場でその表示領域を確保することになるので、結果的にレイアウトがズレるという現象が発生します。

現実で言い換えてみると、会議室を使うのに、予め「10人で予約します。」と言えば会議室を提供する側は10人が入る会議室を事前に確保しますが、当日建物に行って「これから10人入ります。」と言うのでは会議室を提供する側からしたらどっちが負担ですか?というお話です。

レイアウトシフトも結果的にこのように負担が生じるのでネットワーク負荷へと繋がります。

今回はこれらの抑制処理も含めて今後どうlazyloadを実装していくべきかを検証しました。

実験環境

検証ブラウザはGoogle Chromeを使用。

  • Settings
    • Disabled cache : true
    • Network Throttling Profile : Fast 3G
  • Server
    • Localhostサーバ
  • Machine
    • OS : MacOS
    • CPU : Intel core i5
    • メモリ : 8GB

制約条件

  • pugで画像を100回ループさせて表示。
template.pug
ul
  - for (let i = 0; i < 100; i++)
    li.item
      img(src=`./assets/img/sample.jpg?${i}`, alt="")
  • img要素には必ずsrc属性とalt属性を入れる(何も入れなくても一応設定する)。
  • 画像にはクエリパラメータをつけてキャッシュで再読み込みをさせないようにする。
  • ファーストビューは画像が1.5枚(2枚目が半分見切れる)程度映る状態(iphone X想定)で実施。

実験パターン

今回は以下の6パターンを検証しました。

  • Vanilla(何もしないケース)
index.pug
img(src=`./assets/img/sample.jpg?${i}`, alt="")
  • Width, Height属性によるレイアウトシフト抑制処理のみ
index.pug
img(src=`./assets/img/sample.jpg?${i}`, alt="", width="640", height="800")
  • loading属性のみ
index.pug
img(src=`./assets/img/sample.jpg?${i}`, alt="", loading="lazy")
  • loading属性 + Width, Height属性によるレイアウトシフト抑制処理
index.pug
img(src=`./assets/img/sample.jpg?${i}`, alt="", loading="lazy", width="640", height="800")
  • js Intersection Observer API
    使用したライブラリは下記サイトで提供されているCDNから使用。内部的にはIntersection Observer APIを使用している。

https://appelsiini.net/projects/lazyload/

index.pug
img(src="", class="lazyload", alt="", data-src=`./assets/img/sample.jpg?${i}`, width="640", height="800")
  • loading属性 + Width, Height属性によるレイアウトシフト抑制処理 + WebP画像
    軽量なことから最近出てきたWebP。普及にはまだ時間がかかるがこれとlazyloadを組み合わせたら他と比較してどの程度変化があるのか見てみたかったので検証しました。
index.pug
img(src=`./assets/img/sample.webp?${i}`, alt="", loading="lazy", width="640", height="800")

データ概要

使用したjpgのデータ概要

  • 1種類のみ使用
  • 640px × 800px
  • サイズ : 117KB

使用したwebpのデータ概要

  • 上記jpgデータをwebpに変換したものを使用
  • サイズ : 52KB

では本題に入ります。

ネットワーク負荷の違い

計測結果

今回の6ケースにおける計測結果は以下。

  • Case No.
    1. Vanilla
    2. Width, Height属性によるレイアウトシフト抑制処理のみ
    3. loading属性のみ
    4. loading属性 + Width, Height属性によるレイアウトシフト抑制処理
    5. js Intersection Observer API
    6. loading属性 + Width, Height属性によるレイアウトシフト抑制処理 + WebP画像
Case No. requests Finish DOMContentLoaded Load
1 100 1.2min 2.06s 1.1min
2 100 1.2min 1.96s 1.1min
3 100 1.1min 1.97s 6.25s
4 6 6.92s 1.96s 6.24s
5 2 4.41s 1.88s 3.81s
6 6 3.67s 1.85s 3.07s

※requestsは画像のリクエスト数のみ

遅延読み込みを実装した場合(ケース3以降)、Loadまでの時間を圧倒的に短縮できていることが顕著にわかりました。遅延読み込みの実装って大事なんですねという再確認。

この差を考慮すると、JavaScriptでonloadなどのイベントハンドラを使用した処理には大きく影響が出そうですね。

特にケース5のIntersection Observerを用いた手法では3秒台と、Vanillaと比較すると凄まじいくらいにLoadの短縮ができていますね。

DOM自体は実装方法によって大きな差はないので変位もないのは当然ですね。

次項でリクエスト数とフローをみてこのLoad時間の差を詳細にみていきます。

リクエストフロー

次に、各ファイルがどのようにリクエストされているのかを確認してみました。
Networkタブの一番右のグラフ部分、Waterfallタブに注目してリクエストのフローを詳細にみていきます。

1. Vanilla

一度に6つ以上のリクエストは同時に処理できないということもあり、img0~img4までの5枚はほぼ同時にリクエスト、ロードされています。

その後は通信状況によって取得する順番がまちまちになっているという感じでした。
この理由としては、ブラウザ内でファイルのロードの優先度を意味するPriorityが明確に定められていない為にバラバラになっているのだと推察しています。

ですがある一定数ロードするとそこからは順々にリクエスト、ロードされていました。不思議。

2. Width, Height属性によるレイアウトシフト抑制処理のみ

興味深いことに、レイアウトシフト抑制処理を加えるとケース1と異なり、imgが1枚目から100枚目まで順々にロードされるようになりました。

レイアウトシフト抑制処理がロードのpriorityに影響しているということがわかりました。

これによってページを閲覧しているユーザからは、例えimgの読み込みが間に合わなくても上から順々に表示されていくように見えるというわけですね。

3. loading属性のみ

HTMLのloading="lazy"を使用して実装すると、前項の通りload自体は6秒ほどで終わっているのですが、裏では100枚目までリクエストが走っていました。

確かに6枚目のimgからはページのload(赤い縦直線)後にリクエストが行われていることがわかります。

4. loading属性 + Width, Height属性によるレイアウトシフト抑制処理

loading="lazy"にレイアウトシフト抑制処理を加えると、imgのリクエストは6枚で止まっていました。そしてページをスクロールするとその都度imgのリクエストを送る挙動になっていました。

この初めに読み込まれた6枚は同時に処理できる5つのリクエスト+1枚の遅延リクエストといった感じになりますね。

必要な分だけリクエストを送る挙動というまさしくこれがLazyloadというような挙動ですね。これよりloading="lazy"を使うときはwidth, height属性も指定してあげると、loading属性単体の時よりもさらにブラウザファーストな遅延読み込みを実装できるということですね。

5. js Intersection Observer API

Intersection Observer APIを使用して実装すると、ファーストビューの表示に必要な最低枚数2枚のみリクエスト、ロードしてスクロールしたら都度必要な枚数読み込むという形式になっていました。

本当にミニマムなリクエストで制御するという仕様ということですね。

Intersection Observer API自体が「画像のトップが任意に指定したポイント(ビューの下部付近)と交差したら...」というアルゴリズムになっているのでまぁ当然と言えば当然なのですが、改めて確認してみると面白いですね。

6. loading属性 + Width, Height属性によるレイアウトシフト抑制処理 + WebP画像

本ケースはケース4のimgの拡張子のみを変更した実験になるので、この辺のフローに大きな変化はありませんね。

Lighthouse

次に、LighthouseのPerformanceとその詳細を比較します。

  • Case No.
    1. Vanilla
    2. Width, Height属性によるレイアウトシフト抑制処理のみ
    3. loading属性のみ
    4. loading属性 + Width, Height属性によるレイアウトシフト抑制処理
    5. js Intersection Observer API
    6. loading属性 + Width, Height属性によるレイアウトシフト抑制処理 + WebP画像
Case No. Performance
1 33
2 67
3 39
4 73
5 85
6 94

ケース1とケース3を比較すると、loading属性を付与しただけでは特にPerformanceに大きな差がないことがわかります。
これはloading属性のみでは画像のリクエスト数は変わらないという点が得点が変わらないことと関連していそうですね。

そしてケース1,3とケース2を比較すると、img要素にwidth, height属性を加えるだけでperformanceの得点にかなり大きな差があることがわかりました。
SEO対策としては各画像にこれらの属性をつけたほうが望ましいということですね。

ケース4,5から、lazyloadの実装の違いという面ではloading属性を用いた実装よりも、Intersection Observer APIを用いて実装したほうが得点が高いことがわかりました。(おそらくリクエスト枚数の差が大きいと推察していますが。)

ケース4,6の通り、画像はやっぱり軽ければ軽いほうがブラウザ的にはいいということですね笑
ただすべてのサイトにWebPを使用したほうが望ましいかと言えばまた別の話になるので、構築するサイトの趣旨、用途、要件によってWebPを使用するか否かを見極めていきましょう。

Case No. First Contentful Paint Time to Interactive Speed Index Total Blocking Time Largest Contentful Paint Cumulative Layout Shift
1 3.3s 34.9s 3.3s 420ms 18.7s 0.971
2 3.0s 34.8s 3.0s 400ms 3.3s 0
3 1.7s 12.9s 1.7s 470ms 20.4s 0.971
4 1.9s 4.5s 1.9s 450ms 4.1s 0
5 1.6s 3.6s 1.6s 170ms 3.8s 0
6 0.9s 3.2s 0.9s 140ms 2.9s 0

ケース1,2とケース3~6のFirst Contentful Paintを比較しても分かるとおり、ファーストビューの表示に遅延読み込みの実装がかなり効いていることがわかります。
ファーストビューの表示までの速度はSEOの重要な指標になるので侮れないですね。

そしてPerformanceの差に影響しているポイントの一つとしてあげられる、CLS(Cumulative Layout Shift)。こちらも再確認にはなりますが、width, height属性をつけるだけでしっかりとレイアウトシフトを抑制できていることがわかります。

またPerformanceにおいて高得点を出したケース5,6は他のケースよりもTotal Blocking Timeが大幅に短いことも得点アップに寄与しています。ユーザが入力操作ができるまでの時間がかなり早いということになりますね。

まとめ

こうしてNetworkのリクエストまで見てみると実装方法によって大きな違いが出ているというのは非常に面白いですね。

実験結果をまとめると、

  • loading="lazy"やるならwidth, heightも設定する。
  • リクエスト数最小を目指すならIntersection Observer API。
  • やっぱりWebPは軽い。

以上。

Discussion