PageSpeed Insightsのナゾのお告げ

[:ja]先日、勉強会でWebサイトパフォーマンスについてのごく基礎的な内容のワークショップを行いました。その時の自分自身のおさらいのため、PageSpeed InsightsでWebページを分析した時に提示されるとある提案(=お告げ)の内容を紐解きながら「CSSの初期読み込み時の最適化」について記事にしてみました。

通常CSSのルールセットを記述する時には、パフォーマンス最適化をそれなりに意識してを記述している方が多いと思いますが、それが効力を発揮するのはレンダリングの工程以降でのこと。この記事では、その前の段階のリソースのローディング工程における最適化についてまとめています。[:]

ナゾのお告げ

Googleが提供しているPageSpeed Insightsという、ブラウザベースで使用できるツールがあります。任意のURLを入力すると、初期読み込み時パフォーマンス最適化のための具体的な提案をしてくれます。実際に計測してみると、おそらく殆どのWebページで下記の項目が表示されるのではないでしょうか。

スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する このページには、ブロッキングCSSリソースが1あります。これが原因で、ページのレンダリングに遅延が発生しています。以下のリソースの読み込みが終わるまで、このページでスクロールせずに見えるコンテンツを何もレンダリングできませんでした。レンダリングをブロックするリソースの読み込みを遅延させるか、非同期に読み込むか、これらのリソースの重要部分をHTML内に直接インライン化してください。

日本語なのに、何を言っているかさっぱりわかりませんね(^_^;)
筆者も初めてこの記述を見た時は意味がわからず、見なかったふりをしました。

まず「スクロールせずに見えるコンテンツ」とは、ざっくりいうとファーストビューのことだと思われます(厳密に言うと、折りたたまれて非表示となっているコンテンツも除いた部分。原文は”above-the-fold content”)。さしあたってファーストビューのレンダリングのみを先に完了させて、ページが素早く表示されたとユーザーに感じさせる、というのがこの「お告げ」の狙いです。

クリティカル・レンダリング・パスとクリティカル・リソース

具体的にどうすればいいのかを検討する前に、ブラウザのレンダリング工程について簡単に見ていきましょう。

Webサイトパフォーマンスを話題にすると必ず出てくる言葉に「クリティカル・レンダリング・パス」というものがあります。「クリティカル・パス」というのは「重要な経路」という意味で、何か物事を進める過程において、欠くべからざる工程のことです。ブラウザのレンダリングの過程における「クリティカル・レンダリング・パス」とは「ブラウザがサーバーからHTMLのレスポンスを受け取り、スクリーンにピクセルが描画されるまでに必要な工程のこと」です。

ブラウザからHTMLファイルへのリクエストが送られると、サーバーから応答があり、ダウンロードが開始されます。ダウンロードが完了すると、ブラウザのHTMLパーサがパース(解析)を行います。この時、ソースに書かれている順番に外部ファイルが読み込まれていきます。CSSの外部ファイルがある場合はその読み込みが完了してからCSSのパースが開始されます。HTMLとCSSのパースが両方完了すると(DOM構築・CSSOM構築)、ようやくレンダリングの工程が開始される仕様になっており(※1)、つまり、HTMLファイル自体、そしてCSSファイルの読み込みが完了するまでレンダリングが始まらないことになります。こういったリソースは「クリティカル・リソース(※2)」または「レンダー・ブロッキング・リソース」と呼ばれているようです。

ブラウザのレンダリング工程

因みに、HTMLの中にscriptタグがあると、スクリプトの実行が終わるまでパースはブロックされます。すなわちJavaScriptはDOMの構築をブロックします。DOMの構築が終わらないとレンダリングが開始されないので、JavaScriptもクリティカル・リソースであると言えます。

ですので、HTML・CSS・JavaScript(クリティカル・リソース)の読み込みを早くすること、または数を減らすこと、そして読み込みの順序を考慮するなどして、レンダリング開始までにかかる時間をできるだけ短くすること(=クリティカル・レンダリング・パスの最適化)がWebサイトパフォーマンスを良くすることにつながります。

下記ページ・動画では、JavaScriptの振る舞いも含めてこのあたりのことを詳しく解説しています。

1 CSSが当たっていない状態のソースが一瞬描画されてしまう現象(FOUC=Flash of Unstyled Content)を避けるため、このような仕様になっているそうです

