👏

WordPressで行う速度改善(PageSpeed Insights)

2022/08/02に公開

始めに

Google が Core Web Vitals を導入してからしばらく経ちます。みなさまいかがお過ごしでしょうか。
技術情報に聡い方々の間では React サイトの SSR におけるキャッシュ戦略等で速度改善対応に取り組んでおられると思います。詳しくないので適当に書いています。

さて、WordPress において速度改善対応をするとこの辺かなというところをまとめてみます。
普段やっている対策をまとめます。
WordPress と書いていますが、スクラッチサイト等でも行う内容はほとんど同一だと思います。
そして CDN とか使わずに、出来る範囲の事のみを書いています。

速度改善

まずは測定

変化は目に見えるものが必要です。通常 PageSpeed Insights で確認しています。
その他 Lighthouse 等、色々あると思いますが多くの方が現実的に指標としているのが PageSpeed Insights だと思います。

まずは改善施策前に PageSpeed Insights を実施しておきスコアと内容を把握しておきます。
今回改善するサイトのトップページスコアです。

未対策だとこんなスコアのサイトはたくさんあると思います。

ちなみに速度改善はトップページだけではなく下層ページを含めて、さらには時間帯等によっても異なるので複数日における中央値で判断するべき、という話は WordCamp Tokyo 2016 にて竹洞 陽一郎氏が話してくださりました。(平均は最大と最小の中央なので、つまり遅いレスポンスが見逃される可能性がある)
https://www.slideshare.net/takehora/wordpress-66116820 ※スライド40ページあたり
本来は全ページの測定を異なる時間帯で定期的に計測するべきです。
しかしながら現実的にそこまでモニタリング出来る環境を持つ事は難しいと思いますので、頭に留めておいて頂ければと。

画像の圧縮

まずは利用している全ての画像を圧縮していきます。
お金が無いので TinyPng 等で20枚ずつ圧縮しています。
他に良い方法があると思いますが、画質劣化とサイズ削減でバランス良いので、 jpg も png もまずここで圧縮してサイズ削減しています。

画像の種類等にもよりますが、この段階で20~50%程度、あるいはもう少しサイズ削減出来ると思います。通信を圧迫するのが画像なので、この時点で大きな改善が期待出来ます。

また画像サイズにも注意です。PC向けサイズは今のところ1800px程度をターゲットにしているサイトが多いかと思います。同じサイズをスマホ向けに設定すると too match 大きすぎます。
750~1000px程度が適切だと思います。

同じ箇所をレスポンシブで表示する場合、

<img src="pc.jpg" class="pc-only">
<img src="sp.jpg" class="sp-only">

のようにブレイクポイントで表示の切り替えをする事があるかもしれません。
ただこの仕様の場合、スマホでもPC向け画像が読み込まれるので、マイナスポイントとなります。

<picture>
  <source media="(max-width:768px)" srcset="sp.jpg">
  <img src="pc.jpg">
</picture>

のように設定すれば、ブレイクポイントを境に読み込まれる画像を切り替えられます。

画像サイズの設定

WordPress はテンプレート管理なので、主に header.php と footer.php を編集します。サイトによっては front-page.php もテンプレートで管理しているかもしれません。その場合は front-page.php も対応します。
固定ページ管理の場合は、管理画面からファイルをアップロードすると、自動的に window サイズによる最適化と loading="lazy" が設定されます。可能な限り管理画面から画像ファイルをアップする方法を推奨します。

header.php, footer.php には以下のような画像が設定されていると思います。

<div class="f-logo">
	<a href="<?php echo esc_url(home_url('/')); ?>">
		<img src="<?php echo get_template_directory_uri(); ?>/images/share/logo@2x.png" alt="写真:ロゴ">
	</a>
</div>

このような記述を以下に変更していきます。

<div class="f-logo">
	<a href="<?php echo esc_url(home_url('/')); ?>">
		<img loading="lazy" width="292" height="31" src="<?php echo get_template_directory_uri(); ?>/images/share/logo@2x.png" alt="写真:ロゴ">
	</a>
</div>

loading="lazy" と、width, height を追記しました。
lazyload を行わせるために、ブラウザ実装として、loading="lazy" と記載すれば lazyload してくれます。しかしそのまま lazyload した場合、どのようなサイズのファイルかをブラウザは知らないので、読み込んだ瞬間に画像サイズが確定し、その結果 Layout Shift が起こります。カクッとした表示、画像分コンテンツが下に延長されます。
広告の多いサイトで、広告が差し込まれた結果コンテンツが移動し、クリックしようとしたものとは別のものをクリックしてしまった経験は無いでしょうか?
そのような現象を Layout Shift といいます。
Layout Shift は悪です。可能な限り撲滅するべきです。
少なくとも画像表示はコントロール可能なので、width, height の記述、あるいは aspect-ratio の記載をする事で、Layout Shift を防ぐことが出来ます。

webPへ変換する

