サイトアイコン Capital P

【WV.8】WordPressのJSおよびCSSを最適化する

Core Web Vitalの連載第8回では、JSおよびCSSの最適化について説明しよう。配信の最適化(Gzip圧縮、CDN、ブラウザキャッシュ)についてはすでに第1回「リソース配信の最適化」で説明したのでそちらを参照して欲しい。今回最適化するのは次の項目である。

  1. 不要な読み込みをなくす(特にプラグイン)
  2. CSS・JS自体を分割し、必要な時だけ読み込む。

この作業はかなり面倒くさく、サイトごとにチューニングが必要になる作業なので、運用歴の長い既存サイトで導入するのはかなり骨が折れるだろう。だが、最終的にはこの作業をやらないと「使用していないCSS / JavaScriptの削減」という項目をなくすことができない。

「無駄なサイズを省け」という怒られはかなりの確率で発生する。

なにより、ユーザーに余計なデータをダウンロードさせないことは、ユーザー体験の向上につながるだろう。

不要なJS/CSSの読み込みを停止

まずは不要なCSSとJSの読み込みを停止する方法を紹介しよう。よくひっかかる項目としてはContact Form 7のアセットである。Contact Form 7 でreCAPTCHA V3統合を有効にすると、全ページでreCAPTCHAが読み込まれる。全ページで読み込まれるのはCF7としては意図したものであり、その理由はGoogleが複数のページに表示することを推奨しているからなのだが、とにかく「使わないページではオフにする」という動作に変更してみよう。

まず、大抵のサイトではお問い合わせページは単一のページであることが多い。もちろん、サイドバーやコールトゥアクションなどにお問い合わせフォームを設置することもできるが、とにかく「いまこのページではCF7が必要なのか?」ということを判断する必要がある。このためには「ショートコードやブロックがその投稿の本文になければ不要」と判断するような処理を書いてみよう。

/**
 * 現在のページがCF7のアセットを必要とするか
 * 
 * @return bool
 */
function my_cf7_requires_assets() {
    return is_singular() && has_shortcode( get_queried_object()->post_content, 'contact-form-7' ); 
}

has_shortcode は文字列がショートコードを含んでいるかを判定する関数。 get_queried_object は現在のメインクエリに格納されたオブジェクトを返す関数で、シングルページなら WP_Post を返す。要するに、「シングルページかつCF7のショートコードを含んでいたらtrue」という関数を作ったわけだ。

ではこの関数を使って、全ページで読み込まれるアセットを停止してみよう。

// WordPressへのJS/CSS登録フックの最後で実行。
add_action( 'wp_enqueue_scripts', function() {
    if ( ! my_cf7_requires_assets() ) {
        wp_dequeue_style( 'contact-form-7' );
        wp_dequeue_script( 'contact-form-7' );
        wp_dequeue_script( 'google-recaptcha' );
        wp_dequeue_script( 'wpcf7-recaptcha' );
    }
}, 9999 );

これでお問い合わせフォームのないページでCF7のJS/CSSを読み込むことはなくなる。wp_dequeue_styleはCSS、wp_dequeue_scriptはJavaScriptの読み込みをキャンセルする関数だ。ハンドル名がわからない場合は、以前紹介したQuery Monitorを利用しよう。「これは使っていない」と断言できるもの、そして「大したレイアウトじゃないからテーマ側のCSSで吸収」といった感じで削れるものから削っていこう。

CSSをメインコンテンツとそれ以外にわける

LCPを向上させるには、メインコンテンツを最初に描画完了する必要がある。このためには、サイドバーやフッターなどのスタイルとメインコンテンツエリア(多くはヘッダー+投稿本文)のCSSをわけ、前者の優先度が低いCSSを遅延読み込みにするのがよいだろう。

複数のレイアウトパターンがあって、たとえばこのサイトのように「固定ページは1カラム、記事ページは2カラム」というような場合、固定ページのCSSにサイドバーが含まれているべきではない。この場合、アプローチとしては2種類ある。

  1. SASSなどを利用し、別々のCSSを用意する
  2. コンポーネントごとのCSSを用意し、依存関係で解決する。

前者のパターンだと、SCSSは次のようになる。

// page.scss
@import "header";
@import "content";

// single.scss
@import "header";
@import "content";
@import "sidebar";

これらをそれぞれのページごとに読み込み分けるというわけだ。

