リニューアルしたPageSpeed Insightで激減したスコアを改善する

こんにちは。HivelocityでBaseStationというサービスの開発を担当しています、Haradaです。
今回はWEBサイトのパフォーマンスを測るひとつの指標となりえるPageSpeed Insightsのスコア最適化にあたって、主にフロントエンドで行った具体的施策についてお話をしたいと思います。

リニューアルしたPageSpeed Insightsでスコアが激減した

弊社のプロダクトはPageSpeed Insights(以下PSI)でのスコアを意識していますが、2018年11月にPSIの仕様変更があり、スコアが激減しました。

PSIアップデート後の弊社サイトの点数がこちら(今回のスクショを撮るためだけにProduction環境をスイッチバックするのは無理でしたので、Staging環境のスコアになりますがほぼ差分はありませんのでご了承ください)

どうやらGoogleがかねてより提供していた監査ツールである“Lighthouse”ベースでの解析を行うようになったとのこと。(Googleウェブマスター向け公式ブログより)

Lighthouseは、「Performance」、「PWA」、「アクセシビリティ」、「ベストプラクティス」、「SEO」という5つの観点でサイトを判定します。
単純に評価項目が増えたことで採点が厳しくなったということもありそうですが、一番インパクトが大きい要素が3G回線での評価を基準としている点だと思います。

3Gですよ!3G。
2020年春には5Gの商用サービスがスタートするというのに…と嘆いていても仕方ないので、対応を進めていきます。

以前のPageSpeed Insightsと何が違う?

リニューアル前のPSIには「最適化」と「速度」という項目がありました。

最適化という項目は”Googleが推奨する項目にどれだけ対策できているか”をスコア化したものという認識でした。
速度についてはChromeユーザーエクスペリエンスレポート(以下、CrUX)のデータベースにあるサイトの中で、上位1/3を「Fast」、真ん中の1/3を「Average」、下位の1/3を「Slow」と評価したものでした。
つまりCrUXのデータベースに含まれていないサイトは評価対象にもなっていなかったということです。

PSIでのスコアが高いほどサイトの表示が速いと思われがちですが、あくまでも“Googleが推奨する項目にどれだけ対策できているか”をスコアリングした指標になります。

ここは結構解釈がズレている人が多いと思っています。

SEOとか不要な有名プロダクトを開発している人や、純粋にレスポンススピード向上に取り組んでいる人にとっては、PSIへの最適化はさほど優先度が高くなかったと思いますが、弊社の提供するプロダクトは、主に日本の企業様向けに提供しているので、SEOが超重要。もっというとGoogleに高評価される必要があります。

開発する側としては、当然レスポンススピード向上を追求したいところなのに、指標にしているスコアはGoogleが設けた項目ベースという点がしっくりこないまま後ろ向きな気持ちで最適化対応を行っていたのを思い出しました。

Lighthouseベースになったところで、Googleファーストな点は変わらないですが、Performanceという項目が追加されていたり、個人的には評価基準がはっきりしたことが良いと思っています。

どう対応していったか?

スコア自体は大きく減少していましたが、以前のPSI同様に改善案を提示してくれるので、今回もPSIが指摘する項目を確認して対応していきました。

実際に指摘されていた項目は以下のとおり。
上から短縮できる時間が多いとされる順に並んでいるので、最適化したときにインパクトが大きい順とも言えると思います。

ですが今回は、”項目数をできるだけ早く減らして(さくっとできそうなものは早く潰して)、時間がかかりそうな対応に集中して取り組みたい”という理由から、以下の順に行っていきました。

  1. テキスト圧縮の有効化
  2. 次世代フォーマットでの画像の配信
  3. レンダリングを妨げるリソースの除外
  4. 使用していないCSSの遅延読み込み
  5. オフスクリーン画像の遅延読み込み
  6. サーバー応答時間の短縮

それぞれ見ていきましょう。

1. テキスト圧縮の有効化

PSIでの詳細説明を見ると以下のようにあります。

テキストベースのリソースは圧縮(gzip、deflate、またはbrotli)して配信し、ネットワークの全体的な通信量を最小限に抑えてください

こちらはサーバーの設定ファイルを最適化することでクリアできそうなので最初にトライしました。

ブラウザがサーバーにHTML、CSS、JavaScriptなどのファイルをダウンロードしにいくタイミングがありますが、その時にgzipなどの形式で圧縮したものを提供するように以下設定を加えました。

弊社はnginxを使ってますので、設定ファイル(nginx.conf)のhttpディレクティブに以下を追記しました。

