こんにちは、株式会社ファストコーディングのBigViです。先週、夫と一緒にネットで家具を見ていました。写真がたくさんあるページで、スクロールすると画像が突然パッと出てきて、レイアウトがガタッとずれました。。夫が「壊れた?」って聞いてきました。壊れてないけど、確かに気持ちいい体験ではないです。
先日、お客さまから「ECサイトの商品一覧ページがカクカクする」と相談がありました。loading="lazy"で画像を遅延読み込みしているけど、画像が表示されるまで真っ白で、突然表示されるとレイアウトがずれる。CLS(Cumulative Layout Shift)も悪い数値でした。
私もよくあります。画像が多いサイトで、スクロールしたら急にガタッとずれて、読んでいた場所がわからなくなること。。
今回は「画像が読み込まれるまで、灰色の矩形+シマーアニメーション(スケルトン表示)を表示する」という実装をしました。ユーザーに「ここに画像が来ますよ」と伝えて、ガタつきをなくします。
なぜスケルトン表示が必要なのか
loading="lazy"は良い技術です。ページの初期表示が速くなります。でも問題もあります。画像が読み込まれる前は、img要素のサイズが0になることがあります。読み込まれた瞬間にサイズが確定して、レイアウトがずれます。
widthとheight属性をつければCLSは改善できます。でも読み込み中は真っ白の空間です。ユーザーからすると「ここは何もないのかな?」と思ってスクロールしてしまいます。
スケルトン表示は2つの問題を解決します。1つ目は「ここに画像が来る」という視覚的なヒントです。2つ目はシマーアニメーションで「読み込み中」と伝えることです。
ある案件で、最初はスピナー(くるくる回るアイコン)を表示していました。でもスピナーは「待って」という印象が強くて、お客さまに「遅いサイトに見える」と言われました。。スケルトンに変えたら「速く感じる」と言ってくれました。実際の読み込み速度は同じなのに、見せ方で印象が変わります。
実装方法
HTML構造
img要素をラッパーで囲みます。スケルトンはCSSの疑似要素で作ります。
<div class="img-skeleton" data-skeleton>
<img
src="product.jpg"
alt="商品名"
width="400"
height="300"
loading="lazy"
>
</div>widthとheightをつけるのが大事です。ブラウザが事前にアスペクト比を計算して、表示領域を確保してくれます。これだけでCLSが大幅に改善します。
CSSスタイル
スケルトンの灰色矩形とシマーアニメーションをCSSで作ります。
.img-skeleton {
position: relative;
overflow: hidden;
background: #e0e0e0;
line-height: 0;
}
.img-skeleton img {
display: block;
width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
opacity: 0;
transition: opacity 0.3s ease;
}
.img-skeleton[data-loaded] img {
opacity: 1;
}
.img-skeleton::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
}
.img-skeleton[data-loaded]::after {
display: none;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
.img-skeleton::after {
animation: none;
background: rgba(255, 255, 255, 0.2);
}
.img-skeleton img {
transition: none;
}
}ポイントはopacityを使って画像の表示を切り替えることです。display: noneだとブラウザが画像を読み込まないことがあります。opacity: 0なら見えないけど読み込みは始まります。
line-height: 0はよく忘れます。これがないと、imgの下に数ピクセルの隙間ができてスケルトンの灰色がはみ出ます。
シマーの色はrgba(255, 255, 255, 0.4)です。白の半透明が灰色の上を流れると、金属っぽい光沢になります。方向は左から右です。アラビア語のRTLサイトでは逆にする必要があります。
JavaScriptの実装
画像のloadイベントを監視して、読み込み完了したらdata-loaded属性をつけます。
document.addEventListener('DOMContentLoaded', () => {
const skeletons = document.querySelectorAll('[data-skeleton]');
skeletons.forEach((wrapper) => {
const img = wrapper.querySelector('img');
if (!img) return;
function onLoaded() {
wrapper.setAttribute('data-loaded', '');
img.removeEventListener('load', onLoaded);
}
if (img.complete && img.naturalWidth > 0) {
onLoaded();
} else {
img.addEventListener('load', onLoaded);
}
});
});img.completeのチェックが大事です。ブラウザのキャッシュに画像がある場合、DOMContentLoadedの時点ですでに読み込み完了していることがあります。loadイベントは発火しないので、completeで先にチェックしています。
naturalWidth > 0も確認しています。completeがtrueでも、画像のURLが間違っていて読み込み失敗した場合はnaturalWidthが0になります。
aspect-ratioでCLS防止
widthとheight属性だけだと、CSSでサイズを変えたときにアスペクト比がおかしくなることがあります。aspect-ratioプロパティも併用すると安全です。
.img-skeleton img {
width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}ただし、aspect-ratio: attr()は一部のブラウザでまだ対応していないです。確実に動かすには、CSSカスタムプロパティを使う方法もあります。
<div class="img-skeleton" data-skeleton style="--ratio: 400 / 300">
<img src="product.jpg" alt="商品名" width="400" height="300" loading="lazy">
</div>.img-skeleton {
aspect-ratio: var(--ratio, 4 / 3);
}注意点
実装するときに気をつけたことが3つあります。
1つ目はエラーハンドリングです。画像の読み込みが失敗した場合、スケルトンがずっと表示されたままになります。errorイベントもハンドリングして、代替画像を表示するか、スケルトンを非表示にしましょう。
img.addEventListener('error', () => {
wrapper.setAttribute('data-loaded', '');
img.alt = '画像を読み込めませんでした';
});2つ目は画像の枚数です。商品一覧ページで100枚以上の画像がある場合、各画像にloadイベントリスナーをつけると少し重くなります。でもIntersectionObserverと組み合わせて、画面に見える画像だけ処理する方法は複雑になりすぎます。100枚程度なら気にしなくて大丈夫です。
3つ目はprefers-reduced-motionです。シマーアニメーションを無効にしています。代わりに白の半透明を静的に重ねて、「ここはまだ読み込み中」とわかるようにしています。
実際の結果
このお客さまのプロジェクトで、A/Bテストを3週間やりました。
| 指標 | スケルトンなし(A) | スケルトンあり(B) | 差分 |
|---|---|---|---|
| 体感速度の評価(5点満点) | 3.1 | 4.2 | +1.1 |
| ページ滞在時間 | 45秒 | 58秒 | +13秒 |
| CLS(Core Web Vitals) | 0.18 | 0.04 | -0.14 |
体感速度の評価はユーザーアンケートの結果です。実際の読み込み速度はほぼ同じなのに、スケルトンがあるだけで「速い」と感じてもらえました。CLSも0.18から0.04に改善しています。Googleが推奨する「良好」の基準は0.1以下なので、余裕でクリアしました。ページ滞在時間も13秒増えています。ガタつきがなくなったことで、ストレスなくスクロールできるようになったからだと思います。ただし、これは1つのプロジェクトでの結果です。効果は画像の枚数やページ構成によって変わります。
まとめ
loading="lazy"だけだと、画像の場所が空白になって突然表示されます。レイアウトがずれるし、体感的にも遅く感じます。スケルトン表示を入れることで、「ここに画像が来ますよ」と伝えられて、ガタつきもなくなります。
今回のポイントは以下の3つです:
widthとheight属性をつけて、aspect-ratioでアスペクト比を確保する。これだけでCLSが大幅に改善する- シマーアニメーションは
transform: translateX()で作る。leftのアニメーションより軽い(GPUで処理される) img.completeで事前チェックしてからloadイベントリスナーをつける。キャッシュ済み画像の対応を忘れない
お客さまからは「表示が速くなった気がする」と言ってくれました。。実際には速くなっていないです。見せ方が変わっただけです。でもユーザーにとっては「感じる速さ」が大事です。
株式会社ファストコーディングでは、こうしたCore Web Vitalsの改善やフロントエンドの実装サポートをしています。「画像の多いページをなんとかしたい」「CLSのスコアを改善したい」という方は、お問い合わせフォームから気軽にご連絡ください。
※本記事は弊社外国人スタッフによる投稿です。言い回しや表現が不十分な個所がありますことご容赦いただきますようお願いいたします。

