モバイルでのオフスクリーン画像の遅延:ネイティブlazy loadingガイド
ネイティブのlazy loading、content-visibility、そしてJavaScriptによる画像の遅延読み込みがなぜ無用の長物なのか
モバイル画像の遅延:標準的な手法
モバイルのページは、限られたコネクションとCPUを奪い合います。画面外の画像を初期段階で読み込むと、初期描画に本当に必要な画像やスクリプトから帯域幅を奪うことになります。
最終査読:Arjen Karel、2026年3月

Table of Contents!
1. モバイルでの画面外画像の遅延:ネイティブlazy loading
ブラウザがページを読み込む際、並行して開けるコネクション数には制限があります(条件により異なりますが、ドメインあたり6つが一般的です)。画面外の画像(フッターのロゴやカルーセルのスライドなど)のダウンロードにこれらのコネクションを使用すると、クリティカルなリソース(通常はLCP画像、重要なスクリプト、フォント)がスロットと帯域幅の確保で競合します。これがネットワークの競合であり、Core Web Vitalsの悪化に直結します。
ネイティブのloading属性を使用して画面外の画像を遅延させることで、重要なものを優先できます。ブラウザはすぐに表示されるものだけを取得し、Largest Contentful Paint (LCP)やFirst Contentful Paint (FCP)に影響するアセットのために帯域幅を確保します。ネイティブlazy loadingはこの優先順位付けをブラウザ自身のメカニズムに任せます。これにより処理が高速化し、JavaScriptのlazy loadingライブラリが不要になります。
実装
初期のviewport(「フォールド」)より下にあるすべての画像に、loading="lazy"属性を追加します。
<!-- 標準的な遅延画像 -->
<img src="product-detail.jpg"
loading="lazy"
alt="Side view of the chassis"
width="800"
height="600"
decoding="async">
widthとheight属性は必須です。これらがないと、ブラウザは画像が読み込まれる前にスペースを確保できず、layout shift (CLS)が発生します。モバイルのページの62%は、依然として少なくとも1つの画像で明示的なサイズ指定に失敗しています。
モバイルにおけるlazy loadingの仕組み:ブラウザのヒューリスティック
ネイティブlazy loadingはJavaScriptのソリューションよりも優れています。なぜなら、ブラウザはEffective Connection Type (ECT) に基づいて読み込みのしきい値(画像のダウンロードがトリガーされるタイミング)を調整するからです。
- 4G/WiFi環境:Blinkエンジン(Chrome/Edge)は、約1,250pxという控えめなしきい値を使用します。レイテンシが低いと想定し、ユーザーが比較的近くまでスクロールしたときにのみ画像を取得します。
- 3G/低速2G環境:しきい値は約2,500pxまで拡大されます。高いラウンドトリップタイムを補うために、ブラウザははるかに早くリクエストを開始します。これにより、ユーザーがスクロールして表示領域に入る前に画像の準備が整います。
2025 Web Almanacによると、モバイルのページの中央値では、15枚(合計911 KB)の画像を読み込みます。その中でloading="lazy"を使用している画像は約26%にすぎません。残りの画像は即座に読み込まれ、同じ限られたコネクションを競合します。一般的な4Gモバイル回線では、これはつまり、ユーザーが数秒間見ることのない十数枚の画像の背後で、LCP画像が待たされてしまうことを意味します。
重大な例外:LCP候補
よくあるパフォーマンス低下の原因は、Largest Contentful Paintの要素(通常はヒーロー画像)にloading="lazy"を適用することです。これにより、レイアウトが完了するまで取得が遅延します。
Googleの調査では、LCP画像をlazy loadingするとLCPの中央値に624ms追加されることが示されています。これは理論上のリスクではありません。2025 Web Almanacによると、モバイルのページの17%が依然としてこのミスを犯しています。Lighthouseでこれが警告された場合は、lazy loadingされたLCPの警告を修正する方法を参照してください。
LCP画像は即座に読み込み(eager-loaded)、優先順位を高くする必要があります:
<!-- ヒーロー画像:即座に読み込み、優先順位を高くする -->
<img src="hero.jpg"
alt="Summer Collection"
width="1200"
height="800"
loading="eager"
fetchpriority="high">
loading="lazy"とfetchpriority="high"を組み合わせてはいけません。これらは互いに矛盾しています。lazyはブラウザに待機を指示し、highは急ぐよう指示します。lazyが設定されている場合、ブラウザは優先度のヒントを無視します。ブラウザがリソースに優先順位を付ける仕組みの詳細については、リソースの優先順位付けガイドを参照してください。
2. モバイル特有の複雑さ:viewportとタッチ
モバイルのviewportは静的ではありません。ユーザーがスクロールしたり、デバイスを回転させたり、URLバーが引っ込む動作をトリガーしたりするにつれて、表示領域は変化します。ここで、ネイティブlazy loadingがJavaScriptのソリューションに対して真の優位性を発揮します。
- viewport:ブラウザウィンドウの表示される矩形領域です。モバイルでは動的であり、デバイスの向き(縦向きか横向きか)やブラウザのクロームの状態(URLバーの引っ込み)に基づいてサイズが変化します。
- フォールド:viewportの正確な下端です。表示されているコンテンツと画面外のコンテンツを分けるしきい値です。
- フォールドより上:ページの読み込み時にスクロールせずに即座に表示されるコンテンツです。ここにある画像はクリティカルであり、絶対にlazy loadingしてはいけません。
- フォールドより下:フォールドより下に位置するコンテンツです。このコンテンツは非クリティカルであり、ユーザーがその近くまでスクロールするまで遅延させるべきです。