http {
  # gzip圧縮
  gzip: on;
  gzip_types text/plain
             text/css
             text/javascript
             image/gif
             image/jpeg
             image/png
             text/xml
             application/xml
             application/rss+xml
             application/json
             application/javascript
             application/x-javascript;
}

HTTPレスポンスヘッダーを確認してContent-Encodingにgzipが設定されていれば有効化されています。

2. 次世代フォーマットでの画像の配信

体感では、この項目への対応が最も効果が高かったと感じます。
PSIからは以下のアドバイスが表示されるので、 JPEG 2000JPEG XRWebP どれを使うのか検討します。

JPEG 2000、JPEG XR、WebP などの画像フォーマットは、PNG や JPEG より圧縮性能が高く、ダウンロード時間やデータ使用量を抑えることができます。

can i useで対応ブラウザなどを確認しながら、どのフォーマットを選択するかを検討しました。

JPEG 2000

まずJPEG 2000。Safari以外の主要ブラウザで表示できないようなので問題外。

JPEG XR

つぎに、JPEG XR。IEとEdge以外の主要ブラウザでは表示できないようなのでこちらもNG。

WebP

WebPはChromeとFireFoxが対応済みで、SafariとEdgeも対応予定です。
IEが非対応ですが、この先消えゆくブラウザなので今回ここはあまり考慮しませんでした。(3G回線でIEを利用するユーザーってどんな状態なんだろうか…)
仮にWebP画像が表示できないとしても既存フォーマットの画像が表示されるので、WebP形式で画像を配信するようにしました。

また、画像の表示に関して、弊社ではモバイルサイズの場合、PC用の大きい画像を可変させて表示させていたのですが、表示速度を向上させるためにデバイスのサイズに応じて適切なサイズの画像を表示させたいという課題もありました。

上記2点を解決するためにimgixを導入しました。

imgixとは、リアルタイム画像処理機能が付いたCDNサービスです。
画像処理機能付きのCDNについては、ここではあまり詳しく説明しませんが、用意されたパラメータを付与してファイルをリクエストすると様々な処理済みのファイルを返してくれるというサービスです。
好みのサイズに変更したしたり、トリミングした画像をリクエストしたりできます。もちろんフォーマットも指定できるので、imgix経由でWebP画像をリクエストして表示するようにしました。

3. レンダリングを妨げるリソースの除外

この項目は以前のPSIでも指摘がありましたが、JavaScriptの取捨選択とリソースを非同期で読み込むようにするのは比較的すぐに対応できると思います。

しかし、すでに出来上がっているサイトに対して、ファーストビューに必要なCSSを抜き出すという作業がかなり工数を使いそうだったので、ジェネレーターで生成するという簡易的な対応を行っていた経緯があります。
それでも以前のPSIではスコアが90点を超えていたので、おそらくは評価項目が厳しくなったため再度指摘されたのだと思います。

PSIの提示する説明では以下のように解説されています。

ページの初回ペイントをリソースが阻害しています。クリティカルなJSやCSSはインラインで配信し、それ以外のJSやスタイルはすべて遅らせることをご検討ください

要はページがレンダリングされる(要素が視覚的に認識できるようになる)のが遅いということです。

ブラウザのレンダリングは以下の図のように「Loading」→「Scripting」→「Rendering」→「Painting」と大きく4つの工程で行われます。

「Download」でHTMLやCSS、JavaScriptなど必要なリソースをダウンロードし、「Parse」ではパーサーがHTMLやCSSを解析し、DOMツリーやCSSOMツリーへと変換します。しかし、パーサーはHTMLを解析している途中に<script>タグに到達すると取得して実行します。このJavaScriptの実行が完了するまでHTMLのパースは停止した状態になります。

これが、属に言うレンダリングをブロックしている状態になります。

方向性としては対象ページに必要ないJavaScript処理やCSSの描画を削除して処理を減らすことと、ブラウザのレンダリング処理を効率よくして、ブラウザレンダリング処理の完了をできるだけ早くすることが解決につながります。
“クリティカルな”というのは、極端な話スクロールしないと見えないエリアについてはクリティカルではないと捉えていいと思います。

つまり、ファーストビューに必要なJavaScriptの処理やCSSスタイルだけをインラインで記述して、それ以外の処理はhtmlパース完了後に非同期で実行することをPSIは推奨しています。

