Lazy Loading画像と動画でパフォーマンス改善
2025.08.18 : Yamamoto Naoki
Coding
Lazy Loading 画像と動画でパフォーマンス改善

はじめに
この記事では「Lazy Loading 画像と動画でパフォーマンス改善」について、初級者から中級者の方にも分かりやすく、実践的な内容をお届けします。 ウェブサイト制作において、このテーマは SEO やユーザー体験の向上に直結するため、ぜひ理解して活用してください。
Lazy Loading(遅延読み込み)は、ビューポート外の画像や動画、iframe などのリソースを、実際に必要になるまで読み込まない最適化手法です。初期表示のリクエスト数と転送量を削減し、First Contentful Paint (FCP) や Time to Interactive (TTI) の改善に役立ちます。
基本概念
Lazy Loading の対象
- 画像(
<img>
) - 動画(
<video>
のファイル本体、<source>
、サムネイル) - iframe(地図や埋め込み動画など)
- 背景画像(CSS 背景は JS での遅延が必要)
実現方法の種類
- ネイティブ属性:
loading="lazy"
(画像・iframe)、decoding="async"
(画像)。実装が簡単で現在は主要ブラウザに広く対応 - JavaScript + IntersectionObserver: 細かな制御が可能。プレースホルダやアニメーション、フェードインなどの表現に向く
実装手順
1. もっとも簡単: ネイティブ Lazy Loading(画像・iframe)
<!-- 折りたたみ下の画像に lazy を指定 -->
<img
src="/images/gallery-800.jpg"
alt="作品ギャラリー"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
<!-- iframe も同様に対応 -->
<iframe
src="https://www.youtube.com/embed/xxxx"
title="紹介動画"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
ポイント:
width
とheight
を指定して描画領域を確保(CLS 防止)。CSS のaspect-ratio
でも可- ファーストビュー(LCP 候補)の画像は lazy にしない。必要に応じて
fetchpriority="high"
を併用
2. ぼかしプレースホルダ(LQIP/Blur-up)
<img
src="/images/placeholder-blur.jpg"
data-src="/images/hero-1600.jpg"
alt="ヒーロー画像"
class="lazy blur"
width="1600"
height="900"
/>
.blur {
filter: blur(12px);
transition: filter 300ms ease;
}
.blur.is-loaded {
filter: blur(0);
}
const lazyTargets = document.querySelectorAll('img.lazy');
const io = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
const real = img.getAttribute('data-src');
if (real) {
img.src = real;
img.addEventListener(
'load',
() => img.classList.add('is-loaded'),
{ once: true }
);
}
observer.unobserve(img);
});
},
{ rootMargin: '200px 0px' }
);
lazyTargets.forEach((el) => io.observe(el));
3. IntersectionObserver での汎用実装(data-src
/data-srcset
)
<img
src="/images/placeholder.jpg"
data-src="/images/photo-1200.jpg"
data-srcset="/images/photo-600.jpg 600w, /images/photo-1200.jpg 1200w"
data-sizes="(max-width: 600px) 600px, 1200px"
alt="写真"
width="1200"
height="800"
class="js-lazy"
/>
function upgradeSource(target) {
const src = target.getAttribute('data-src');
const srcset = target.getAttribute('data-srcset');
const sizes = target.getAttribute('data-sizes');
if (src) target.src = src;
if (srcset) target.srcset = srcset;
if (sizes) target.sizes = sizes;
}
const observer = new IntersectionObserver(
(entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
upgradeSource(entry.target);
obs.unobserve(entry.target);
}
},
{ rootMargin: '200px 0px', threshold: 0.01 }
);
document
.querySelectorAll('img.js-lazy')
.forEach((img) => observer.observe(img));
<noscript>
<img src="/images/photo-1200.jpg" alt="写真" width="1200" height="800" />
</noscript>
4. 動画の遅延読み込み(preload="none"
と遅延アタッチ)
<video
controls
preload="none"
poster="/video/thumb.jpg"
width="1280"
height="720"
class="video-lazy"
>
<source data-src="/video/movie-1080.mp4" type="video/mp4" />
</video>
const videoObserver = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const video = entry.target;
const source = video.querySelector('source[data-src]');
if (source && !source.src) {
source.src = source.dataset.src;
video.load();
}
obs.unobserve(video);
});
},
{ rootMargin: '300px 0px' }
);
document
.querySelectorAll('video.video-lazy')
.forEach((v) => videoObserver.observe(v));
5. 背景画像(CSS)の遅延
<div class="card js-bg-lazy" data-bg="/images/card-bg.jpg">コンテンツ...</div>
.card {
min-height: 240px;
background-size: cover;
background-position: center;
}
const bgObserver = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const el = entry.target;
const url = el.getAttribute('data-bg');
if (url) el.style.backgroundImage = `url("${url}")`;
obs.unobserve(el);
});
});
document
.querySelectorAll('.js-bg-lazy')
.forEach((el) => bgObserver.observe(el));
ベストプラクティス
- LCP 候補は lazy にしない: ヒーロー画像やファーストビューのカルーセル 1 枚目は通常読み込みし、
fetchpriority="high"
を検討 - CLS 防止:
width
/height
またはaspect-ratio
でレイアウト領域を予約 - レスポンシブと併用:
srcset
/sizes
を合わせて帯域を節約 - rootMargin のチューニング: 100–300px のプリフェッチ余裕をとる( IntersectionObserver のオプション)
- ポスター画像の活用(動画):
poster
を必ず用意し、プレースホルダとして表示 - iframe の遅延:
loading="lazy"
をデフォルトに。地図や外部ウィジェットに有効 - アクセシビリティ:
alt
を適切に記述。遅延は読み上げ順序に影響しないようマークアップ順序を維持 - SEO 配慮:
data-src
手法を用いる場合は<noscript>
でフォールバックを提供。ネイティブ lazy(src
明示)ならクローラの理解が安定
フレームワーク/プラットフォームでの活用
- Next.js:
next/image
は既定で最適化。LCP はpriority
、その他はloading="lazy"
(デフォルト) - Nuxt/Image:
nuxt-img
でloading="lazy"
、sizes
を指定 - WordPress: 5.5+ で
loading="lazy"
がデフォルト。ヒーローは除外設定を検討 - ライブラリ: 可能ならネイティブ優先。追加機能が必要なら Lozad.js / lazysizes を選択
テストと検証
- Lighthouse: 画像要素の遅延読み込み、LCP/CLS の回帰をチェック
- Chrome DevTools: Network の優先度、Waterfall、
img
のDecoded Size
を確認 - WebPageTest: 1st/Repeat View、モバイル実機シミュレーションで効果を比較
トラブルシューティング
- 画像が表示されない
data-src
の移し替え忘れ/誤ったセレクタIntersectionObserver
のroot
がスクロールコンテナに合っていないrootMargin
が小さすぎてユーザーより先に読み込めていない
- LCP が悪化した
- LCP 画像に
loading="lazy"
を付けてしまっている。外し、fetchpriority="high"
を付与する
- LCP 画像に
- CLS が増えた
width
/height
未指定。aspect-ratio
で領域を設定する
- SEO が不安
data-src
運用時は<noscript>
フォールバックを必ず用意。可能ならネイティブ lazy を使用する
まとめ
Lazy Loading は初期ロードの負荷を下げ、体感速度とエンゲージメントを高める強力な施策です。
- まずはネイティブ
loading="lazy"
を適用し、上位要素(LCP 候補)は対象外にする width
/height
(またはaspect-ratio
)で CLS を防止- 必要に応じて IntersectionObserver でプレースホルダや動画、背景画像まで拡張
- Lighthouse/DevTools/WebPageTest で計測し、
rootMargin
や画像サイズを継続的に調整
日々の制作に取り入れて、パフォーマンスと体験の底上げを図りましょう。