結論

スケルトンスクリーンとは、データの読み込み中に、完成後のレイアウトをなぞった灰色の仮の枠(画像・見出し・文章の形)を先に表示しておく表示方法です。スケルトンローディング、スケルトンUI とも呼びます。くるくる回るスピナーと違い「どこに何が出るか」を先に見せられ、枠の寸法を完成形に合わせればレイアウトのずれ(CLS)も防げます。

この記事でわかること

  • スケルトンスクリーンの意味と、スピナー・プログレスバーとの使い分け
  • CSS だけで作るコードと、空白・スピナー・スケルトンで CLS を Chromium で実測した結果
  • アニメーションの負荷の違い、prefers-reduced-motion、React・Tailwind CSS での書き方と使うべきでない場面

2026年10月29日更新|Chromium 153・React 19.2・react-loading-skeleton 3.5.0・Tailwind CSS 4.3 で実行確認

スケルトンスクリーンは、YouTube や SNS のタイムラインを開いた直後に一瞬見える、灰色の四角や線が並んだ画面のことです。中身が届くと、同じ位置に本物の画像や文字が入れ替わって表示されます。この記事では、意味と使いどころを整理したうえで、127.0.0.1 に立てたテスト用サーバーと Chromium で「本当にずれを防げるのか」「アニメーションは重くないか」を計測した結果を載せます。

flowchart TD
    B{1秒未満?} -->|はい| C[出さない]
    B -->|いいえ| D{形が決まる?}
    D -->|はい| E[スケルトン]
    D -->|いいえ| F{進捗が分かる?}
    F -->|はい| G[プログレス<br>バー]
    F -->|いいえ| H[スピナー]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
スケルトンスクリーンとは 読み込み中に完成形の骨組み(灰色の枠)を先に出す表示 スケルトンスクリーンとは(場面別の意味の表)
スケルトンとスピナーの違い スケルトンは「形」を、スピナーは「処理中であること」だけを伝える スピナー・プログレスバーとの違い
skeleton loading の CSS 灰色の背景+疑似要素の光を transform で流す CSSだけでスケルトンスクリーンを作る
CLS(レイアウトのずれ)は防げる? 寸法が合っていれば 0。合っていないと残る(実測 0.0564) CLSを実測
体感速度は必ず上がる? 上がるとは限らない。スピナーより遅く感じたという実験もある 効果と研究
アニメーションは重くない? background-position を動かすと 3秒で約360回の再描画。transform なら 0回 アニメーション3種の負荷
React・Next.js での書き方 Suspense の fallback や loading.js にスケルトンを置く React・Next.jsでのスケルトン
使うべきでない場面 1秒未満の読み込み、形が決まらない画面、アップロードなどの処理 使うべきでない場面(判断表)

確認環境:Ubuntu 24.04.5 LTS の上で、Node.js 22.23.4 で書いたテスト用サーバーを 127.0.0.1 だけで待ち受けさせ、Playwright 1.63.0 が同梱する Chromium 153.0.8010.12(ヘッドレス)で開いて計測しました。API の応答を 100〜3,000ミリ秒遅らせ、「空白のまま待つ」「スピナー」「寸法を合わせたスケルトン」「寸法が違うスケルトン」「画像に width・height が無いスケルトン」を同じ内容(カード3枚)で比べています。CLS・LCP・FCP は PerformanceObserver、再描画の回数は Chromium のトレース(devtools.timeline)、メインスレッドの時間は Chrome DevTools Protocol の Performance.getMetrics、読み上げで見える内容はアクセシビリティツリー(Accessibility.getFullAXTree)で取りました。React 19.2.8・react-dom 19.2.8・react-loading-skeleton 3.5.0・Tailwind CSS 4.3.3(@tailwindcss/cli)は作業用のディレクトリに npm で入れています。Next.js の loading.js は公式ドキュメントで、効果の研究は Viget と Nielsen Norman Group の公開記事で、aria-busy と CLS の定義は MDN と web.dev で確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「スケルトンスクリーンとは|スピナーとの違いとCSSの作り方・CLS実測」

スケルトンスクリーンとは(場面別の意味の表)

この節では、「スケルトン」という言葉がふだんの会話・Web の画面・プログラミングでそれぞれ何を指すのかと、スケルトンスクリーンの正確な意味が分かります。

スケルトンスクリーン(skeleton screen)は、読み込み中の画面に、これから表示される内容の「骨組み」だけを先に描いておく UI のパターンです。 skeleton は英語で「骸骨・骨組み」の意味で、画像の場所に灰色の四角、見出しや文章の場所に灰色の横線を置きます。データが届いたら、同じ場所に本物の画像や文字を入れ替えます。

「スケルトン」はカタカナ語として、IT 以外でもよく使われます。検索して話がかみ合わないときは、たいてい次の表のどれを指しているかが違います。

場面 「スケルトン」が指すもの 例
ふだんの会話 骸骨、または中が透けて見えるもの スケルトンの腕時計(機械が見える)、スケルトン仕様の家電
住宅・不動産 内装や設備が無い、建物の骨組みだけの状態 スケルトン渡し、スケルトン・インフィル
Web・アプリの画面 読み込み中に出す灰色の仮の枠(この記事の題材) スケルトンスクリーン、スケルトンローディング、スケルトンUI
プログラミング 中身を書く前の、形だけのひな形のコード スケルトンコード、スケルトンプロジェクト
3DCG・ゲーム キャラクターを動かすための骨(ボーン)の構造 スケルトンメッシュ、リギング