続いて、依存関係で解決するアプローチは次の通り。

if ( is_page() ) {
    wp_enqueue_style( 'my-page', $url, [], $version );
} elseif( is_singular() ) {
    wp_enqueue_style( 'my-single', $url, [ 'my-sidebar' ], $version );
} else {
    wp_enqueue_style( 'my-archives', $url, [], $version ); 
} 

ファイル数が増えるオーバーヘッドとブラウザキャッシュによるメリットを秤にかけると、どちらがいいのかを判断するのは骨が折れるが、いずれにせよひとつの style.css を全ページで読み込むよりは改善されるはずだ。

JSの処理を分ける

Core Web Vitalの指標FIB=”First Input Delay”はユーザーに対してインタラクティブになるまでの時間を示している。これはスタイルの素早いレンダリングと、ブロッキングタイムの短いJavaScriptによって向上する。

ラボデータに表示される項目。

筆者がよく見かけるパターンとして、スタイルシート同様、すべてのJavaScriptを app.js などにバンドルして、あらゆる操作をその中で行う全部乗せのロングタスクだ。Core Web Vitalは一つのJavaScriptに許容されるブロッキングタイムを50m秒と定義し、それを超える時間を合算してTBT(=Total Blocking Time) と位置付けている。ロングタスクはTBTが蓄積しやすい処理だ。たとえば、次のようにjQuery.readyで何でもかんでも登録するケースである。

jQuery( document ).ready( function() {
    // スライダーを実行
    $( '.sliders' ).slider();
    // 開閉パネルを実行
    $( '.header-menu' ).each( function( index, menu ) {
        $( menu ).menu();
    } );
    // こんな感じであらゆるタスクが登録されている
    // ....
} );

すべてが必要な処理ならば仕方ないが、極力別のJavaScriptにわけ、不要なページでは読み込まないようにしておこう。

ちなみに、ロングタスクをつきとめるためには、Chrome Dev ToolsのPerformance タブを利用することができる。録画ボタンを押すとパフォーマンスの計測が開始され、停止後に結果を確認できる。

上のグラフで赤いフラグがついたものはすべてロングタスク。スクロールするとズームインしていくので、どの処理に時間がかかっているかがわかる。こうしてみると、HTMLパースやSDKおよび広告タグの読み込みがすべてロングタスクになっていることがよくわかる。

必要なときにだけJS/CSSを読み込む

上で紹介した通り、そもそもCSS/JSを必要なときだけ読み込めばパフォーマンス最適化が測れる。たとえば、次のようなケースを想定してみよう。

こうしたケースでは、すべてのCSSとJSをガッチャンコするのではなく、別々に分割し、存在するものだけ読み込むようにしよう。CF7の例で紹介したように読み込んでもいいが、そもそも register_block_type を使っておけば、使った時だけ読み込まれるようになるWordPress 5.8以降では使った時だけ読み込まれるようになる。

add_action( 'wp_enqueue_script', function() {
    register_block_type( 'my-theme/clipboard', array(
        'script' => 'my-theme-clipboard-script',
        'style'   => 'my-theme-clipboard-style',
    ) );
} );

必要なブロックだけの読み込みを有効にするには、WordPress5.8時点でフィルターフックをかける必要がある。

/**
 * ブロックアセットを分けて読み込み
 */
add_filter( 'should_load_separate_core_block_assets', '__return_true' );

まだ検証できていないのだが、とりあえずコアブロックに関してはこれで「使われているブロックのアセットだけ読み込み」という状態になる。巨大な block-library.css が読み込まれることない。

また、WordPressのバージョンが古かったり、もし上記のコードでもブロックのアセットが読み込まれてしまう場合は、次のように「投稿が該当するブロックをもつかチェックしてから読み込む」という処理を入れてみよう。一個一個やるのは面倒だが。

// スクリプト読み込みのときにチェックする
add_action( 'wp_enqueue_scriipt', function() {
    if ( is_singular() ) {
        foreach ( $myblocks => $block_name => $handle ) {
            if ( has_block( $block_name, get_queried_object() ) ) {
                // シングルページでかつ投稿がブロックを使っている
                wp_enqueue_style( $handle );
            }
        }
    }
} );

ウィジェットの中であればもう少し話は簡単だ。いっそのことウィジェット出力コードで両方読み込んでしまってもよいだろう。

