仕様と実装から読み解くHTMLのloading属性
こんにちは。ニコニコQセクションのHajime-san(GitHub)です。
本稿では、HTMLのloading属性の仕様とブラウザの実装を解いていきます。これにより、ブラウザからのコンテンツ配信の最適化を支える判断材料の1つとなれば幸いです。
執筆の動機
筆者が開発に携わっているサービスであるニコニコオーディションが間接的に利用している社内ファミリーサービスについて、ふと自分のスマートフォンで見てみたところページの初期表示に時間がかかるように感じました。 オーディションシステムはiframe要素によって社内ファミリーサービスなどに専用のUIを埋め込むことが可能になっており、iframe要素のloading属性を用いた「遅延読み込み」(以下、遅延読み込みはloading属性によるもの)が効果的なのではないかと考えました。
まずは、実際にloading="lazy"
を適用して読み込みが後回しにされるのかと確認しようとしたところ、遅延読み込みでない即時の読み込みとなり、直感に反するように思えたことが執筆の動機です。
遅延読み込みの歴史
本稿で取り扱うloading属性は、今日のモダンブラウザに実装されているものですが、その歴史はサードパーティーによるスクリプト実装が由来です。
古くはjQueryプラグインなどで提供され、その内部実装がscrollイベントを監視するものから、Intersection Observer API
による対象要素とビューポートの交差を監視する実装への移り変わりを経るなどがありました。
その後、ブラウザでloading属性によりネイティブに遅延読み込みがサポートされるに至ったという経緯があります。
遅延読み込みがLCP高速化に及ぼしうる可能性について
HTMLのimg要素に代表されるサブリソースは、詳細を簡略して記述すると、ブラウザがHTMLをパース中に当該サブリソースへHTTPリクエストを発行[1]し、レンダリングまでを行うことを通常のフローと仮定します。
この時、loading="lazy"
と指定することによる遅延読み込みは、前述のフローの実行に必要なクライアントの計算資源が節約可能になります。
これにより、Largest Contentful Paint (以下、LCP)を高速化できる可能性があります。
仕様
遅延読み込みについて、まずはHTMLの仕様から見ていきます。 簡潔に書かれており物量も多くないものの、その中で下記に重要な点を掻い摘んで取り上げます。
- loading属性の値は
lazy
とeager
の2つで、eager
はデフォルト値になっている - 要素の交差の監視にはJavaScriptに提供しているIntersection Observer APIをブラウザが内部で実行する
Intersection Observer API
には、rootMargin
というビューポートから対象の要素への交差判定に加算される矩形領域をオプションとして指定できる引数がある
この値はブラウザの実装依存となり、フィンガープリンティングとならないような判断材料、例えばネットワークレイテンシーなどを判別してrootMargin
の値を指定できる
各ブラウザにおける実装
先述の仕様について、rootMargin
の値はブラウザの実装に依存することが、まさに冒頭の直感に反する挙動を引き起こしていたようです。この実装について把握すべく、モダンブラウザのレンダリングエンジンのソースコードを見ていきます。
Blink
要素名 | rootMargin | 主たる実装へのリンク | 備考 |
---|---|---|---|
img | ネットワークレイテンシーに応じて1250px から8000px の間で5段階に変容 |
settings.json5 lazy_load_image_observer.cc |
Browser-level image lazy loading for the web |
iframe | ネットワークレイテンシーに応じて2500px から8000px の間で5段階に変容 |
settings.json5 lazy_load_frame_observer.cc |
It’s time to lazy-load offscreen iframes! |
WebKit
要素名 | rootMargin | 主たる実装へのリンク | 備考 |
---|---|---|---|
img | 100% px単位では window.innerHeight と同等 |
LazyLoadImageObserver.cpp | |
iframe | なし | LazyLoadFrameObserver.cpp | WebKitにはQuirksという互換性を維持するための内部実装がある 既存Webサイトのiframeの遅延読み込みに影響が出たことへの対策の例 |
Gecko
要素名 | rootMargin | 主たる実装へのリンク | 備考 |
---|---|---|---|
img | 600px |
StaticPrefList.yaml DOMIntersectionObserver.cpp |
|
iframe | - | - | 実装は検討段階 |
実装まとめ
各ブラウザが対象要素の遅延読み込み判定に用いるIntersection Observer API
のrootMargin
の値は、三者三様になっています。
また、遅延読み込みの処理自体にもブラウザに僅かながらオーバヘッドが発生することを見て取れます。
実装からねらいを推測する
img要素に関して、どのブラウザ実装にも共通することは、loading="lazy"
として配置されている場合は、 rootMargin
に正の値が指定されています。
すなわち、ビューポートの下端より下の位置から読み込みを開始するように実装されているということです。
また、iframe要素に関してその特性に触れておきます。
iframe要素は指定されたURLをドキュメント内に配置する要素であり、iframe要素から更にサブリソースの読み込みが発生するケースは多いです。
これにより、iframe要素は通常、表示に時間のかかる可能性が高い要素と言えます。
Blinkの実装は、クライアントのネットワークレイテンシーを判別して少し早い段階から読み込みを開始し、
ビューポートに入った際には画像の表示を終えている状態にしたいという意図がこちらに書かれてあります。
iframe要素の場合はimg要素より大きいrootMargin
の値の設定が設定されているのが見受けられ、まさに先述のiframe要素の特性を考慮した実装でないかと考えられます。
WebKitのiframe要素の実装では特にrootMargin
の値が設定されておらず、ビューポートの下端は対象のiframe要素と交差したら読み込みが開始される実装になっています。
明確な意図が記述された文書を見つけられなかったものの、これはiframe要素により発生するサブリソースの読み込みを、可能な限り抑制しようとしている、と受け取ることも不可能ではないと言えます。
Geckoではimg要素のrootMargin
の値を幾度か検証[3]した後に、最終的に600px
が適当であるという着地点に落ち着いたようです。
適切な利用方法
以前より遅延読み込みの利用に関しては、主に以下のようなセオリーが流布しており、本稿でも主張は変わりません。
- ページのAbove the fold[4]に位置するものに関しては、LCPを悪化させる可能性が高いので遅延読み込みを避ける
- 遅延読み込みによる表示がユーザー操作によるスクロールに間に合わない場合を加味して、レイアウトシフトを避けるための
width
およびheight
属性の値の指定を考慮する
加えて、先ほど紹介したブラウザの実装を踏まえると、サポート対象とするブラウザが限定されている場合は、より細かなパフォーマンスチューニングを実施できる可能性があります。
ユースケースを紹介
Webフロントエンド実装に携わる開発者
HTMLのコーディングに携わる開発者は、まずLCPなどパフォーマンスの改善が必要かどうかを、ブラウザの開発者ツール、LighthouseやPaint Timing APIなどを用いて計測します。
その上で改善すべきと判断した時、その解決方法の1つとして遅延読み込みを選択し、実際に効果が出るかどうかを再び計測します。この時、ブラウザ実装の差異を思い浮かべ、最適な遅延読み込みを配置できる可能性があります。
UIライブラリやWebフレームワーク実装に携わる開発者
WordPressの例
PHP製のCMSであり世界中で広く用いられるWordPressの場合、細かなパフォーマンスの調整を期待することが難しい開発者以外もサイト構築に用いることが想定されます。そこで、可能な限り対象の要素を遅延読み込みに倒すことで、LCPを速くするような試みは選択肢の1つです。
下記はWordPressの遅延読み込みに関するリンクおよびトピックです。
- https://make.wordpress.org/core/2020/07/14/lazy-loading-images-in-5-5/
- img要素のwidthおよびheight属性の値の指定がある場合に自動で
loading="lazy"
を付与するようになった
この変更によってパフォーマンス指標が悪化した例も確認された
- img要素のwidthおよびheight属性の値の指定がある場合に自動で
- https://make.wordpress.org/core/2021/02/19/lazy-loading-iframes-in-5-7/
- iframe要素のwidthおよびheight属性の値の指定がある場合に自動で
loading="lazy"
を付与するようになった
- iframe要素のwidthおよびheight属性の値の指定がある場合に自動で
- https://make.wordpress.org/core/2021/12/29/enhanced-lazy-loading-performance-in-5-9/
- この変更によってHTMLの最初に現れるimgおよびiframe要素は
loading="lazy"
を付与しない実装になった
- この変更によってHTMLの最初に現れるimgおよびiframe要素は
- https://web.dev/articles/lcp-lazy-loading/
- WordPress標準テーマの遅延読み込みによるパフォーマンスへの効果の検証
Next.jsの例
JavaScriptランタイム向けのWebフレームワークであるNext.jsでは、<Image />
という画像表示に有用なユーティリティReactコンポーネントを提供しています。
ドキュメントおよび内部実装[5]では、遅延読み込みがデフォルト値に設定されています。これに対し、Above the foldにおいてはpriority
の値を設定する、あるいはfetchPriority
[6]を実装していないブラウザの場合はloading="eager"
を指定することで遅延読み込みを無効にできます。こちらもWordPressと似て、多数の開発者に利用されるフレームワークが提供するUIコンポーネントという立場からLCPを高速化するための一例です。
どちらにも共通するのは、様々なコンテンツの提供者(開発者)を想定し、彼らにとって最大公約数となる実装を提供することで、全体的なWebのパフォーマンスを高めようという狙いがあると考えられます。
UIライブラリあるいはWebフレームーワーク立ち位置の開発者である場合、前述のような実装例や本稿で取り上げたブラウザ実装の差異を念頭に入れつつ、対象のコンテンツの提供者を想定して最適なものを提供できます。
まとめ
Webサイトのパフォーマンス最適化の一手として、遅延読み込みがモダンブラウザに実装されてしばらく経ちました。遅延読み込みのためのrootMargin
の値は実装依存であり、Blink[7]やGecko[3]では幾度か変更されています。今後、世界のネットワーク事情によって将来的に変更される可能性もあります。
本稿で取り上げた仕様や実装を1つのポインタとし、適宜最適な実装を行えるとよいでしょう。
[1]: モダンブラウザによるpreload scannerが実行されていると仮定しています。↩
[2]: loading=“auto"に関する説明
BlinkではM100で廃止されたLite mode向けにauto
を実装していた
現在はauto
を指定するとeager
に倒される: loading_attribute_value.h
html_image_element.cc
↩
[3]: GeckoにおけるrootMarginの変遷
↩
[4]: ユーザーのスクロール操作を含まないブラウザのページ初期表示領域のこと。語源は新聞の一面。
↩
[5]: fetchPriorityによるリソース読み込み優先度の制御している: https://github.com/vercel/next.js/blob/v13.4.19/packages/next/src/shared/lib/get-img-props.ts#L375-L392
https://github.com/vercel/next.js/blob/v13.4.19/packages/next/src/client/image-component.tsx
↩
[6]:fetchPriority
はSafari Technology Preview 178よりデフォルトで利用可能
Geckoは実装予定↩
[7]: BlinkにおけるrootMarginの変遷
↩
株式会社ドワンゴでは、様々なサービス、コンテンツを一緒につくるメンバーを募集しています。 ドワンゴに興味がある。または応募しようか迷っている方がいれば、気軽に応募してみてください。