Web の画面での呼び名は、スケルトンスクリーン・スケルトンローディング(skeleton loading)・スケルトンUI・スケルトンローダーのどれも同じものです。英語の検索では「skeleton loader」「skeleton screen」が多く使われます。

スケルトンスクリーンを作る部品

スケルトンスクリーンは、だいたい次の4つの部品を組み合わせて作ります。

1

画像の枠

写真やサムネイルの場所に、完成時と同じ縦横比の灰色の四角を置く。

aspect-ratio で比率を固定

2

文字の線

見出しや本文の行の場所に、細い灰色の横線を置く。最後の行は短くする。

行の高さを本物に合わせる

3

丸い枠

アイコンやプロフィール画像の場所に、灰色の円を置く。

border-radius: 50%

4

動き(任意)

光が流れる・明滅するアニメーションで「読み込み中」を伝える。

動きを減らす設定では止める

スケルトンスクリーンとスピナー・プログレスバーの違い

この節では、読み込み中の表示(ローディング表示)の種類ごとの違いと、どれを選ぶかの目安が分かります。

スケルトンは「これからこういう形の内容が出ます」と予告し、スピナーは「処理中です」とだけ伝え、プログレスバーは「どこまで進んだか」を伝えます。 どれが優れているかではなく、伝えたいことと待ち時間で選びます。

スケルトンスクリーン

完成後の配置が先に分かる

枠の寸法が合えば、表示が切り替わってもずれない

画面全体やカード一覧など、形が決まった内容向き

スピナー

処理中であることだけを伝える

小さいので、終わったあとに周りが動きやすい

ボタンの送信中など、部分的で短い処理向き

Nielsen Norman Group の記事「Skeleton Screens 101」(2023年6月公開)は、待ち時間の目安を次のように整理しています。

表示 伝えること 向く待ち時間(NN/g の目安) 向く場面
何も出さない ― 1秒未満 すぐ終わる読み込み。出すと点滅に見える
スケルトンスクリーン 完成後の形 10秒未満 ページ全体の読み込み、一覧・カード・プロフィール
スピナー 処理中であること 2〜10秒程度 1つの部品の読み込み、送信・保存
プログレスバー 進み具合・残り 10秒を超える アップロード・ダウンロード・変換

同じ記事は、ヘッダーやフッターなどの外枠だけを出す「フレームだけのスケルトン」は、長く待たされると止まっているように見えるため勧めていません。中身の形まで描くのが基本です。

スケルトンスクリーンの効果と研究(体感速度は必ず上がるか)

この節では、スケルトンスクリーンで「速く感じる」と言われる理由と、それに反する実験結果が分かります。

スケルトンスクリーンは、待っている間に完成形の配置を先に見せることで、読み込みの途中でも「もう始まっている」と感じさせるのがねらいです。 配置が先に頭に入るので、内容が届いたときに目で探し直す手間も減ります。ただし、どんな場面でも体感速度が上がるわけではありません。

Viget の実験ではスケルトンが最も遅く感じられた

Web 制作会社 Viget が2017年10月に公開した「A Bone to Pick with Skeleton Screens」では、モバイルの利用者136人に、同じ長さの読み込み表示(スケルトン・スピナー・空白のどれか1つ)を見せ、体感を聞いています。

表示 人数 「早く読み込まれた」に同意 体感の待ち時間(平均) 課題の完了時間
スケルトン 39人 59% 2.82秒 10.54秒
スピナー 39人 74% 2.41秒 9.49秒
空白 58人 66% 2.29秒 9.50秒

記事の著者は、見慣れない表示が注意を引いた可能性、見慣れた画面でこそ効く可能性、ごく短い待ち時間でしか効かない可能性を挙げ、「スケルトンは万能薬ではない」とまとめています。条件が1つの実験なので、これだけで「スケルトンは逆効果」とは言えませんが、入れれば必ず速く感じるものではないことは押さえておきたい点です。

実際の速さ(LCP)は変わらない

手元の計測でも、スケルトンを出しても内容が出る時刻は変わりませんでした。API を1,500ミリ秒遅らせたページで、主要な内容が描かれた時刻(LCP)を比べた結果です。

1,540ms空白のまま待つLCP(API 1,500ms 遅延)
1,556msスピナーLCP(同じ条件)
1,564msスケルトンLCP(同じ条件)

どれも LCP の要素は本文の段落(P)で、灰色の枠は LCP の候補になっていません。web.dev の定義でも、LCP の対象は画像・文字のブロックなどで、背景色だけの箱は含まれません。スケルトンは「見せ方」の工夫で、サーバーや画像を軽くする改善の代わりにはなりません。 差が出たのは、次の節で見る CLS(レイアウトのずれ)のほうです。

CSSだけでスケルトンスクリーンを作る(コピペ用コード)

この節では、ライブラリを使わずに HTML と CSS だけでスケルトンスクリーンを作るコードと、それぞれの指定の意味が分かります。

