レンダリングを妨げるリソースの除外でPageSpeed改善2019

SEOに力を入れているウェブサイトの制作者や運営者なら、PageSpeed InsightsGTmetrix などを利用して、ウェブページの読み込み時間 (表示速度) をチェックしていると思います。

今回は PageSpeed Insights の診断で「レンダリングを妨げるリソースの除外」という項目に引っ掛かってしまった際に、当ウェブサイトで行った施策をご紹介します。

レンダリングを妨げるリソースとは?

「リソース」は直訳すると「資源」を意味します。ウェブサイトにおいてはウェブページを構成するファイルを意味します。

「レンダリングを妨げるリソース」はウェブページに <link rel="stylesheet" href="…" /><script src="…"></script> といったタグで読み込まれる、外部のCSSファイルやJSファイルを指します。

HTMLファイルがブラウザに読み込まれると、レンダリング (描画処理) が行われます。その際に、これらの外部ファイルが見つかると、レンダリングを中断 (レンダリングブロック) して外部ファイルの読み込みが行われます。外部ファイルの読み込みが終わるとレンダリングは再開されます。

なるべく速くウェブページを表示させるには、レンダリングブロックを発生させない必要があります。

外部JSファイルによるレンダリングブロックを防ぐ

複数のJSファイルがあると、その数だけレンダリングブロックが発生する要因になります。なので、まとめられるものはまとめて、ファイルサイズを軽量化 (minify) しておきます。

次に、head要素内にあるhead要素で実行する必要のないscript要素をbody終了タグ (</body>) の前の移します。その際に、async属性やdefer属性を適宜追加します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    …
  </head>
  <body>
    …
    <script src="…" async="async"></script>
    <script src="…" defer="defer"></script>
  </body>
</html>

async属性とdefer属性について

HTML5から追加されたasync属性とdefer属性をscript要素に追加すると、JSファイルが非同期で (HTMLファイルと並行して) 読み込まれレンダリングブロックを抑えることができます。

async属性とdefer属性の有無によるレンダリングの流れは以下のようになります。

script要素にasync属性もdefer属性もない場合
  1. HTMLファイルを読み込む
  2. 外部JSファイルが見つかる
  3. HTMLファイルの読み込みを中断する (レンダリングブロック発生)
    1. JSファイルを読み込む
    2. JSファイルの読み込みが完了する
    3. JSコードを実行する
  4. HTMLファイルの読み込みを再開する
script要素にasync属性がある場合
  1. HTMLファイルを読み込む
  2. 外部JSファイルが見つかる
    1. HTMLファイルと並行してJSファイルを読み込む
    2. JSファイルの読み込みが完了する
  3. HTMLファイルが読み込み中なら中断する (レンダリングブロック発生)
    1. JSコードを実行する
  4. HTMLファイルが読み込み中なら再開する
script要素にasync属性がなくdefer属性がある場合
  1. HTMLファイルを読み込む
  2. 外部JSファイルが見つかる
    1. HTMLファイルと並行してJSファイルを読み込む
    2. JSファイルの読み込みが完了する
  3. HTMLファイルの読み込みが完了する
    1. JSファイルを見つけた順にJSコードを実行する

WordPressで運用しているウェブサイトの場合

WordPressでscript要素を追加するにはwp_enqueue_script関数を使用しますが、このwp_enqueue_script関数の5番目のパラメータ ($in_footer) を true に設定することでbody終了タグの前に設置することができます。

add_action( 'wp_enqueue_scripts', 'my_wp_enqueue_scripts', 10 );
if( !function_exists( 'my_wp_enqueue_scripts' ) ) {
  function my_wp_enqueue_scripts() {
    …
    wp_enqueue_script(
      'JSファイルのハンドル名',
      'JSファイルのURL',
      array( '依存するJSファイルのハンドル名' ),
      'JSファイルのバージョン',
      true
    );
    …
  }
}

また、async属性とdefer属性は、script_loader_tagフックを利用することで追加できます。

add_filter( 'script_loader_tag', 'my_script_loader_tag', 10, 3 );
if( !function_exists( 'my_script_loader_tag' ) ) {
  function my_script_loader_tag( $tag, $handle, $src ) {
    global $pagenow;
    if( !is_admin() && !in_array( $pagenow, array( 'wp-login.php', 'wp-register.php' ), true ) ) {
      switch( $handle ) {

        case 'async属性を追加したいJSファイルのハンドル名';
        $tag = str_replace( '></script>', ' async="async"></script>', $tag );
        break;

        case 'defer属性を追加したいJSファイルのハンドル名';
        $tag = str_replace( '></script>', ' defer="defer"></script>', $tag );
        break;

      }
    }
    return $tag;
  }
}