2 画像ファイルはレンダリングをブロックしないので、クリティカルリソースではありません。ただし、loadイベントはブロックするので、ファイルサイズを小さくして読み込みを早くすることはページの描画完了を早めることにつながります

ナゾのお告げに従ってみる

クリティカル・レンダリング・パスの最適化のために必要なことを整理します。

  • クリティカル・リソースの読み込みを早くする
  • クリティカル・リソースの数を減らす
  • クリティカル・パス長(ラウンドトリップ回数)を最小限に抑える

リソースの読み込みを早くするには、コメントやスペースを削除してリソースのファイルサイズを抑えたり、データ転送の際にGzip圧縮を利用したり、ブラウザキャッシュを利用するなどが、有効な手段となります。とはいえ、コメントやスペースを削除すると、ソースの可読性や保守性が損なわれてしまいます。こういった処理を行う場合は、開発環境とは別にデプロイ用のファイルを自動的に書き出すなど、ワークフローを整える必要がありそうです。

次に、クリティカル・リソースの数を減らすことを考えてみましょう。具体的には外部ファイルをHTML内にインライン化しすればいいのですが、すべてのCSSルールセットをインライン化すると、クリティカル・リソースの数は減るものの、HTMLファイルのサイズがCSSファイルのサイズ分増えてしまいます。ファイルサイズを肥大化させずにクリティカル・リソースの数を減らすにはどうしたらいいでしょうか。

CSSについては「お告げ」に記載されているように「リソースの重要部分を HTML 内に直接インライン化」します。

すなわち、ファーストビューの表示に必要なCSSルールセットのみをインライン化し、CSSファイル自体は非同期で読み込む(=レンダリングをブロックしない形で読み込む)ようにします。(詳細は割愛しますがJavaScriptについては、インライン化する以外に描画に関係のない外部ファイルは非同期で読み込む(=async属性を付ける)などの対応でクリティカル・リソースでなくすることができます。)これについても、手動で行っていてはソースコードの保守性が著しく損なわれることになりそうです。そこで、gulpのCriticalというパッケージを使ってインライン化作業を行ってみました。

すると、単純に二つのCSSファイルを読み込んでいただけのソースコードが…

インライン化作業前のソース

このようになりました。

インライン化作業後のソース

CSSファイル読み込み用のlinkタグのrel属性が”preload”となっています(※3)(loadイベント発火後に”stylesheet”に書き換える)。するとこのCSSファイルは非同期で読み込まれるようになり、レンダリングをブロックしなくなります。ただし、この状態ではスタイル適用前の状態が一瞬表示されてしまうため(※4)、ファーストビューの表示に必要なCSSルールセットをインライン化してhead内に記述しています。

この作業を施したページをPageSpeed Insightsで計測したところ「スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する」というお告げが消え、とりわけモバイルでのスコアが大幅に改善しました。

計測に使用したページのファイルセットは、ワークショップのためにアップルップルの堀さんが用意してくださったものを利用させていただいています。

とはいえ、両者のページ描画完了までにかかる時間はさほど変わらないようでもあります。ChromeDevToolsのperformanceパネルでプロファイルを取り、両者のイベントログを見てみました。左側は適用前、右側が適用後のイベントログです。

インライン化作業前後のイベントログ

適用前はCSSファイルの読み込みとCSSのパースが完了してから初めてレンダリングが始まっており(1583ms後)、最初の描画は1618ms後に開始されています。一方、適用後はCSSファイルの読み込み完了を待たずにレンダリングが開始されており(1228ms)、最初の描画は1254ms後に開始されています。初期の描画(First Meaningful Paint)まで364ms短縮されています。しかし体感速度的にはさほど変わらない気がします。そして、loadイベントが発火するのは、3.94s → 3.75sと、あまり変わっていません。

同じくChromeDevToolsのAuditsパネル(※5)でパフォーマンスのスコアを取ってみました。

インライン化作業前後のAudits計測結果

performanceパネルの結果とは若干数値が異なりますが、First Meaningful Paintのスコアが改善されているのがわかります(因みに、Auditsの計測結果は毎回大きく異なり、動作が安定していない様子…)。