灰色の箱を本物と同じ寸法で並べ、疑似要素(::after)の光を transform で左から右へ流すのが基本形です。 次のコードは、画像・見出し1行・本文3行のカードを1枚分作ります。テスト用のページで、実際にこの形で計測しました(div を使わず、span を display: block にしています)。

<ul class="news" aria-busy="true">
  <li class="card" aria-hidden="true">
    <span class="sk sk-img"></span>
    <span class="sk sk-title"></span>
    <span class="sk sk-line"></span>
    <span class="sk sk-line"></span>
    <span class="sk sk-line short"></span>
  </li>
</ul>
<p class="visually-hidden" role="status">お知らせを読み込んでいます</p>
/* 本物のカードと同じ寸法にする */
.card h3 { font-size: 16px; line-height: 24px; margin: 8px 0 4px; }
.card p  { font-size: 14px; line-height: 21px; margin: 0; }
.card img, .sk-img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; }

.sk {
  display: block;
  position: relative;
  overflow: hidden;
  background: #e5e7eb;
  border-radius: 4px;
}
/* 行の箱は line-height と同じ高さにし、灰色だけを細く見せる */
.sk-title { box-sizing: border-box; height: 24px; padding: 4px 0;
            background-clip: content-box; margin: 8px 0 4px; width: 70%; }
.sk-line  { box-sizing: border-box; height: 21px; padding: 3.5px 0;
            background-clip: content-box; }
.sk-line.short { width: 60%; }

/* 光が流れる動き(transform なので再描画が起きにくい) */
.sk::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent);
  animation: sk-wave 1.4s linear infinite;
}
@keyframes sk-wave { to { transform: translateX(100%); } }

/* 動きを減らす設定の人には止める(::after も忘れずに) */
@media (prefers-reduced-motion: reduce) {
  .sk, .sk::after { animation: none; }
}
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden;
                   clip: rect(0 0 0 0); white-space: nowrap; }

指定ごとの意味

  • aspect-ratio: 16 / 9:画像の枠の高さを、幅から自動で決めます。画面幅が変わっても、本物の画像と同じ高さになります。
  • 行の高さ=line-height:本文が 14px・行の高さ 21px なら、スケルトンの1行も高さ 21px にします。灰色を 14px に見せたいときは、padding と background-clip: content-box で内側だけ塗ります。
  • display: block:中身の無い span はインライン要素のままだと幅・高さが 0 になり、何も表示されません(後の「つまずき」で実測)。
  • aria-hidden="true":灰色の枠そのものは読み上げても意味が無いので、支援技術から隠します。代わりに role="status" の文で「読み込み中」を伝えます。

データが届いたら入れ替える

入れ替えは、リストの中身を本物に差し替え、aria-busy を false に戻すだけです。外から届いた文字は textContent で入れ、HTML として解釈させないようにします。

const list = document.querySelector('.news');
const status = document.querySelector('[role="status"]');

const res = await fetch('/api/items');
const items = await res.json();

const nodes = items.map((item) => {
  const li = document.createElement('li');
  li.className = 'card';
  const img = document.createElement('img');
  img.src = item.img;
  img.alt = '';
  img.width = 320;   // 寸法を必ず付ける(付けないと読み込み後にずれる)
  img.height = 180;
  const h3 = document.createElement('h3');
  h3.textContent = item.title;
  const p = document.createElement('p');
  p.textContent = item.text;
  li.append(img, h3, p);
  return li;
});
list.replaceChildren(...nodes);
list.setAttribute('aria-busy', 'false');
status.textContent = `お知らせを${items.length}件表示しました`;

読み込みに失敗したときは、スケルトンを出したままにせず、エラーの文と「再読み込み」のボタンに差し替えます。灰色の枠が出続けると、利用者は待ち続けてしまいます。

読み込み表示ごとのCLSを実測(空白・スピナー・スケルトン)

この節では、空白・スピナー・スケルトンで、内容が届いたときのレイアウトのずれ(CLS)がどれだけ違うかが分かります。

寸法を合わせたスケルトンだけが CLS 0 で、空白とスピナーは 0.72〜0.75 の「不良」になりました。 CLS(Cumulative Layout Shift)は、表示中の要素が予期せず動いた量を表す Core Web Vitals の指標で、web.dev では 0.1 以下が良好、0.25 を超えると不良です。

計測したページは、見出し「お知らせ一覧」の下にカード3枚のリスト、その下に高さ 600px の「関連するお知らせ」の区画と段落がある形です。画面は 390×844(スマホ相当)、API の応答は1,500ミリ秒遅らせました。

読み込み中の表示 リストの高さ(前→後) CLS(下に区画あり) CLS(下は段落だけ)
空白のまま待つ 0px → 971px 0.7525 0.0787
スピナー(40px の円) 80px → 971px 0.7229 0.0787
スケルトン(寸法を合わせた) 971px → 971px 0 0
スケルトン(画像 120px 固定・本文1行) 641px → 971px 0.0564 0.0308
スケルトン(本物の img に width・height 無し) 971px → 971px 0.7987 0.5723

CLS は、動いた要素の面積と動いた距離から計算されます。下の段落が小さいときは値も小さく出ますが、広告や「関連記事」などの大きな区画が下にあると、同じずれでも一気に不良の範囲に入ります。