外部CSSファイルによるレンダリングブロックを防ぐ

外部のCSSファイルを読み込むことでレンダリングブロックは発生するので、HTMLコードにCSSコードを直書きすればレンダリングブロックは発生しなくなります。

<!DOCTYPE html>
<html lang="ja">
  <head>
    …
    <style>/* CSSコード */</style>
  </head>
  <body>
    …
  </body>
</html>

しかし、今回はこの方法を採用しませんでした。その理由には次のようなことが挙げられます。

  • 文章構造 (HTML) とスタイル (CSS) は分離するという原則を無視しているから。
  • 自動生成させる場合、処理が複雑になるから。WordPressなら次のような処理になります。
    1. CSSファイルの更新日時をfilemtime関数で取得する。
    2. get_theme_mod関数でCSSコードの更新日時を取得する。
    3. CSSコードの更新日時がない、もしくはCSSコードとCSSファイルの更新日時が一致しない場合。
      1. CSSファイルの更新日時をCSSコードの更新日時としてset_theme_mod関数で保存する。
      2. CSSファイルのCSSコードをWP_FileSystemなどで取得する。
      3. CSSコードをpreg_replace関数などで修正する。
      4. CSSコードをset_theme_mod関数で保存する。
      5. CSSコードをstyle要素でマークアップして出力する。
    4. CSSコードの更新日時があり、CSSコードとCSSファイルの更新日時が一致する場合。
      1. get_theme_mod関数でCSSコードを取得し、style要素でマークアップして出力する。
  • 将来的には無くなると思われるテクニックだから。

CSSをpreload (先読み) で読み込ませるという方法

今回はレンダリングブロックを発生させずに外部CSSファイルを非同期で先読みさせる <link rel="preload" as="style" href="…" /> を使用しました。

2019年10月現在、SafariやChromeといったwebkit系のブラウザでのみ有効となっているため、IE、Edge、Firefoxなどのブラウザには代替手段が必要ですが、将来的には、このpreloadさせる方法が一般的になりそうです。

注意点として、preloadは読み込みだけを行うので、スタイルを適用させるには別途 <link rel="stylesheet" href="…" /> が必要です。また、スタイルの適用が遅れるため、スタイル適用前のウェブページが一瞬表示される場合があり、その対策も不可欠です。

では、先にHTMLコードをお見せします。後から1つずつ解説していきます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    …

    <!-- ※1 -->
    <style>body{opacity:0;}</style>
    <noscript id="onload-style"><style>body{opacity:1;}</style></noscript>

    <!-- ※2 -->
    <link rel="preload" as="style" href="/style-1.css" onload="if(!document.documentMode)this.rel='stylesheet';" />
    <noscript><link rel="stylesheet" href="/style-1.css" /></noscript>

    <!-- ※3 -->
    <link rel="preload" as="style" href="/style-2.css" />
    <noscript><link rel="stylesheet" href="/style-2.css" /></noscript>

    …
  </head>
  <body>
    …
    <script>

    /* ※4 */
    Array.prototype.forEach.call( document.head.querySelectorAll( 'link[rel="preload"][as="style"]' ), function( link ) {
      link.rel = 'stylesheet';
    } );

    /* ※5 */
    window.addEventListener( "load", function() {
      var div = document.createElement( "div" );
      div.innerHTML = document.getElementById( "onload-style" ).innerText;
      document.head.appendChild( div.children[ 0 ] );
    }, false );

    </script>
  </body>
</html>
※1
スタイル適用前のウェブページが表示されないよう、body要素 (とその子孫要素) を非表示にしています。JavaScriptが無効な環境では、スタイル適用前のウェブページが表示されることはないので、次の行の <noscript id="onload-style">…</noscript> で表示させます。
※2
style-1.cssを非同期で読み込ませています。読み込み完了直後にonload属性値の this.rel='stylesheet'; によって rel="preload" の部分が rel="stylesheet" に置き換わり、スタイルが適用されます。script要素のasync属性に似た挙動になります。IE11はこのonload属性値によってフリーズする場合があるので、if(!document.documentMode) の部分でIEを除外しています。onload属性が作用しないJavaScriptが無効な環境を想定して、次の行に <noscript>…</noscript> を追加しています。
※3
style-2.cssを非同期で読み込ませています。onload属性を設定していないので、スタイルの適用は※4のJavaScriptの部分で行われます。script要素のdefer属性に似た挙動になります。JavaScriptが無効な環境を想定して、次の行に <noscript>…</noscript> を追加しています。
※4
この段階になるとレンダリングブロックは発生しないので、preloadによる読み込み途中・完了を問わず rel="preload"rel="stylesheet" に置き換えてスタイルの適用を行います。preloadに対応していないブラウザはこの段階でCSSファイルの読み込みが始まります。
※5
最後にbody要素 (とその子孫要素) を表示させます。

