【実務・中級編】 レイアウトシフト(CLS)の発生メカニズム – Webブラウザの仕組み実践ガイド

やあ。今日も今日とて、ユーザーが快適に使えるピカピカのUIを追求していることだろう。

さて、君は最近、スマホでニュースサイトやECサイトを見ていて、「まさにリンクを押そうとした瞬間、広告が割り込んできやがって、全然関係ないボタンを押しちまった!」という激しい怒りを覚えたことはないだろうか?

あれだ。あれこそが、今回徹底的に解剖するレイアウトシフト(CLS: Cumulative Layout Shift)の正体だ。

中級から一歩抜け出して「真のシニア・フロントエンドアーキテクト」を目指すなら、画面がガタガタと揺れるこの現象を「CSSの調整不足でしょ」などと片付けてはいけない。ブラウザのレンダリングパイプラインの裏側で何が起きているのか、その仕組みを物理レベルで理解し、根本から叩き潰す必要がある。

今日は、ブラウザが裏でどうやってレイアウトを計算し、なぜ動的なコンテンツ挿入がその計算を狂わせるのか、そしてどうやってそれを防ぐのかを、現場のリアルな知見を交えて解説しよう。

—

1. ブラウザの裏側で何が起きているのか?(レンダリングの泥臭い現実)

まずは、ブラウザがHTMLを読み込んで画面にピクセルを描き出すまでの「おさらい」だ。
ブラウザは、ネットワーク経由でHTMLを受け取ると、以下のような過酷なパイプラインを猛烈なスピードで実行する。

1. HTML Parsing(HTMLパース): 文字列をトークン化し、DOM(Document Object Model)ツリーを組み立てる。
2. Style Calculation(スタイル計算): CSSOM(CSS Object Model)を構築し、各DOMノードに適用されるスタイルを確定させる。
3. Layout(レイアウト / リフロー): 各要素が「画面のどこに、どのくらいのサイズで配置されるべきか」の幾何学的情報を計算する。
4. Paint(ペイント): テキストの色やボーダー、影などのピクセルを描画するための指示リストを作る。
5. Compositing(合成): レイヤーを重ね合わせてGPUに送り、画面に映し出す。

この中で、今回主役になるのは「3. Layout(レイアウト)」だ。

レイアウトシフトのトリガーは「後から入ってきたサイズ不明の要素」

ブラウザは、上から下へHTMLを読み進めながらDOMとCSSOMを作り、レイアウトを計算していく。
ここで問題になるのが、JavaScriptによって後から非同期で挿入されるコンテンツだ。例えば、API経由で取得した画像や、広告バナー、遅延ロードされるリッチなウィジェットなどがこれに該当する。

もし、それらの要素の「幅(width)」と「高さ(height)」があらかじめCSSなどで指定されていない場合、どうなるか?

ブラウザの初期レイアウト計算時には、その要素の領域は「高さ0(あるいは中身なし)」として扱われる。
その後、画像やデータがロードされ、実際のサイズが判明した瞬間、ブラウザはこう叫ぶ。

> 「おいおい! さっき計算したレイアウトだと収まらねぇぞ! 下にあるコンテンツを全部押し下げろ!!」

これが、レイアウトシフト(CLS)発生のメカニズムだ。ブラウザは悪くない。サイズを教えてやらなかった僕たちのコードが悪いのだ。

—

2. 現場で使える!CLSを防ぐための実践的プラクティス

この憎きレイアウトシフトを防ぐために、僕たちフロントエンドエンジニアが現場で絶対に守るべき鉄則はただ一つ。

「動的に読み込まれるコンテンツの『予約席(アスペクト比の確保)』をあらかじめCSSで確保しておくこと」だ。

具体的なコードパターンを見ていこう。

パターンA: 画像・メディア要素には `width` と `height`、または `aspect-ratio` を指定する

昔は「レスポンシブだから画像の幅と高さはCSSに任せよう」と言われた時代もあったが、現代のWeb開発においては大間違いだ。HTMLの属性、あるいはCSSでアスペクト比を明示しなければならない。

サンプル画像

ここに記事のタイトルが入ります。

/ 良い例:CSSの aspect-ratio を使って、あらかじめ領域を確保する /
.card img {
width: 100%;
height: auto;
/ 16:9 のアスペクト比をあらかじめブラウザに教えておくことで、画像未ロード時でも高さを確保する /
aspect-ratio: 16 / 9;
object-fit: cover;
background-color: #f0f0f0; / ロード中のプレースホルダー色 /
}

この `aspect-ratio` は本当に神機能だ。これがあるおかげで、ブラウザはレイアウト計算の段階で「この画像は高さがこれくらいになるな」と予測でき、後続の要素を押し下げる必要がなくなる。

—

パターンB: 動的に挿入される広告やウィジェット(Container)の対策

APIから非同期でバナー広告やウィジェットを挿入する場合、挿入先のコンテナ(親要素)のサイズが可変だと必ずレイアウトシフトが起きる。

ここで使えるのが、「スケルトンスクリーン(プレースホルダー)」と「最小高さ(min-height)」の組み合わせだ。

.ad-slot {
width: 100%;
/ 広告の標準的なサイズ(例: 300×250 レクタングル広告)の最小高さをあらかじめ担保する /
min-height: 250px;
background-color: #fafafa;
display: flex;
justify-content: center;
align-items: center;
}

/ スケルトンローダーのスタイル /
.skeleton-loader {
width: 100%;
height: 250px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}

このように、「コンテンツが無くても、そこには一定の空間が存在している状態」をCSSで強制するのが、CLS対策の王道にして最強の手段だ。

—

3. シニアからのアドバイス:測定なしに改善なし

最後に、実務でこの問題を扱う際の心構えを伝えておこう。

「なんとなくレイアウトがズレる気がする」という感覚でコードをいじってはいけない。Google ChromeのDevToolsにある Performanceパネル や、Lighthouse、あるいはWeb Vitalsの計測スクリプトを使って、どこでどの要素がレイアウトシフトを起こしているかを必ず数値として可視化しなさい。

DevToolsの「Layout Shift」イベントを追うと、どのDOMノードがどれだけ移動したのかが赤枠でハイライトされて一発でわかる。犯人は大抵、サイズ指定を忘れた画像や、後から挿入されたバナー広告だ。

Webブラウザのレンダリングの仕組みを理解していれば、「なぜブラウザが再計算(リフロー)せざるを得なかったのか」が手に取るようにわかるはずだ。ブラウザの気持ちになってコードを書けるようになれば、君も立派なフロントエンド・アーキテクトだ。

さあ、今すぐプロダクトのコードベースを開いて、CLSのスコアを「0」に叩き直してやろうじゃないか。

コメント

タイトルとURLをコピーしました