動的なviewport
モバイルのブラウザでは、viewportの高さ(vh)は流動的です。ユーザーがタッチスクロールを開始すると、多くの場合URLバーとナビゲーションコントロールが引っ込み、表示領域のサイズが変わります。
JavaScriptのlazy loadingライブラリは通常、ページの読み込み時に一度だけviewportの高さ(window.innerHeight)を計算します。モバイルブラウザがスクロール中にURLバーを隠して表示領域を動的にリサイズしても、JavaScriptの手法は古くて小さい高さの値を使い続けます。拡張されたviewportに画像が入っても読み込まれないままであり、訪問者には空白のプレースホルダーが表示されてしまいます。
ブラウザの内部レイアウトエンジンは視覚的なviewportを自動的に追跡するため、viewportのサイズ変化に関係なくネイティブlazy loadingのトリガーが発火します。これが、JavaScriptの代替手段よりもネイティブlazy loadingを選択する理由の1つです。
3. モバイルでの画像デコードとCPUスロットリング
モバイルデバイスはCPUリソースが限られており、モバイルでの画像デコードは比較的遅く高コストです。JPEGをビットマップに変換するには多くのCPUサイクルが必要です。モバイルプロセッサでは、大きな画像のシーケンスをデコードすると、それぞれmain threadを50msから100msブロックし、入力レイテンシを引き起こします。
解決策:content-visibility
CSSプロパティcontent-visibility: autoは、遅延レンダリングとして機能します。画面外の要素について、レイアウトとペイントのフェーズを完全にスキップするようブラウザに指示します。要素はDOMに存在しますが、viewportに近づくまではレンダリングツリーには存在しません。
この最適化は要素のサブツリーのレンダリングをスキップすることで機能するため、サブツリーを持たない<img>タグに直接適用することはできません。画像とそのコンテンツをホストする製品コンテナや画像カードにcontent-visibilityを適用してください。
@media (max-width: 768px) {
.image-card, .product-card {
/* コンテナとその子要素のレンダリングをスキップ */
content-visibility: auto;
/* 必須:コンテナの高さが0pxに潰れるのを防ぐ */
contain-intrinsic-size: auto 300px;
}
}
これにより、画像がダウンロードされたとしても、ユーザーが実際にそこまでスクロールするまで、ブラウザはレイアウトやペイントのコストを支払わずに済みます。
content-visibilityは、Safari 18がサポートを開始した2024年9月にBaselineステータスに到達しました。現在、世界中のブラウザの93%で動作します。Googleのベンチマークによると、画面外のセクションが多いページでは、初期読み込み時のレンダリングパフォーマンスが7倍向上することが示されています。
実際のデバイスでのレンダリングの改善を確認したい場合、Real User Monitoringが、モバイルのトラフィック全体における実際のINPとLCPへの影響を示してくれます。CoreDashが監視しているサイト全体で見ると、製品グリッドにcontent-visibility: autoを使用しているページは、使用していないページと比較して、モバイルでのINPが約15%優れています。
4. レガシーな手法:なぜ避けるべきか
loading="lazy"がブラウザでサポートされる前は、JavaScriptが唯一の選択肢でした。ネイティブlazy loadingが世界中で95%サポートされている現在、これらのJavaScriptメソッドは技術的負債です。削除してください。
スクロールハンドラーの時代(2010年~2016年)
初期の実装では、スクロールイベントにイベントリスナーをアタッチしていました。
// 非推奨:使用しないでください
window.addEventListener('scroll', () => {
images.forEach(img => {
if (img.getBoundingClientRect().top < window.innerHeight) {
img.src = img.dataset.src;
}
});
});
main threadのブロック:スクロールイベントは1秒間に何十回も発火します。アクティブなスクロール中にロジックを実行し、レイアウト(getBoundingClientRect)を計算すると、フレームドロップ(ジャンク)が発生します。
レイアウトスラッシング:ジオメトリプロパティをクエリすると、ブラウザは同期的にレイアウトを再計算することを強制されます。これはモバイルCPUにとって計算コストの高い操作です。
IntersectionObserverの時代(2016年~2019年)
IntersectionObserver APIは、要素の可視性の変化を非同期に監視することでパフォーマンスを向上させました。
// レガシー:可能な限りネイティブのloading="lazy"を優先してください
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
スクリプトへの依存:JavaScriptの実行が必要です。main threadがフレームワーク(React/Vue)のハイドレーションでビジー状態の場合、画像がviewport内にあっても読み込まれないままになります。
ネットワーク認識の欠如:ネイティブの読み込みとは異なり、IntersectionObserverは固定のマージン(例:rootMargin: '200px')を使用します。低速なネットワークでバッファを自動的に拡張しないため、接続状態の悪いユーザーには空白のフラッシュが発生します。
lazy loading以外の画像最適化技術の完全な概要、または(loading="lazy"がカバーしていない)CSS背景画像の遅延について学ぶには、それぞれの専用ガイドを参照してください。