WordPressサイトのCLS(Cumulative Layout Shift)が 0.704 という壊滅的な値から、最終的に 0 まで改善できました。

世の中のCLS改善記事に出てくる「画像サイズ指定」「Webフォント対策」「広告挿入を避ける」といった定番対策は、今回ほとんど役に立ちませんでした。実際の犯人は、パンくずリストの遅延CSS目次の動的生成、そして Content Viewsプラグインの裏側の挙動でした。さらにLiteSpeed Cacheのキャッシュ状態によって計測結果が乱高下し、何度も振り回されました。

原因特定までに紆余曲折があったので、その実録を時系列で残します。同じくCLSで悩んでいる方の参考になれば幸いです。


    スタート地点:CLSがなぜか改善しない

    前回のLCP・TBT改善(前記事)でPerformanceスコアは→57まで戻せたものの、CLSだけが頑固に悪い数値のままでした。

    GTmetrixで計測したWeb VitalsのCLS値

    GTmetrixで計測してみると、LCPやTBTは緑だが CLSが 0.23(基準は0.1以下)。Google PageSpeed Insightsでも計測ごとに 0〜0.7 でバラついて、安定しません。

    定番の対策はひととおり試しました。

    • 画像・動画に width / height を明示
    • Webフォントに font-display: optional を指定し、ローカル配信に切り替え
    • 遅延読み込みの広告や埋め込みなし

    しかし数値はほとんど動きません。「教科書通りの対策では届かない原因がある」と判断し、ここから個別に犯人を絞り込んでいきました。


    犯人①:パンくずリストの遅延CSS

    クリティカルCSSへの統合で解決

    Chrome DevToolsの Performance パネルで「レイアウトシフト」を可視化すると、初期描画直後にコンテンツが パンくずリストの高さ分だけ縦にズレていることが確認できました。

    原因は明快で、_breadcrumb.css が外部CSSファイルとして読み込まれていたため、CSSがパースされるまでパンくず領域の高さが 0 として描画され、CSS適用後に「後から押し下げ」が発生していました。

    対策は、パンくずに必要な最小限のスタイルを クリティカルCSSとしてHTMLに直接インラインすることです。

    <style>
    /* breadcrumb */
    .breadcrumb {
      font-size: clamp(0.8125rem, calc(0.8125rem + ((1vw - 0.225rem) * 0.1563)), 0.8750rem);
      /* ... 必要最小限のレイアウトプロパティだけ */
    }
    </style>
    

    このインライン化だけで、CLSが 0.601 → 0.064 まで激減しました。

    パンくずクリティカルCSS適用後のCLS改善

    犯人②:目次(inppend-tocList.js)の動的生成

    枠固定+スクロール流し込み方式へ変更

    パンくずを潰したあと、別ページを計測すると今度は 記事中の目次部分でレイアウトシフトが発生していることがわかりました。

    目次は inppend-tocList.js という自作スクリプトで、ページ内の h2h4 を走査して ul.tocList に流し込む仕組みです。JSが動くまで <ul class="tocList"></ul> は空のままで高さがゼロ。JS実行後に項目が一気に挿入されて高さが伸びる、というのが典型的なCLSパターンでした。

    対策として、目次のHTML側で 高さを固定し、内容が溢れたらスクロールで見せる方式に変更しました。

    .tocList {
      height: 18rem;
      overflow-y: auto;
    }
    

    これで、目次項目の数に関係なく初期描画時の高さが固定され、JS実行後にも高さが変動しなくなりました。


    犯人③:Content Viewsプラグイン(本命)

    パンくずと目次を潰しても、まだCLSが計測ごとに 0〜0.7 で安定しません。とくに 教科書通りの対策が全部効かないのがおかしい。これは「動的にDOMを挿入している何か」が背後にいると判断しました。

    プラグインを1つずつ無効化して犯人を特定

    仮説検証として、まず必要最小限のプラグインだけ有効化した状態で計測すると、CLSが一発で 0 になりました。原因はプラグインのどれかで確定です。

    そこから、プラグインを少しずつ有効化していきます。

    有効化したプラグインCLS判定
    VK All in One Expansion Unit / VK Link Target Controller0シロ
    + Content Views / Content Views Pro1.03🚨 クロ
    + OneSignal / Rinker / Broken Link Checker変化なしシロ
    + Better Admin Bar変化なしシロ
    WordPress管理画面のプラグイン一覧でContent Viewsを確認

    Content Views および Content Views Pro を有効化した瞬間、CLSが跳ね上がります。これがCLS悪化の本命でした。


    寄り道:LiteSpeed Cacheに惑わされた話

    切り分け中、ややこしい現象がありました。「Content Viewsを有効のままLiteSpeed Cacheを無効化すると、CLSが0になる」というケースが出てきたのです。一瞬「LiteSpeed Cacheが犯人?」と疑いましたが、すぐに辻褄が合わなくなります。

    順番を変えて検証すると、こうでした。

    • キャッシュHIT時:Content Viewsは静的HTMLを返す → CLS = 0
    • キャッシュMISS時(パージ後・初回計測):Content ViewsがAJAXで動的にDOMを挿入 → CLS = 0.704

    つまり、CLSを発生させているのはContent Views本体、それを運良く隠していたのがLiteSpeed Cache、という関係です。

    PageSpeed InsightsはヘッドレスブラウザでアクセスするためクエリやUAが普通のユーザーと違い、キャッシュMISSになりやすい。そのためPageSpeedだけが大きなCLSを出していたわけです。実ユーザーには見えないCLSが、計測ツールには見えていたという構造でした。


    対処:plugin-handles.phpで Content Views を必要ページ以外から除去

    Content Views を完全に止めるわけにはいきません。記事ページの「カテゴリー別関連記事」、ツールページの「ツール一覧」など、サイトのコアな箇所で使っています。

    そこで、Content Viewsを実際に使う固定ページテンプレートのみで有効化し、それ以外のページ(記事ページ含む)ではプラグインのCSS/JSを wp_dequeue する設計にしました。

    テンプレート判定で除外

    Content Views を使うページには専用のテンプレート templates/page-converter.php を割り当て、それ以外では除去する形にしました。

    add_action('wp_print_styles', function () {
      // コンバーター用テンプレート以外で Content Views を無効化
      if (!is_page_template('templates/page-converter.php')) {
        wp_dequeue_style('pt-cv-public-style');
        wp_dequeue_style('pt-cv-public-pro-style');
      }
    }, 100);
    
    add_action('wp_print_scripts', function () {
      // コンバーター用テンプレート以外で Content Views を無効化
      if (!is_page_template('templates/page-converter.php')) {
        wp_dequeue_script('pt-cv-content-views-script');
        wp_dequeue_script('pt-cv-public-pro-script');
      }
    }, 100);
    

    対応後の数値

    dequeue適用後のPageSpeed計測結果 CLS=0

    記事ページのPageSpeed Insightsで、CLSの値が以下のように改善しました。

    指標作業前作業後変化
    CLS(最大値)0.7040✅ 完全解消
    CLS(平均)0.23〜0.70〜0.066✅ 計測ごとの揺れもほぼ消滅

    「教科書では届かない」CLSは、 パンくず(外部CSSの読み込み待ち)目次(JSによる動的DOM挿入)Content Views(プラグインのAJAX描画)という3つの組み合わせが原因でした。


    まとめ:今回得られた教訓

    教科書の対策は出発点に過ぎないこと。画像サイズ、フォント、広告 — これらが原因の場合はもちろん有効ですが、それらをすべて潰しても改善しない場合、 JSやプラグインによる動的なDOM挿入を疑う必要があります。とくに記事一覧プラグイン(Content Views系)、目次自動生成、サイドバーウィジェットの遅延描画あたりが盲点になりがちです。

    PageSpeedと実ユーザーの体感は乖離すること。キャッシュHITで隠れるCLSは、PageSpeedのヘッドレス計測では一発でMISSになって露呈します。逆に言えば、PageSpeedで悪化していても実ユーザーには問題ない、というケースもあります。判断材料としてはCrUX(フィールドデータ)も合わせて見るのが正解です。

    プラグインを1つずつ無効化するのが最強の切り分けであること。今回もコードレベルでの推測は何度も外れましたが、プラグインを順番に無効化する地道な検証が最終的に犯人を特定する決め手になりました。CLSが「教科書通りの対策で改善しない」状況に陥ったら、まず疑うのはプラグインです。

    3つの犯人をすべて潰した結果、CLSは 0.704 → 0 に。一連の改善で、CODE PLUSのPageSpeedスコアは安定して計測できるようになりました。