計測に使ったコード

ページの中で PerformanceObserver を使い、layout-shift の値を足しています。操作の直後(500ミリ秒以内)のずれは CLS に数えない決まりなので、hadRecentInput が true のものは除きます。

window.__shifts = [];
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (!e.hadRecentInput) window.__shifts.push({ v: e.value, t: Math.round(e.startTime) });
  }
}).observe({ type: 'layout-shift', buffered: true });

Playwright から読み出した、各モードの記録の一部です(t はずれが起きた時刻のミリ秒)。

mode=blank      {"cls":0.7525,"shifts":"0.7525@1530","lcp":1540,"lcpEl":"P"}
mode=spinner    {"cls":0.7229,"shifts":"0.7229@1548","lcp":1556,"lcpEl":"P"}
mode=skeleton   {"cls":0,"shifts":"","lcp":1564,"lcpEl":"P"}
mode=bad        {"cls":0.0564,"shifts":"0.0564@1524","lcp":1540,"lcpEl":"P"}
mode=noimg      {"cls":0.7987,"shifts":"0.2673@1540 0.5314@1554","lcp":1556,"lcpEl":"P"}

1スピナーは空白とほぼ同じ

スピナーは高さ 80px の場所しか取らないので、内容が届くと下の区画が 891px 押し下げられた。CLS の点では空白と変わらない。

2寸法違いは小さく残る

画像の枠を 120px に固定し本文を1行にしたスケルトンは、カード1枚が 203px と本物の 313px より低く、0.0564 のずれが残った。

3img の寸法忘れが最悪

スケルトンの寸法が正しくても、本物の img に width・height が無いと、画像が届いた瞬間に2回目のずれ(0.5314)が起き、空白より悪い 0.7987 になった。

スケルトンの寸法を本物に合わせる方法(画像・行の高さ)

この節では、スケルトンと本物の高さがずれる原因と、ずれを数値で確かめる方法が分かります。

CLS が 0 でも、スケルトンの高さが本物と合っているとは限りません。 最初に作ったスケルトンは、本文の線を2本にし、線の高さを 14px・余白で調整していました。CLS は 0 と出ましたが、カードの高さを測ると違っていました。

版 カード1枚の高さ リスト全体(前→後) CLS(画面 390×844) CLS(画面 390×1100)
最初の版(線2本・余白で調整) 292px(本物 313px) 908px → 971px 0 0.0045
直した版(線3本・行の高さ 21px) 313px(本物 313px) 971px → 971px 0 0

最初の版で CLS が 0 だったのは、ずれた段落が画面の外(844px より下)にあったからでした。縦に長い画面(1100px)で開くと、同じページで 0.0045 のずれが出ました。スマホの画面1つで CLS 0 を確かめただけでは足りず、要素の高さを前後で測るのが確実です。

高さを測るコード

// 読み込み中と読み込み後に、それぞれ実行して比べる
const h = (sel) => Math.round(document.querySelector(sel).getBoundingClientRect().height);
console.log({ list: h('.news'), card: h('.news > li') });

寸法を合わせるときの手順

1

本物の部品の寸法を書き出す

画像の縦横比、見出しと本文の font-size・line-height・margin、行数を DevTools の Computed で控える。行数は、想定する文字数と画面幅で何行になるかを数える。

今回は本文3行(約50字・幅310px)

2

スケルトンの箱を同じ寸法にする

画像は aspect-ratio、行は line-height と同じ高さの箱にする。灰色を細く見せたいときは padding と background-clip で内側だけ塗り、箱の高さは変えない。

余白で調整すると、margin の相殺で計算が合わなくなりやすい

3

本物の img に width・height を付ける

width と height を付けると、ブラウザは画像が届く前に縦横比を計算して場所を取る。CSS の height: auto と組み合わせれば、幅に合わせて伸び縮みする。

付けないと CLS 0.7987(実測)

4

前後の高さと CLS を測る

画面の高さを変えて2回以上測る。文字数が日によって変わる場合は、いちばん多い場合に合わせるか、カードの高さを固定して「続きを読む」で切る。

DevTools の Performance パネルでも Layout shift を確かめられる

件数が読み込むまで分からない一覧(検索結果など)は、よく出る件数か画面1枚分だけスケルトンを出します。届いた件数が少なければ下が空き、多ければ下に伸びますが、画面の外へ伸びるぶんは CLS に数えられません。

スケルトンのアニメーション3種の負荷を実測(シマー・ウェーブ・パルス)

この節では、スケルトンでよく使う3種類のアニメーションが、ブラウザにどれだけの負担をかけるかが分かります。

background-position を動かす書き方は、3秒間に約360回の再描画が起きました。transform(ウェーブ)と opacity(パルス)は再描画 0回でした。 灰色の箱15個(カード3枚分)を、読み込みが終わらない状態で3秒間動かし、Chromium のトレースで数えています。