スコアとしてはいずれも改善が見られましたが、このサンプルでは体感速度的に著しく改善されたわけでもありません。スコアはあくまでもツールの示す指標であり、目指すのはユーザーが快適に閲覧できるページを作ることで、スコアを上げること自体が目標ではありません。複数の計測方法を併用し、CSSファイルの読み込みが初期読み込み時のボトルネックになっていることが明らかな場合のみ、インライン化作業を行えばいい気がします。

また、サイト内共通のファイルを読み込んでいる場合は、2ページ目閲覧以降はブラウザキャッシュが効いた状態となり、リソースのダウンロードにかかる時間は大幅に短縮されます。このことも念頭に置き、労力と効果のバランスを勘案して最適化していくのが良さそうです。

3 rel=”preload”という記述方法は主要PCブラウザではChromeのみが対応しています。8行目のスクリプトは、他のブラウザでも同様の処理を行うためのものである気がするのですが、どうでしょう…?
rel=”preload”のサポート状況

4 Async CSS w/ link[rel=preload](非同期に読み込んだCSSファイルのスタイルが遅れて適用されるデモ)

5 Lighthouseという拡張機能として提供されていた計測ツールがChromeのver.60から本体に統合され、Auditsパネルから起動できるようになりました(以前のAuditsもLegacy Auditsとして残っています)。パフォーマンスのスコアのみならず、Progressive Web Appとしてのスコア、アクセシビリティのスコアも出してくれます。

メディアクエリごとにファイルを分ける

CSSファイルの読み込みを早めるためには、メディアクエリごとにファイルを分けるのも有効です。linkタグのmedia属性に表示中のページにマッチしないメディアクエリが指定されていると、そのCSSファイルの読み込みは後回しにされます。描画に必要なリソースがすべてダウンロードされてから取得され、レンダリングもブロックしません。すなわちクリティカル・リソースではなくなります。クリティカル・リソースであるCSSのファイルサイズが減らせるとともに、レンダリング(CSSのマッチング処理)にかかる時間も減らせるので、クリティカル・レンダリング・パスを最適化できることになります。

下記のようにスタイルシートを読み込むと…

<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/style_pc.css" media=(min-width:768px)>

画面サイズが768pxより小さい時は、一つ目のCSSファイルのみがクリティカルリソースとなります。二つ目のCSSファイルの読み込みは、随分遅れて始まっていますね。

画面サイズが768pxより小さい時のスタイルシート読込状況

画面サイズが768px以上の時は、一つ目のファイルの読み込みが終わった直後に二つ目のファイルの読み込みが始まっています。

画面サイズが768px以上の時のスタイルシート読込状況

このことから、CSSをモバイルファーストで記述すると、モバイルでの表示時にはクリティカル・リソースの数・ファイルサイズともにPC表示時よりも小さくなり、パフォーマンス的によりモバイルフレンドリーになることがわかります。

とはいえ、近頃はメディアクエリごとに記述するよりも、運用のしやすさから次のように同じセレクタごとにまとめて記述することが多いのではないでしょうか。

.sp {
    display: block;
}

@media only screen and (min-width: 768px) {
    .sp {
        display: none;
    }
}

とりわけSassなどのCSSプリプロセッサを使っている方は、設定ファイルにメディアクエリ用の設定を書いておいて、効率よく記述できるようにしている方が多いと思います。筆者は下記のソースをSCSSの設定用ファイルに記述し、実際に記載するソースをスニペットに登録して、トリガーを数文字入力するだけでメディアクエリの記述ができるようにしています。

@mixin mq {
  @media only screen and (min-width: $breakPoint) {
    @content
  }
}

こういったワークフローを今更変えたくはないですよね。そこでメディアクエリごとに記述をまとめるgulpのパッケージを見つけましたが…

メディアクエリごとにファイルを分割するというオプションはなさそうです…。HTTP/1.1では、パフォーマンス最適化のためにはファイルをまとめる方向に向かっていたので、メディアクエリごとにCSSファイルを分割する方法は、筆者自身もこれまで取ってこなかった気がします。HTTP/2が普及するにつれて、このあたりの傾向も変わってくるのでしょうか。

ファイル分割するためのgulpパッケージ、もしくはgulpの利用以外で同様の操作を実現する方法をご存じの方はご一報ください(^^)/

参考