Lazyload(遅延読み込み)は、ページを開いた瞬間には表示されていない画像や動画・iframeの読み込みを後回しにすることで、初期表示速度を大幅に改善できる手法です。Largest Contentful Paint(LCP)の改善にも直結し、Core Web Vitals の観点でもSEOに影響します。
かつては外部ライブラリが必要でしたが、現在はHTMLの loading 属性だけでブラウザネイティブの遅延読み込みが実現できます。この記事ではネイティブ実装からJavaScript(IntersectionObserver)を使ったカスタム実装、LCPへの悪影響を避ける設計まで体系的に解説します。
ネイティブ遅延読み込み:loading=”lazy” 属性(JSゼロ)
最もシンプルな方法はHTML属性の loading="lazy" を付けるだけです。JavaScriptは一切不要で、ブラウザが自動的にビューポート外の画像・iframeの読み込みを遅延させます。
<!-- 画像 --> <img src="large-photo.jpg" alt="説明テキスト" width="800" height="600" loading="lazy" > <!-- iframe(YouTube埋め込みなど) --> <iframe src="https://www.youtube.com/embed/XXXXX" width="560" height="315" loading="lazy" allowfullscreen ></iframe>
| 属性値 | 動作 | 用途 |
|---|---|---|
loading="lazy" |
ビューポートに近づいたら読み込む | ファーストビュー外の画像・iframe |
loading="eager" |
即座に読み込む(デフォルト) | ファーストビューの重要な画像 |
loading="auto" |
ブラウザに委ねる | 通常は eager と同じ |
loading="lazy" を使うとき、width と height 属性がないとブラウザが画像の領域を事前に確保できません。画像が読み込まれるたびにレイアウトがずれる CLS(Cumulative Layout Shift) が発生し、Core Web Vitals のスコアを下げます。必ず width と height(または CSS の aspect-ratio)を指定してください。loading="lazy" は Chrome 77+、Firefox 75+、Safari 15.4+、Edge 79+ で対応しており、2024年時点でほぼすべての主要ブラウザで利用できます。未対応ブラウザでは属性が無視されて通常の読み込みになるだけなので、フォールバックを気にせず安全に使えます。LCP を悪化させないための注意点
最大コンテンツの表示タイミングを測る LCP(Largest Contentful Paint) の対象がヒーロー画像など「ファーストビューに表示される大きな画像」の場合、loading="lazy" を付けると読み込みが遅れて LCP スコアが悪化します。
<!-- NG: ファーストビューのメイン画像に lazy を付けると LCP が悪化 --> <img src="hero.jpg" alt="ヒーロー画像" width="1920" height="1080" loading="lazy" > <!-- OK: ファーストビューのメイン画像は eager(またはデフォルト)+ fetchpriority="high" --> <img src="hero.jpg" alt="ヒーロー画像" width="1920" height="1080" loading="eager" fetchpriority="high" > <!-- OK: スクロールしないと見えない画像には lazy --> <img src="gallery-1.jpg" alt="ギャラリー画像" width="600" height="400" loading="lazy" >
- ページを開いてすぐ見える(ファーストビュー)画像 →
loading="eager"または省略 - ページ最上部のメインビジュアル(LCP候補) →
fetchpriority="high"も追加 - スクロールしないと見えない画像・iframe →
loading="lazy" - ギャラリー・カード一覧・フッター画像 →
loading="lazy"
IntersectionObserver によるカスタム遅延読み込み
画像だけでなく「コンテンツブロック全体」を遅延レンダリングしたいとき、loading="lazy" では対応できません。JavaScript の IntersectionObserver を使ってカスタムの遅延読み込みを実装します。
<!-- src の代わりに data-src に実際の画像URLを書く --> <!-- src には小さなプレースホルダーか省略 --> <img data-src="large-photo.jpg" alt="遅延読み込み画像" width="800" height="600" class="lazy-img" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" >
/**
* IntersectionObserver で data-src を src に差し替えて遅延読み込みする
*/
function initLazyImages() {
const images = document.querySelectorAll('img[data-src]');
if (!images.length) return;
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // data-src を src に移す
img.removeAttribute('data-src'); // 処理済みマーク
img.classList.add('lazy-loaded'); // ロード後のスタイル用
obs.unobserve(img); // 読み込んだら監視解除
});
}, {
rootMargin: '200px 0px', // ビューポートより200px手前で発火
threshold: 0,
});
images.forEach(img => observer.observe(img));
}
document.addEventListener('DOMContentLoaded', initLazyImages);
/* 読み込み前はぼかして半透明 */
img[data-src] {
filter: blur(4px);
opacity: 0;
transition: opacity 0.4s ease, filter 0.4s ease;
}
/* 読み込み後にフェードイン */
img.lazy-loaded {
filter: none;
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
img[data-src],
img.lazy-loaded {
transition: none;
}
}
srcset・picture 要素への対応
レスポンシブ画像(srcset)でも同様に data- 属性を使って遅延読み込みできます。
<!-- data-srcset に実際の値を書く --> <img data-src="photo-800.jpg" data-srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w" sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px" alt="レスポンシブ画像" width="800" height="600" class="lazy-img" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" >
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
// srcset も差し替える
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute('data-srcset');
}
img.src = img.dataset.src;
img.removeAttribute('data-src');
img.classList.add('lazy-loaded');
obs.unobserve(img);
});
}, { rootMargin: '200px 0px', threshold: 0 });
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
コンテンツブロックの遅延レンダリング
画像だけでなく、SNSの埋め込みウィジェットや重いUIブロック全体を「画面に近づいたときに初めて挿入する」方法です。初期HTMLは軽くなり、DOMの構築が速くなります。
<!-- template 要素の中身はDOMにレンダリングされない -->
<div class="lazy-block" id="heavy-widget">
<template>
<!-- ここに重い埋め込みウィジェットや複雑なHTMLを書く -->
<div class="widget">
<iframe src="https://example.com/widget" width="300" height="250"></iframe>
</div>
</template>
</div>
function initLazyBlocks() {
const blocks = document.querySelectorAll('.lazy-block');
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const container = entry.target;
const template = container.querySelector('template');
if (template) {
// template の中身を複製してDOMに挿入
container.appendChild(template.content.cloneNode(true));
template.remove();
}
obs.unobserve(container);
});
}, { rootMargin: '300px 0px', threshold: 0 });
blocks.forEach(block => observer.observe(block));
}
document.addEventListener('DOMContentLoaded', initLazyBlocks);
<template> 要素の内容はDOMとして解析されますが、レンダリングされず、スクリプトも実行されません。ページ読み込み時のHTMLパースに大きな影響を与えずに「後で挿入するHTML」を準備しておける仕組みです。YouTubeやGoogleマップの埋め込みなど、読み込みコストが高いiframeを遅延挿入するのに最適です。rootMargin と threshold のチューニング
rootMargin と threshold を調整することで、遅延読み込みのトリガーするタイミングを細かく制御できます。
const observer = new IntersectionObserver(callback, {
// rootMargin: 監視領域をビューポートから拡張する余白(CSSのmarginと同じ形式)
// "200px 0px" → ビューポートの上下200px外から監視開始
// 正の値で「早めに発火」、負の値で「中心に入ったら発火」
rootMargin: '200px 0px',
// threshold: 対象要素のどれだけが見えたら発火するか(0〜1)
// 0 → 1pxでも見えたら発火(最も早い)
// 0.5 → 要素の50%が見えたら発火
// 1 → 要素全体が見えたら発火
threshold: 0,
});
/*
* 用途別チューニング例:
*
* 画像の先読み(スクロール前に準備)
* rootMargin: '400px 0px', threshold: 0
*
* アニメーション(要素が半分入ったら開始)
* rootMargin: '0px', threshold: 0.5
*
* 無限スクロールの次ページ読み込み
* rootMargin: '100px 0px', threshold: 0
*
* フッターの到達検知
* rootMargin: '0px', threshold: 1
*/
完成形:LazyLoader クラス
画像・srcset・コンテンツブロックをまとめて処理する汎用クラスです。
class LazyLoader {
#observer;
/**
* @param {object} opts
* @param {string} opts.imageSelector - 遅延画像のセレクタ(デフォルト: 'img[data-src]')
* @param {string} opts.blockSelector - 遅延ブロックのセレクタ(デフォルト: '.lazy-block')
* @param {string} opts.rootMargin - IntersectionObserver の rootMargin
*/
constructor({
imageSelector = 'img[data-src]',
blockSelector = '.lazy-block',
rootMargin = '200px 0px',
} = {}) {
this.#observer = new IntersectionObserver(
(entries, obs) => this.#handle(entries, obs),
{ rootMargin, threshold: 0 }
);
document.querySelectorAll(imageSelector)
.forEach(el => this.#observer.observe(el));
document.querySelectorAll(blockSelector)
.forEach(el => this.#observer.observe(el));
}
#handle(entries, obs) {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const el = entry.target;
if (el.tagName === 'IMG') {
this.#loadImage(el);
} else {
this.#loadBlock(el);
}
obs.unobserve(el);
});
}
#loadImage(img) {
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute('data-srcset');
}
img.src = img.dataset.src;
img.removeAttribute('data-src');
img.classList.add('lazy-loaded');
}
#loadBlock(container) {
const tpl = container.querySelector('template');
if (tpl) {
container.appendChild(tpl.content.cloneNode(true));
tpl.remove();
}
}
/** 動的に追加した要素を追加監視する */
observe(el) {
this.#observer.observe(el);
}
/** 監視を全て停止する */
disconnect() {
this.#observer.disconnect();
}
}
// 使用例
document.addEventListener('DOMContentLoaded', () => {
new LazyLoader({ rootMargin: '300px 0px' });
});
よくある質問(FAQ)
loading="lazy" と IntersectionObserver のどちらを使えばいいですか?loading="lazy" 属性が最もシンプルで推奨です。ブラウザネイティブの実装なのでパフォーマンスも優れています。フェードインアニメーションの付与、コンテンツブロックの遅延レンダリング、より細かいトリガーのコントロールが必要な場合に IntersectionObserver を使います。loading="lazy" を付けたら LCP が悪化しました。loading="lazy" を付けると読み込みが遅れて LCP スコアが下がります。ヒーロー画像には loading="eager"(またはデフォルト)に加えて fetchpriority="high" を付けて最優先で読み込むようにしてください。loading="lazy" はHTMLの属性なのでJavaScript不要で動作します。IntersectionObserver を使うカスタム実装では、JavaScript が無効の場合は data-src のまま画像が読み込まれません。<noscript> タグで通常の <img src="..."> を用意しておくフォールバックが安全です。<img> に width と height 属性を必ず指定してください。CSSの aspect-ratio プロパティで代替することもできます(例: aspect-ratio: 16 / 9)。画像の領域を事前に確保することで、読み込み完了後のレイアウトずれを防げます。loading="lazy" を使っている場合、印刷時には window.onbeforeprint イベントで data-src を一括 src に差し替える処理を追加します。IntersectionObserver 版では、フォールバック関数を用意してすべての要素を即座に読み込む loadAll() メソッドを実装しておくのが実用的です。まとめ
| 手法 | 対象 | JS不要 | アニメーション | コンテンツブロック |
|---|---|---|---|---|
loading="lazy" |
img・iframe | ○ | × | × |
| IntersectionObserver | img・あらゆる要素 | × | ○ | ○ |
| 実装のポイント | 内容 |
|---|---|
| ファーストビュー画像 | loading="eager" + fetchpriority="high" |
| スクロール外の画像・iframe | loading="lazy" |
| CLS対策 | width・height 属性を必ず指定 |
| 先読みタイミング | rootMargin: "200〜400px 0px" |
| 重いウィジェットの遅延 | <template> + IntersectionObserver で遅延挿入 |
IntersectionObserver の使い方全般はIntersectionObserverでスクロール時に要素が見えると表示が変わる方法で、複数要素への適用についてはIntersectionObserverを使って複数の要素にスクロールイベントを設定する方法もあわせて参照してください。