アニメーション 動かすプロパティ 再描画(Paint) スタイル再計算 メインスレッドの時間
動きなし ― 0回 0回 0.7〜0.8ms
シマー(グラデーションを移動) background-position 358〜360回 178〜180回 161.8〜274.6ms
ウェーブ(疑似要素を移動) transform 0回 0回 3.4〜4.2ms
パルス(明滅) opacity 0回 0回 3.6〜5.9ms
参考:スピナー(回転) transform 0回 0回 3.5〜4.0ms

値は2回計測した幅です。3秒は60コマ毎秒で180コマなので、シマーは毎コマ、スタイルの再計算と再描画をしていることになります。transform と opacity は、ブラウザが描画済みの層を動かす(合成する)だけで済むため、メインスレッドの仕事がほとんど増えません。

光が流れる動きを付けるとき

やってはいけないグラデーションの background-position を keyframes で動かす。毎コマ再描画になり、3秒で約360回の Paint が起きた
→
代わりに::after にグラデーションを描き、transform: translateX で動かす。再描画は0回だった

ネットでよく見るシマーのコードは background-position 型が多いので、書き換えて使う

メインスレッドが読み込み中の描画に使われると、同じ時間に動く JavaScript(届いたデータの処理など)が遅れます。読み込み中にボタンを押したときの反応の遅さ(INP)にも影響しうるので、関連記事「INP改善の方法|Core Web Vitalsの原因別コードと改善前後の実測値」で扱っています。keyframes や transition の書き方そのものは、関連記事「CSS アニメーション|transitionとkeyframesの違いと実用20選」で扱っています。

prefers-reduced-motion とアクセシビリティ(aria-busy・role=status)

この節では、動きを減らす設定への対応と、スクリーンリーダーでスケルトンがどう伝わるかが分かります。

OS で「視差効果を減らす」「アニメーションを減らす」を設定している人には、スケルトンの動きを止めます。枠は残したままにします。 CSS の prefers-reduced-motion: reduce で判定できます。Chromium で動きを減らす設定を再現すると、上の CSS ではアニメーション名が none になり、再描画も 0回になりました(前の節の表の「動きなし」と同じ値)。

shimmer+reduce  anim: none / none   Paint 0  TaskDuration(ms) 0.6
wave+reduce     anim: none / none   Paint 0  TaskDuration(ms) 1.0

利用者側の設定は、Mac ならアップルメニュー →「システム設定」→「アクセシビリティ」→「視差効果」→「視差効果を減らす」、iPhone なら「設定」→「アクセシビリティ」→「動作」→「視差効果を減らす」です(Apple サポートの記載)。作る側は、DevTools の「Rendering」パネルにある「Emulate CSS media feature prefers-reduced-motion」で、設定を変えずに確かめられます。

スクリーンリーダーには灰色の枠は何も伝わらない

読み込み中のページをアクセシビリティツリーで見ると、スケルトンの入ったリストは中身が空でした。灰色の箱は文字を持たないので、読み上げる内容がありません。

skeleton 読み込み中  main busy=true  children=0
- heading "お知らせ一覧" [level=1]
- main
- paragraph: このページの下にある文章です。…

spinner 読み込み中  main busy=true  children=1
  status node name= 読み込み中
- main:
  - status "読み込み中"

スピナーには role="status" と aria-label="読み込み中" を付けていたので「読み込み中」が伝わりました。スケルトンでも同じように、見えない文で状態を伝えます。

  • aria-busy="true":MDN によると、要素が更新中であることを示し、支援技術は更新が終わるまで変更を利用者に伝えるのを待つべきとされます。終わったら false に戻します。
  • role="status":「読み込んでいます」「10件表示しました」などの短い文を入れると、操作を邪魔せずに読み上げられます。
  • aria-hidden="true":灰色の箱に付け、意味の無い要素を読み上げの対象から外します。

色は、背景が白なら #e5e7eb 前後の薄い灰色が一般的です。灰色の枠は操作できる部品ではないので文字ほどのコントラストは求められませんが、ダークモードでは背景より少し明るい灰色に変えます。

速い読み込みでのちらつき対策(遅延表示と最低表示時間)

この節では、読み込みが速いときにスケルトンが一瞬だけ出て「ちらつく」問題と、その対策の効果と代償が分かります。

すぐにスケルトンを出す方法では、100ミリ秒で終わる読み込みでも 106ミリ秒だけ灰色の枠が見えました。 NN/g が「1秒未満なら何も出さない」としているのは、この一瞬の切り替わりが点滅に見えるためです。よく使われる対策は「300ミリ秒たっても終わらなければ出す」「出したら最低500ミリ秒は出しておく」の組み合わせです。3つの書き方を、API の遅れを変えて比べました。

API の遅れ すぐ出す:枠が見えた時間/内容が出た時刻 遅らせて出す(場所は確保):枠/内容 遅らせて挿入(場所なし):CLS
100ms 106ms / 121ms 出ない / 122ms 0.7525
250ms 279ms / 295ms 出ない / 269ms 0.7525
400ms 415ms / 438ms 501ms / 825ms 0.7525
800ms 823ms / 851ms 507ms / 829ms 0.7525
3,000ms 3,007ms / 3,026ms 2,708ms / 3,026ms 0.7525