class My_Widget extends WP_Widget {
     
    private static $loaded = false;

    // ウィジェットの中身を表示する関数
    public function widget() {
        // 複数のウィジェットを考慮し、最初の描画の時だけアセットを読み込み。
        if ( ! self::$loaded ) {
            self::$loaded = true;
            wp_enqueue_style( 'my-widget-style' );
            wp_enqueue_script( 'my-widget-script' );
        }
        //  中身を出力
        //...
    }
}

wp_head よりあとに wp_enqueue_* を呼び出した場合、JSの場合はフッターに、CSSの場合は呼び出した場所に出力される。JSは特に問題ないと思うが、linkタグのスタイルシートは body-ok なのだが、推奨される方法ではないので、こだわりのある方は別の方法を模索してみて欲しい。

なんにせよ、ファイルは分割しておいて必要なときに読み込みという手法にすれば、最適化は測れる。

フレームワークを利用している場合は必要なものだけ

Bootstrapのようなフレームワークを利用している場合も「使っていないCSSを削除せよ」というお叱りをうけることが多々ある。これはおっしゃる通りなので、必要なものだけを利用してみよう。まず、デフォルトのBoostrapはこの通り。

// scss-docs-start import-stack
// Configuration
@import "functions";
@import "variables";
@import "mixins";
@import "utilities";

// Layout & components
@import "root";
@import "reboot";
@import "type";
@import "images";
@import "containers";
@import "grid";
@import "tables";
@import "forms";
@import "buttons";
@import "transitions";
@import "dropdown";
@import "button-group";
@import "nav";
@import "navbar";
@import "card";
@import "accordion";
@import "breadcrumb";
@import "pagination";
@import "badge";
@import "alert";
@import "progress";
@import "list-group";
@import "close";
@import "toasts";
@import "modal";
@import "tooltip";
@import "popover";
@import "carousel";
@import "spinners";
@import "offcanvas";

// Helpers
@import "helpers";

// Utilities
@import "utilities/api";
// scss-docs-end import-stack

まず、次の”Configuration”と書いてあるパートは、mixinや関数、変数などが定義されたファイルで、これを読み込まないとBootstrapはコンパイルできない。

@import "functions";
@import "variables";
@import "mixins";
@import "utilities";

つづいて”Components”だが、これらは各パーツである。以下はどのページでも使いそうなもの。

@import "root";
@import "reboot";
@import "type";
@import "images";
@import "containers";
@import "grid";

残りは必要に応じて読み込むような設定にし、別々のCSSとして書き出すことになるのだが……ここで一つの重大な問題が持ち上がってくる。

そもそも、BootstrapのようなCSSフレームワークを利用しているユーザーのうち、こうした使い方をする人は稀だろう。楽をしたいからフレームワークを使っているのであって、その中身を仔細に検討しだしたら全然楽ではないからだ。これはBootstrapに限らずどのCSSフレームワークを使っても同じことがいえる。

もちろん、Bootstrapではテーマ(色の種類、primaryとかsuccessとか)を減らしたり、色々と軽量化する余地はあるが、そんなに簡単な作業ではないことを覚えておこう。「CSSフレームワークを使っていなかったら最初のリリースはできなかったに違いない」などと自分に言い聞かせ、この辛い時間を乗り切って欲しい。

インライン展開すべきか?

さらなる最適化Tipsとして「小さいCSS・JSをインライン展開せよ」というTipsがある。WordPressのようなCMSでは、同じ雛形から複数のコンテンツが作られるので、インライン展開はしなくてもよいというのが筆者の持論だ。複数のページをユーザーが見た場合、インライン展開だと同じコード断片(<style>タグの中身)を毎回ダウンロードする羽目になるが、外部スタイルシートならばブラウザキャッシュが効く。

もちろん、これはサイト全体をトータルで考えた場合の話なので、どうしてもインライン展開すべしという場合はこの限りではない。AutoOptimizeなどのプラグインはCSSのインライン配信に対応している。

まとめ

今回紹介した内容は、コードの修正を多く含むものだった。サイトごとにやり方も異なるし、わりかし辛い作業になるだろう。ここで体験したことを二度と繰り返さないため、「どうせ納品したあとにPSIの点数が低いとか言われるんだから最初から〇〇しておこう」という具合によい知見を溜めていって欲しい。

モバイルバージョンを終了