WordPress は webP のサポートを開始し、直接 webP ファイルをアップロードする事が可能になっています。またアップロードしたファイルを 自動でwebPに変換する仕様も検討中 です。
手動で変換しても良いのですが、さすがに面倒なので、プラグインによる変換を行います。
推奨はEWWW Image Optimizerです。このプラグインを利用すると、テーマファイル内で読み込んでいる画像も含めてwebP変換を行ってくれます。また webP 非対応ブラウザに対してはオリジナル画像を返す処理も対応していましたが、Safari が対応してくれたのでこの機能はそれほど重要ではなくなりました。

インストール後、設定→「EWWW Image Optimizer」を選択すると、以下のような画面が表示されますが、

赤枠のI know what I'm doing, leave me alone!(俺はやる事わかってるから放っておいてくれ)を選択してください。

次に webP 変換を選択します。

リライトルールを挿入するか聞かれるので、そのままボタンを押してください。

そして設定を保存します。

次に「メディア」→「一括最適化」からスキャンし、webP変換を行います。
画像の多いサイトは時間かかりますが、放置しておけば進行してくれます。
これで webP 変換対応は完了です。

JavaScriptファイルのdefer設定

次に JavaScript ファイルの非同期読み込みを設定します。
そのまま JavaScript ファイルを読み込むと、DOM レンダリングの妨げになったりしますので、初期表示に必要な JavaScript を除いて、非同期読み込みする事で、初期表示を速やかに行う事が出来ます。
WordPress では footer.php による読み込み管理よりも、WordPress 内でキュー管理する関数が用意されており、例えば重複する js ファイルがある場合は整理してくれたりします。
アセットを1箇所で管理出来るので、関数による管理の方を推奨します。

add_action('wp_enqueue_scripts', function(){
	wp_enqueue_script( 'asset', add_filedate('js/asset.js'), array('jquery-core'), NULL );
	wp_enqueue_style('style', get_template_directory_uri() . '/css/sytle.css', array(), null, 'all');
});

wp_enqueue_script で JavaScript ファイルの管理、
wp_enqueue_style で CSS ファイルの管理を行います。
第5引数を true にすると、footer で読み込みます。従来はこの方法が推奨されてきましたが、 defer の場合はダウンロードを先に行うため、むしろ header で読んだ方が最終的に早くなります。

add_filter('script_loader_tag', function ($tag, $handle) {
    if (is_admin() ) return $tag;
    if (!preg_match('/¥b(async|defer)¥b/', $tag)) {
        return str_replace(' src', ' defer src', $tag);
    }
    return $tag;
}, 10, 2);

こちらの記述を行う事で、 script タグに defer が設定されます。async/defer が既に設定されている場合は除外します。

Cumulative Layout Shiftの対応

先ほど Layout Shift への対応については記述しましたが、例えばトップページのスライダーなど動的に変更されるコンテンツの場合は、初期表示で Layout Shift が発生する事があります。Cumulative Layout Shift(累積レイアウトシフト)というスコアが悪くなります。
スライダー表示で悪くなる場合は、一定の(中央値の)横幅サイズにおけるスライダーの高さを設定し、その高さを初期値として設定する事で改善される場合があります。

header.php
<style>
@media screen and (max-width: 800px) and (min-width: 0px) {
	.hero-slider{
		height: 251px;
		overflow: hidden;
	}
}
</style>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
	setTimeout(function(){
		document.querySelector('.hero-slider').style.height = 'auto';
	},10);
});
</script>
<?php endif;?>

このサイトの場合は、開発者ツールで見た値が height: 251px だったので、その値で設定し、DOMContentLoaded 後は height: auto に戻します。これによって Layout Shift を抑制する事が出来ます。

iframe読み込みへの対応

ここまでで画像の圧縮、lazyload、Layout Shift、 script タグの defer 対応と進めてきました。これだけで大幅にスコアアップするサイトも多いです。ただ Youtube や Google Map を読み込んでいたり、 css による背景画像を読み込んでいたりする事で、Core Web Vital が悪化することがあります。

これも色々方法がありますが、個人的には lazysizes.min.jsを推奨しています。この lazysize.min.js をローカルにコピーして読み込み、 iframe の場合には以下のように設定します。

<iframe class="lazyload"
data-src="https://www.google.com/maps/embed" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy"></iframe>

class に lazyload を追加し、 src を data-src に変更します。この設定により ifreame 要素に対しても lazyload がかかります。

ただ、例えば初期読み込みの範囲に Youtube があると、「使用していないJavaScriptの削減」で指摘されたりします。その場合は Youtube サムネイルをクリックすると再生が始まる仕様に変更すれば、大幅に改善します。

<a id="modal-open" href="javascript:;">
	<img loading="lazy" src="<?php echo get_stylesheet_directory_uri() ?>/images/youtube-thumb.jpg" alt="" width="800" height="448">
</a>

<div id="modal-content">
	<div class="inner">
		<div id="player"></div>
	</div>
