定義
ローディング中に灰色の骨組み(スケルトン)を表示していたのに、データが到着した瞬間に下のコンテンツが上下にガクッと押されて画面が揺れる——これを防ぎたいときにこの概念を使う。
Cumulative Layout Shift(CLS、ユーザー入力なしに画面要素がどれだけ突然動いたかを累積したスコア) は、ユーザー入力なしに表示中の要素が移動するときに累積されるレイアウト安定性の指標である。ローディングスケルトン(コンテンツが来る前に場所を確保しておく灰色の骨組みUI)は単なる装飾ではなく、最終コンテンツが占めるレイアウトplaceholder(場所予約用の空箱)である。スケルトンの行数・高さ・幅がロード後のUIと異なると、データ到着の瞬間に下のコンテンツが押されてCLSが発生する。
なぜ必要か
テーブル・リスト・カードグリッドは、非同期データの前後でDOMの高さが変わる。スケルトンを短く(例:10行)描き、実際のデータが長い(例:22行)と、fold下だけでなくsummary barやpagination周辺でもshiftが累積する。SSR prefetchでデータが速く来るとスケルトン→コンテンツの遷移がより目立つため、prefetchとスケルトン設計をセットで見なければならない。良いCLSは一般的に0.1以下である。
動作原理
CLSは各layout shiftについて、impact fraction(動いた要素が画面で占めた面積の割合)× distance fraction(その要素が画面の高さに対してどれだけ遠く移動したか)を加算する。スケルトンが実際より小さい箱を占有すると、置き換え時にimpactが大きくなる。animate-pulseのようにopacityだけでなくwidthが変わるplaceholderは、数値・カウント領域でshiftの原因になりやすい。
| 原因 | 典型的な症状 |
|---|---|
| 行数の不一致 | スケルトン10行 → 実際N行、body高さがジャンプ |
| 固定min-height不足 | min-h-[600px] < 実際のテーブル高さ |
| 可変幅テキスト | ローディングバー → 実際の数字の桁数の差 |
| 遅いmount | 空の領域 → 突然テーブル全体が出現 |
実務での適用
- スケルトンの行数 = pagination page size。
- 数値スロット:
min-width+tabular-nums;pulseの代わりに静的なbarを検討。 - 検証:Performance trace
Layout Shifts/ CLS、同一環境で3回reloadのmedian。
トレードオフ
| 選択 | 利得 | コスト |
|---|---|---|
| page size分のスケルトン | CLS ↓ | ローディング時にDOMノードが一時的に増加 |
| 大きなmin-heightを固定 | 安定的 | データが少ないとき空白領域 |
| スケルトン除去(hydrateのみ) | shiftなし | cache miss時に空白画面 |
| spinnerのみ | DOMが少ない | 0→Nの高さshiftの可能性 |
使ってはいけない場合
- 高さが本質的に予測不可能(無限エディタ、未計測の動的コンテンツ)。
- ローディングが極めて短く、prefetchが常に成功してスケルトンがほとんど見えない場合(それでも良いCLSを目標とするなら予約の維持を推奨)。
よくある間違い
- 「スケルトンがあればCLSは安全」——サイズが合わなければむしろ悪化する。
- 固定
min-heightだけを使い、行数を無視する。 - SSR prefetch後に「スケルトンを気にしなくてよい」——query keyのmiss時にflash・CLSが再発する。
関連概念
- critical-rendering-path — layout・paintの順序
- largest-contentful-paint — LCPは良好でもCLSは別
- ssr-hydration-mismatch — 最初のクライアントレンダリングとサーバーHTMLの整合
- ssr-prefetch-query-cache-hydration — prefetchでスケルトンの露出自体を減らす