弊社では以下の手順で対応を行っていきました。

  • JavaScript/CSSの整理(不要なJavaScript処理やCSSを削除するなど)
  • JavaScriptを非同期で読み込むように設定
  • ファーストビューに必要なJavaScript/CSSをまとめてインラインで記述
  • ファーストビューで使用するCSS以外を非同期で読み込むように設定

JavaScript/CSSの整理

まず各ページで共通で読み込んでいるJavaScriptやCSSを整理することから始めました。
TOPページの場合で考えると、そのほかのページで使用するJavaScriptやCSSなどは不要なのでTOPページでは読み込まないなど、各ページごとに必要なスタイルだけを読み込むように整理していきました。

JavaScriptを非同期で読み込むように設定

各ページとも必要な処理だけを行うようにした後は、JavaScriptを非同期で読み込むようにasync属性およびdefer属性をつけるようにしました。
これにより、上の図でいう「Parse」(htmlパース)を停止することなくJavaScriptのダウンロードが完了でき、次の「Scripting」にスムーズに移ることができるようになります。

ファーストビューに必要なJavaScript/CSSをまとめてインラインで記述

次に各ページのファーストビューに必要なJavaScriptとCSSを抽出して、インラインで記述します。

さらに、残りのCSSは<link rel=”preload”>をつけて非同期で読み込むようにします。
<link rel=”preload”>は今の時点でChrome、Safari以外は対応していないので、loadCSSというJavaScriptを利用します。

A function for loading CSS asynchronously(Github)

使い方の詳細はGithubに書いてありますが、ざっと説明します。

通常は以下のように記述すると思います。

<link rel="stylesheet" href="hogehoge.css" as="style">

loadCSSを使う場合は、以下のように onload="this.onload=null;this.rel='stylesheet'" を追記してください。

<link rel="preload" href="hogehoge.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="hogehoge.css"></noscript>

さらに、JavaScriptをOFFにしている環境にも対応できるように <noscript><link rel="stylesheet" href="hogehoge.css"></noscript> も追加します。

loadCSSの処理は、対象のCSSを読み込んだあとに cssrelpreload.js ファイルの記述をインラインで実装している方法が多かったのですが、ここのJavaScriptだけインラインで記述するのが気持ち悪かったので、外部読み込みにして defer 属性を付けました。

Githubの README.md にも以下のように外部から参照するのもOKとあったのでそうしました。

we recommend either inlining that CSS in a style element, or referencing it externally and server-pushing it using http/2.

ただ、PSIのスコアを最適化したいという目的だけなら、Chromeだけ気にしていればよいのでは?とも感じます。

対応するにあたりネット上で他の方のコードを参考にさせてもらったりしましたが、ファーストビュー用のCSSを作成してインラインで記述すればすんなりクリアとはいかず、実際そのように対応を行っても指摘が消えず四苦八苦しました。

これは何かの節に気づきましたが、ファーストビューに必要なCSSを作成するときにPCサイズで見た場合のファーストビューCSSしかインラインで記述していなかったため、モバイルのPSIスコアが一向に改善されずにハマりました。

モバイルサイズのときにはモバイルサイズで見た場合のファーストビューCSSがインラインで記述されるように調整を行うことでクリアできましたが、この辺はなかなか言及されていなかったので盲点でした。

4. 使用していないCSSの遅延読み込み

こちらの項目については、上記の”レンダリングを妨げるリソースの除外”のCSS対応のときに同時に解決することになると思いますのでそちらを参照ください。

5. オフスクリーン画像の遅延読み込み

詳細説明には以下のようにありますが、こちらもおそらくはCSSなどと同様に”ファーストビューに不要な画像も遅れて読み込む”ということを推奨しているのだと思います。

オフスクリーンの非表示の画像は、クリティカルなリソースをすべて読み込んだ後に遅れて読み込むようにして、インタラクティブになるまでの時間を短縮することをご検討ください。

実装にあたり、ファーストビューに不要な画像は初回ロードのタイミングでは読み込まないで、対象要素が画面内に入ってきたタイミングで読み込むようにしました。

こういう場合、スクロールイベントを使って実装することが多いですが、今回はパフォーマンス面を考慮(※1)して、IntersectionObserver APIを使って判定を行いました。

※1、スクロールイベントはスクロールしている間イベントが大量に発火するので処理が重くなりがちです

Googleもデベロッパーサイトにて、遅延読み込みを実装しているページをクロールしてインデックスする方法を公開しており、IntersectionObserver APIを使うことを推奨しています。