</div>
functions.php
add_action('wp_enqueue_scripts', function(){
	wp_enqueue_script('youtube-js', get_template_directory_uri().'/js/youtube.js', array('jquery-core'), null);
	wp_enqueue_style('youtube-css', get_template_directory_uri().'/css/youtube.css', array(), null, 'all');
});
youtube.js
jQuery(function($) {
    'use scrict';

    //プレイヤー変数
    var player;

    //オブジェクト生成
    function youtubeAPIInit() {
        var scriptTag = document.createElement('script');
        scriptTag.src = "https://www.youtube.com/iframe_api";
        var fsTag = document.getElementsByTagName('script')[0];
        fsTag.parentNode.insertBefore(scriptTag, fsTag);
        window.onYouTubeIframeAPIReady = function(){
            player = new YT.Player('player', {
                height:'540',
                width:'960',
                videoId:'n1aG6WUWO5Q',
                playerVars:{
                    autohide:1,
                    controls:1,
                modestbranding:1,
                iv_load_policy:3,
                    showinfo:0,
                    rel:0,
                    autoplay:1
                }
            });
        };
    }

    //モーダル
    var modal = {}, $lay, $content;
    modal.inner = function() {
        if($("#modal-overlay")[0]) return false;
        $("body").append('<div id="modal-overlay"></div>');
        $lay = $("#modal-overlay");
        $content = $("#modal-content");
        $lay.fadeIn("slow");
        youtubeAPIInit();
        this.resize();
        $content.fadeIn("fast");
        $lay.unbind().click(function() {
            player.pauseVideo();
            $content.add($lay).fadeOut("fast",function(){
                $lay.remove();
            });
        });
    };

    //リサイズ処理
    modal.resize = function(){
        var $winWidth = $(window).width();
        var $winHeight = $(window).height();
        var $contentOuterWidth = $("#modal-content").outerWidth();
        var $contentOuterHeight = $("#modal-content").outerHeight();
        $("#modal-content").css({
            "left": (($winWidth - $contentOuterWidth) / 2) + "px",
            "top": (($winHeight - $contentOuterHeight) / 2) + "px"
        });
    }

    //クリック処理
    $("#modal-open").click(function(){
        modal.inner();
        // player.playVideo();
    });
    $(window).resize(modal.resize);
});
youtube.css
#modal-content {
  width: 80%;
  margin: 0;
  padding: 0;
  background: #fff;
  position: fixed;
  display: none;
  z-index: 99999;
}

@media only screen and (max-width: 800px) {
  #modal-content {
    width: 95%;
  }
}

#modal-content .inner {
  position: relative;
  width: 100%;
  padding-top: 56.25%;
  overflow: hidden;
}

#modal-content .inner #player {
  position: absolute;
  top: 0;
  right: 0;
  width: 100%;
  height: 100%;
}

#modal-overlay {
  z-index: 9999;
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 120%;
  background-color: rgba(0, 0, 0, 0.75);
}

Google Font対策

さて、ここまでで通常は完了し、PageSpeed Insights のスコアも70以上になることが多いですが、今回のサイトはまだまだスコアが上がりません。

この理由としては、Google fonts による Noto Sans JP を読み込んでいるためと推測出来ました。LCP は最も大きいコンテンツで、通常はメインビジュアルなのですが、今回は日本語フォントファイルを読み込んでいる事もあり、ここが原因でした。
ちなみに、上記のスコアは Google Fonts を preload で先読みし最適化記載の、ローカルフォントとして非同期で読み込んだ場合のスコアでした。preload しても改善されませんでした。
英字フォントの場合は非同期で読み込んだメリットの方が大きいのですが、日本語ファイルの場合は容量が大きくなります。1ウェイトで1.6MBくらいあります。モバイル向けとしては致命的に大きいです。

Google font から読み込んだ場合は、必要なフォント分を分割ダウンロードしてくれるので、CDN配信で問題ないのですが、デフォルトの場合、

<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" rel="stylesheet">

この記述で読み込んだ場合は、レンダリングブロックが発生します。「レンダリングを妨げるリソースの除外」で指摘されます。そのため、先ほどの参考サイトでも利用した webfontsloader を利用します。このスクリプトでは google font をサポートしていますので、以下のように記述します

add_action('wp_enqueue_scripts', function(){
	wp_enqueue_script( 'webfont', '//ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js', array('jquery-core'), NULL );
	wp_enqueue_script( 'asset', add_filedate('js/asset.js'), array('jquery-core', 'webfont'), NULL );

});
WebFont.load({
	google:{
   families:['Noto+Sans+JP:400,500']
 },
  active: function() {
    sessionStorage.fonts = true;
  }
});

結果発表

上記の記述を設定する事で、最終的に以下のスコアになりました。良かったですね🎉

PageSpeed Insights が全てではなく、下層に渡っての平均値、また安定的な配信が望ましいのは言うまでもなく、その場合には CDN の利用なども必要になってくる事があります。
しかし一般的な零細・中小事業者の場合は、アクセス集中することも稀で、程々に提供されていれば良い、そのために最低限のコストでという事が求められます。
上記の対応であれば2~3時間で対応出来るので、最小のコストで最大の結果という意味では参考になるシーンが多いのではないかと思います。

Discussion