この方法は、HTMLコードにCSSコードを直書きする場合と比べ、HTMLコードにpreloadとJavaScriptコードを設定するだけなので、非常に手軽でHTMLコードもクリーンです。また、外部ファイルは、(設定してあれば) サーバーキャッシュやブラウザキャッシュの対象になるので、速く表示される可能性もあります。

このHTMLコードは、スタイルが適用されてスクリプトも実行された段階で表示されるようになっています。ファーストビューを少しでも速く表示させたい場合は、ファーストビューを設定しているCSSファイルに body { opacity: 1; } を追加し、※2の方法で読み込むことにより可能となります。

WordPressで運用しているウェブサイトの場合

テーマのfunctions.phpに、先の※1の部分をwp_headフックで、※2と※3の部分をstyle_loader_tagフックで、※4と※5の部分をwp_footerフックで追加します。

add_action( 'wp_head', 'my_wp_head', 7 );
if( !function_exists( 'my_wp_head' ) ) {
  function my_wp_head() {
    echo '<style>body{opacity:0;}</style><noscript id="onload-style"><style>body{opacity:1;}</style></noscript>';
  }
}

add_filter( 'style_loader_tag', 'my_style_loader_tag', 10, 4 );
if( !function_exists( 'my_style_loader_tag' ) ) {
  function my_style_loader_tag( $html, $handle, $href, $media ) {
    global $pagenow;
    if( !is_admin() && !in_array( $pagenow, array( 'wp-login.php', 'wp-register.php' ), true ) ) {
      $other = '';
      if( strpos( $html, ' crossorigin="anonymous"' ) !== false ) {
        $other .= ' crossorigin="anonymous"';
      }
      switch( $handle ) {

        case 'script要素のasync属性のように読み込ませたいCSSファイルのハンドル名';
        $html = '<link rel="preload" as="style" href="'. $href. '" media="'. $media. '"'. $other. ' onload="if(!document.documentMode)this.rel=\'stylesheet\';" /><noscript>'. trim( $html ). '</noscript>';
        break;

        case 'script要素のdefer属性のように読み込ませたいCSSファイルのハンドル名';
        $html = '<link rel="preload" as="style" href="'. $href. '" media="'. $media. '"'. $other. ' /><noscript>'. trim( $html ). '</noscript>';
        break;

      }
    }
    return $html;
  }
}

add_action( 'wp_footer', 'my_wp_footer', 100 );
if( !function_exists( 'my_wp_footer' ) ) {
  function my_wp_footer() {
    echo '<script>Array.prototype.forEach.call(document.head.querySelectorAll(\'link[rel="preload"][as="style"]\'),function(link){link.rel=\'stylesheet\'});window.addEventListener(\'load\',function(){var div=document.createElement(\'div\');div.innerHTML=document.getElementById(\'onload-style\').innerText;document.head.appendChild(div.children[0])},!1)</script>';
  }
}

今回のpreloadを使用した対策をすることで、PageSpeed Insights と GTmetrix の評価は大きく改善しました。preloadによる非同期の先読みは、手軽にできる改善方法として有効だと思います。

ただ、2019年10月現在、当サイトではデータ量の多い Google Fonts のNotoフォントを使用しているため、結果はあと一歩と言ったところです。また、サーバー環境や通信環境によるものなのか、PageSpeed Insights の点数が分析するたびに増減して安定しないのも気になる点ではあります。

これらの評価はサーバーの性能や機能にも大きく左右されるので、ウェブページの読み込み時間 (表示速度) を改善したいのであれば、高性能なサーバーを利用する必要が出てくると思います。

ちなみに、当サイトはエックスサーバーを利用しており、管理画面 (サーバーパネル) から「高速化」できる「Xアクセラレータ」「サーバーキャッシュ」「ブラウザキャッシュ」を有効にしています。

関連記事