表から分かることは3つです。

  1. 1遅らせて出すと、短い読み込みのちらつきは消える100ms・250ms では枠が一度も出なかった
  2. 2最低表示時間は、内容を遅らせる代償がある400ms で届いた内容が 825ms まで待たされた(すぐ出す方法は 438ms)
  3. 3遅らせるときも、場所は最初から確保する後から枠を挿入すると、枠を入れた瞬間に CLS 0.7525 が出た

場所を確保して遅らせる書き方は、スケルトンの HTML を最初から置いておき、visibility: hidden で隠しておいて、300ミリ秒後に見せる形です。

// .sk-wait .sk { visibility: hidden; } を CSS に書いておく
const list = document.querySelector('.news');
list.classList.add('sk-wait');
let shownAt = null;
const timer = setTimeout(() => {
  list.classList.remove('sk-wait');  // 300ms たっても終わらなければ見せる
  shownAt = performance.now();
}, 300);

const items = await (await fetch('/api/items')).json();
clearTimeout(timer);
if (shownAt !== null) {
  const rest = 500 - (performance.now() - shownAt);  // 出したら最低500ms
  if (rest > 0) await new Promise((r) => setTimeout(r, rest));
}
render(items);  // 入れ替えの処理(前の節のコード)

最低表示時間を付けるかどうかは、ちらつきと待ち時間のどちらを避けたいかで決めます。300〜500ミリ秒で終わることが多い画面では、最低表示時間を短く(200ミリ秒など)するか、付けない選択もあります。

React・Next.jsでのスケルトン(Suspense・loading.js・react-loading-skeleton)

この節では、React と Next.js でスケルトンを出す仕組みと、ライブラリ react-loading-skeleton の出力が分かります。

React では、データを待つ部品を Suspense で囲み、fallback にスケルトンの部品を渡します。 Next.js の App Router では、フォルダーに loading.js を置くと、そのページ全体が自動で Suspense に囲まれ、loading.js の中身が読み込み中に出ます。公式ドキュメントも「スケルトンやスピナーを置ける」と書いています。

import { Suspense } from 'react';

function NewsSkeleton() {
  return (
    <article className="card" role="status" aria-label="読み込み中">
      <span className="sk sk-img" aria-hidden="true" />
      <span className="sk sk-title" aria-hidden="true" />
    </article>
  );
}

export default function Page() {
  return (
    <>
      <h1>お知らせ一覧</h1>
      <Suspense fallback={<NewsSkeleton />}>
        <News />   {/* use(promise) などでデータを待つ部品 */}
      </Suspense>
    </>
  );
}

サーバーから先にスケルトンが届く様子

同じ形の部品を React 19.2.8 の renderToPipeableStream でサーバー描画し、データを800ミリ秒遅らせて、届いた順に記録しました。

[+24ms] 353B: <!DOCTYPE html><html><head></head><body><h1>お知らせ一覧</h1><!--$?--><template id="B:0"></template><article class="card" role="status" aria-label="読み込み中"><span class="sk sk-img" aria-hidden="true"></span><span class="sk sk-title" aria-hidden="true"></span></article><!--/$-->…
[+816ms] 926B: (hidden 属性付きの要素 S:0 に本物のリスト、続けて差し替え用の script)
[+817ms] 14B: </body></html>

最初の 24ミリ秒で見出しとスケルトンが届き、816ミリ秒後に本物の内容と、スケルトンを差し替える小さな script が同じ通信の続きとして届きました。JavaScript の読み込みを待たずにスケルトンが表示されるので、ブラウザ側で fetch してから枠を出すより早く枠が見えます。Suspense と lazy の書き方は関連記事「React Suspenseとlazyの使い方|コード分割とuseフック対応」、Vue での同じ仕組みは関連記事「Vue Suspenseと非同期コンポーネントの使い方|読み込み中表示」で扱っています。

react-loading-skeleton 3.5.0 の出力

自分で CSS を書かない場合は、react-loading-skeleton がよく使われます。<Skeleton count={2} /> と円形の枠をサーバー描画すると、次の HTML になりました。

<span aria-live="polite" aria-busy="true"><span class="react-loading-skeleton">‌</span><br/><span class="react-loading-skeleton">‌</span><br/></span>
<span aria-live="polite" aria-busy="true"><span class="react-loading-skeleton" style="width:40px;height:40px;border-radius:50%">‌</span><br/></span>

付属の skeleton.css を読むと、光の動きは ::after の transform(translateX)で作られ、@media (prefers-reduced-motion) で疑似要素を非表示にして動きを止める作りでした。前の節の実測で負荷が小さかった書き方と同じです。aria-busy と aria-live も自動で付きます。

import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';  // 読み込み忘れると灰色にならない

function NewsCard({ item }) {
  return (
    <article className="card">
      {item ? <img src={item.img} alt="" width={320} height={180} /> : <Skeleton style={{ aspectRatio: '16 / 9' }} />}
      <h3>{item ? item.title : <Skeleton width="70%" />}</h3>
      <p>{item ? item.text : <Skeleton count={3} />}</p>
    </article>
  );
}

この書き方は、h3 や p の中に Skeleton を置くので、行の高さが本物の文字と同じになりやすいのが利点です。

Tailwind CSSでのスケルトン(animate-pulse)

この節では、Tailwind CSS でスケルトンを作るクラスと、生成される CSS の中身が分かります。