IntersectionObserver APIは、DOM要素がviewportに入ったかどうかを判定できるJavaScript APIです。

基本的には以下のように IntersectionObserver オブジェクトを作成し、監視したい要素を observe することで利用できます。
第一引数には要素が表示されたときに呼び出すコールバックを渡し、第二引数にはオプションのオブジェクトを渡します。

// IntersectionObserverオブジェクトを作成する
// 要素が表示されたときに実行するコールバック関数を渡す
const observer = new IntersectionObserver((entries) => {
  for(const e of entries) {
    console.log(e);
  }
}, options);

// 監視したい要素をobserveする
observer.observe(document.querySelector('.hogehoge'));

IntersectionObserver APIの詳しい使い方についての説明はここでは省きますが、以下リンク先に詳細説明やサンプルコードがあるので参考にしてみてください。

Intersection Observer API – MDN – Mozilla
IntersectionObserver’s Coming into View (英語)

次に画像まわりのタグを設定します。
以下のように、あらかじめ src 属性に空のデータを入れておき、 data-src 属性に実際に表示させたい画像のパスを入れておきます。

<img src="default.jpg" data-src="after.jpg" alt="I'm an image!">

あとはIntersectionObserverで判定を行い、 data-src 属性を src に変更します。

実際のコードは以下のようになります。

// ターゲット指定
const targets = Array.from(document.querySelectorAll('img[data-src]'));

// 実際の画像パス
const imgPath = 'data-src';

// オプション
const options = {
  // 上下100px手前で発火
  rootMargin: '100px 0';
};

// IntersectionObserverオブジェクトを作成する
const observer = new IntersectionObserber(callback, options);

targets.forEach(function(img) {
  // 監視の開始
  observer.observe(img);
});

// コールバック
function callback(entries, object) {
  entries.forEach(function(entry, i) {
    // 交差していない
    if (!entry.isIntersecting) return;

    // ターゲット要素
    const img = entry.target;

    // 遅延ロード実行
    loading(img);

    // 監視の解除
    object.unobserve(img);
  });
};

// 画像の遅延ロード処理
function loading (img) {
  // data-srcの値を取得
  const srcVal = img.getAttribute(imgPath);

  if (srcVal) {
    // 画像パスを設定
    img.src = srcVal;
    img.classList.add('is-show');
    img.onload = function () {

      // data-src属性を削除
      this.removeAttribute(imgPath);
    };
  };
};

6. サーバー応答時間の短縮(TTFB)

最後にサーバー応答時間の短縮です。
PSIでは、サーバーの応答時間を200ミリ秒以下に抑えることが推奨されています。

こちらのリファレンスにサーバーの応答が遅くなる要因として以下7点が挙げられていますが、サーバースペックやOS、利用しているミドルウェアの種類や設定など、環境によってさまざまなので最適な解決策は提示できません。
Googleのリファレンスページにも明示されているように、まずは計測したうえでボトルネックを見つけて対処していくほかないと思います。

  1. 速度の遅いアプリケーションロジック
  2. 遅いデータベースクエリ
  3. 遅いルーティング
  4. フレームワーク
  5. ライブラリ
  6. リソースによるCPUの消費
  7. メモリ不足

弊社では、PHPまわりの処理が遅かったので、BlackfireというPHPプロファイラーを使ってボトルネックとなっている箇所を探しだして改善するという方法をとりました。

まとめ

上記の項目の最適化を対応し、ようやくスコアが90点を超えました。

ただ、サードパーティのリソースを読み込んだ途端、一気に30点ほど下がってしまいます。(上記のスコアはMAの計測タグを削除した状態となります)
Google Analyticsのタグはなんとか大丈夫そうですが、Googleタグマネージャで管理しているその他の計測タグを有効化するとスコアが大きく下がります。

ちょっと前までよく見かけたFacebookやTwitterなどSNS系サービスのカードは、わざわざサイトに表示しなくてもさほど大きな問題にはならないですが、計測系のタグも大きくスコアが下がる要因になってしまう点はどうにかならないものかと思います。

今回、Googleへの最適化の一環としてPSIへの対応を行いましたが、同じように最適化に取り組んでいるエンジニアの方々にとって、この記事が少しでも参考になれば幸いです。

また、HivelocityではUI/UXにも寄り添って考えられるフロントエンドエンジニアを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。


弊社が提供するサイト構築パッケージBaseStationに少しでもご興味を持った方は、以下よりお気軽にご相談いただければ幸いです。

  • f
  • t
  • p
  • h
  • l
  • n