Tailwind CSS では、灰色の背景(bg-gray-200 など)と animate-pulse を組み合わせるのが定番です。 Tailwind CSS 4.3.3 でビルドすると、animate-pulse は opacity を 0.5 まで下げて戻す明滅(パルス)として出力されました。opacity なので、前の節の実測どおり再描画は起きにくい書き方です。

<article class="rounded-lg border p-3">
  <span class="block aspect-video rounded-md bg-gray-200 motion-safe:animate-pulse"></span>
  <span class="mt-2 block h-4 w-3/4 rounded bg-gray-200 motion-safe:animate-pulse"></span>
</article>

ビルドした CSS のうち、関係する部分の抜粋です。

--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;

.animate-pulse {
  animation: var(--animate-pulse);
}
@media (prefers-reduced-motion: no-preference) {
  .motion-safe\:animate-pulse {
    animation: var(--animate-pulse);
  }
}
@keyframes pulse {
  50% {
    opacity: 0.5;
  }
}

animate-pulse だけを書くと、動きを減らす設定の人にも明滅が続きます。motion-safe:animate-pulse と書くと、prefers-reduced-motion: no-preference のときだけ動く CSS になります。 Tailwind CSS の導入や v4 の設定は、関連記事「Tailwind CSS 使い方|v4の導入からつまずき対策まで実行結果つき」で扱っています。

スケルトンスクリーンのよくあるつまずきとエラー

この節では、スケルトンを作ったときに起きやすい問題を、症状・原因・直し方の順に確かめた結果が分かります。

多くは「表示されない」「消えない」「ずれる」「止まらない」の4つに分かれます。 どれも手元の Chromium と Tailwind CSS で再現しました。

症状 原因(実測した値) 直し方
灰色の枠が表示されない 中身の無い span がインラインのまま。大きさは 0x0 だった display: block(か inline-block)と高さを付ける
react-loading-skeleton が透明 skeleton.css を読み込んでいない。背景色 rgba(0, 0, 0, 0)、幅 0 パッケージの dist/skeleton.css を import する(追加後は rgb(235, 235, 235))
hidden を付けたのに本物が見えたまま クラスの display: block が hidden に勝つ。display は block のままだった [hidden] { display: none !important; } を共通の CSS に書く
Tailwind の animate-pulse が効かない `animate-${s}` と組み立てたクラスは生成されない(.animate-pulse の規則 0件) ok ? "animate-pulse" : "" のように完全なクラス名を書く(規則 1件)
動きを減らす設定でも光が流れる .sk だけ止め、::after を止めていない。::after のアニメーション名は w のまま、動いているアニメーション 1件 @media の中で .sk::after も animation: none にする(0件になった)
読み込み後に画面がガクッと動く 本物の img に width・height が無い。CLS 0.7987 width・height か aspect-ratio を付ける
スケルトンが一瞬だけ見えてちらつく 100ms の読み込みで 106ms だけ枠が出た 300ms たってから見せる(場所は最初から確保)
失敗したのに枠が出続ける fetch の失敗時に入れ替えていない catch でエラーの文と再読み込みボタンに差し替え、aria-busy を false に戻す

React で Math.random() を使って線の幅をばらつかせると、サーバーとブラウザで違う HTML になり、ハイドレーション(サーバーの HTML に React をつなぐ処理)の不一致の原因になります。幅のばらつきは、決まった値の配列(70%・90%・60% など)から選ぶのが安全です。

スケルトンスクリーンを使うべきでない場面(判断表)

この節では、スケルトンを使うか、スピナー・プログレスバー・何も出さない、のどれにするかを場面ごとに判断できます。

スケルトンが向くのは「形が決まっていて、1〜10秒ほどかかる、ページや一覧の読み込み」です。 それ以外は、別の表示のほうが伝わりやすくなります。

場面 おすすめの表示 理由
タイムライン・商品一覧・記事一覧の初回表示 スケルトン カードの形が決まっていて、ずれも防げる
プロフィール・ダッシュボードの画面全体 スケルトン 配置が先に分かり、目で探し直さずに済む
ほとんど1秒未満で終わる読み込み 何も出さない(または300ms 後に出す) 一瞬の枠が点滅に見える
ボタンを押したあとの送信・保存 ボタン内のスピナー+ボタンを無効にする 形が変わらない処理で、押した場所の近くに出すほうが分かる
ファイルのアップロード・変換・書き出し プログレスバー(割合・残り時間) 進み具合を伝えられ、10秒を超えやすい
検索結果など、形や件数が読み込むまで分からない 画面1枚分だけスケルトン、または控えめなスピナー 多めに出すと、届いた後に大きく空く
エラーになることが多い通信 スケルトン+失敗時の表示を必ず用意 枠が出たままだと、待ち続けさせてしまう
静的な HTML だけのページ 使わない 待つ処理が無い。画像は width・height で場所を取る

入れる前に確かめること

よくある質問

この節では、スケルトンスクリーンについてよく聞かれる疑問への答えが分かります。

Q. スケルトンスクリーンとスケルトンUI・スケルトンローディングは違うものですか?

A. 同じものです。読み込み中に灰色の骨組みを出す表示を、呼び方を変えて言っています。英語では skeleton screen・skeleton loader・skeleton loading と書かれます。

Q. スケルトンスクリーンを入れると、ページは速くなりますか?

A. 実際の速さは変わりません。手元の計測では、LCP は空白 1,540ms・スピナー 1,556ms・スケルトン 1,564ms とほぼ同じでした。変わるのは待っている間の見え方と、レイアウトのずれ(CLS)です。

Q. 体感速度は必ず上がりますか?

A. 必ずではありません。Viget の実験(136人)では、スケルトンを見た人の体感の待ち時間が平均 2.82秒で、スピナー 2.41秒・空白 2.29秒より長くなりました。見慣れた画面で、形が合ったスケルトンほど効きやすいと考えられています。

Q. 何秒以上かかるときに出すべきですか?

A. NN/g は、1秒未満なら何も出さず、10秒未満ならスケルトン、10秒を超えるならプログレスバーを目安にしています。すぐ終わることも多い画面では、300ミリ秒たっても終わらないときだけ見せる書き方が使えます。

Q. スケルトンを入れれば CLS は改善しますか?

A. 枠の寸法が本物と同じなら改善します。手元では、空白 0.7525・スピナー 0.7229 に対して、寸法を合わせたスケルトンは 0 でした。寸法が違うと 0.0564 が残り、本物の img に width・height が無いと 0.7987 と空白より悪くなりました。

Q. ライブラリを使わずに作れますか?

A. 作れます。灰色の箱と ::after の光のアニメーションだけで、この記事の CSS は50行ほどです。React なら react-loading-skeleton、Tailwind CSS なら animate-pulse を使うと、動きを減らす設定への対応なども含めて短く書けます。

Q. シマー(光が流れる)とパルス(明滅)はどちらがよいですか?

A. 見た目の好みで選んでよく、負荷はどちらも transform か opacity で書けば小さくなります。手元の計測では、background-position で動かすシマーだけが3秒で約360回の再描画を起こしました。

Q. SEO に悪い影響はありますか?

A. スケルトン自体は検索順位を下げる要素ではありませんが、本文をブラウザ側の fetch だけで入れると、検索エンジンが読む HTML に本文が無い形になります。Next.js の loading.js のようにサーバーから続けて本文を送る方式なら、公式ドキュメントは SEO に影響しないとしています。CLS は Core Web Vitals の1つなので、ずれを防ぐ点では良い方向に働きます。

Q. スマホアプリ(iOS・Android)でも使われますか?

A. 使われます。Web と同じく、一覧やプロフィールの読み込み中に灰色の枠を出す作りがよく見られます。考え方(寸法を合わせる・短い読み込みでは出さない・動きを減らす設定で止める)は同じです。

まとめ

  • スケルトンスクリーンは、読み込み中に完成後のレイアウトをなぞった灰色の枠を先に出す表示。スケルトンUI・スケルトンローディングも同じもの。ふだんの「スケルトン」(透けて見える・骨組み)とは使う場面が違う。
  • 実際の速さ(LCP)は変わらない。変わるのは見え方とレイアウトのずれで、寸法を合わせたスケルトンは CLS 0、空白は 0.7525、スピナーは 0.7229 だった。
  • 枠の高さは、画像は aspect-ratio、文字は line-height と同じ箱で合わせ、本物の img に width・height を付ける。CLS 0 でも画面の外でずれていることがあるので、高さを前後で測る。
  • 光の動きは transform(ウェーブ)か opacity(パルス)で書く。background-position は3秒で約360回の再描画を起こした。prefers-reduced-motion では ::after まで止める。
  • 1秒未満の読み込み・形が決まらない画面・アップロードには向かない。遅らせて出すときも場所は最初から確保し、aria-busy と role="status" で読み込み中と完了を伝える。

参考・出典

確認日はいずれも 2026年10月29日です。

  • Nielsen Norman Group「Skeleton Screens 101」:https://www.nngroup.com/articles/skeleton-screens/
  • Viget「A Bone to Pick with Skeleton Screens」:https://viget.com/articles/a-bone-to-pick-with-skeleton-screens
  • web.dev「Cumulative Layout Shift (CLS)」:https://web.dev/articles/cls
  • web.dev「Largest Contentful Paint (LCP)」:https://web.dev/articles/lcp
  • MDN「aria-busy」:https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy
  • MDN「prefers-reduced-motion」:https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-reduced-motion
  • MDN「LayoutShift」:https://developer.mozilla.org/ja/docs/Web/API/LayoutShift
  • Next.js Docs「loading.js」:https://nextjs.org/docs/app/api-reference/file-conventions/loading
  • React「<Suspense>」:https://react.dev/reference/react/Suspense
  • react-loading-skeleton(npm):https://www.npmjs.com/package/react-loading-skeleton
  • Tailwind CSS「animation」:https://tailwindcss.com/docs/animation
  • Apple サポート「Mac で画面上の動きを止める/減らす」:https://support.apple.com/ja-jp/guide/mac-help/mchlc03f57a1/mac
  • Apple サポート「iPhone の画面の動きを減らす」:https://support.apple.com/ja-jp/111781

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「スケルトンスクリーンとは|スピナーとの違いとCSSの作り方・CLS実測」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ
参加受付中

AI駆動開発、 無料セミナー