2022/05/27

Lighthouseの点数を50点以上改善したお話

トラベルブックのフロントエンドチームでは2020年の9月から、ページのパフォーマンス改善に取り組んでいます。

今回は今までどのようにやってきたのかを紹介したいと思います。

Core Web Vitals

2020年5月、Core Web Vitals がSEOに影響されるというのがGoogleから発表され、集客的にもユーザー体験をページパフォーマンスが重要になりました。

弊社はメディアサービスを運用しており、SEOはビジネス的に重要な指標としています。
そのため、Core Web Vitals をパフォーマンス改善の指標としました。

Core Web Vitalsはより良いユーザー体験を提供するための指標となっていて、読み込み時間、インタラクティブ性、視覚的な安定性 に焦点をあてた下記3つの指標をベースに計測します。

Largest Contentful Paint (最大視覚コンテンツの表示時間、LCP) 読み込みのパフォーマンスを測定するための指標です。優れたユーザー エクスペリエンスを提供するためには、ページの読み込みが開始されてからの LCP を 2.5 秒以内にする必要があります。

First Input Delay (初回入力までの遅延時間、FID) インタラクティブ性を測定するための指標です。優れたユーザー エクスペリエンスを提供するためには、ページの FID を 100 ミリ秒以下にする必要があります。

Cumulative Layout Shift (累積レイアウト シフト数、CLS) 視覚的な安定性を測定するための指標です。優れたユーザー エクスペリエンスを提供するためには、ページの CLS を 0.1 以下に維持する必要があります。

https://web.dev/vitals/

Core Web VitalsGoogle Chromeの検証機能 や、Page Speed Insightsを使ってLighthouse で計測できるため、 Lighthouse を実行して出てきた指摘点をベースにパフォーマンス改善をしていきました。

改善のための計画作り

パフォーマンス改善で直すべきポイントは無限にあるため、どのページをどうあげていくか、指標と目標をシンプルにするため下記の流れで進めるようにしました。

  1. ベンチマークとなるページを選定して Lighthouse で計測
  2. Lighthouse の指摘項目ですぐにできそうなものをリストアップ・修正
    • 次世代フォーマットでの画像の配信
    • オフスクリーン画像の遅延読み込み
    • 静的なアセットと効率的なキャッシュポリシーの配信
    • など
  3. Lighthouse の指摘項目で直すのが難しい部分を分析&分解
    • 使用していないJavaScriptの削減
    • 過大なDOMサイズの回避
  4. Lighthouse の指摘項目外でのLCP FID(TBT) CLSの箇所を分析、改善
    • とくにLCPは Page Speed Insights 上の採点が高いので重点的にチューニング
    • CLSは直しやすいので積極的に直す
  5. パフォーマンス監視ツールの導入

方針としては Lighthouse の赤く指摘されている部分を重点的に直していきました。
(2) までは比較的簡単に修正ができるのですが、
(3) の改善については、 Lighthouse の指摘だけでは直せない部分も多かったので、Chrome DevtoolPerformance を使って 仮説出し → 修正 → 計測 → 仮説出し → 修正 … を繰り返しました。

ベンチマークページの選定

弊社のメインコンテンツである、記事ページから、とくに処理や機能が多い記事ページをベンチマークとしました。
Page Speed Insights で計測したところ、 19点 というスコアとなっていました。

モバイルからのアクセスが多いので、計測対象のデバイスはモバイルにしましたが、計測点数はデスクトップと比べると点数が悪くなっています。

その理由としては、Lighthouse では3G回線をシュミレートするために下記のスペックで計測を行っています。

  • 3G回線

  • 低スペック

この計測で出た Lighthouse の指摘項目について改善できることをリストアップし、その中から効果が大きそうで、すぐにできそうなものを基準に優先順位をつけて直していきました。

優先順位付けと改善

Lighthouse で指摘された項目の中ですぐに改善できそうな部分をリストアップしました。

  • 次世代フォーマットでの画像の配信
  • 効率的な画像フォーマット
  • オフスクリーン画像の遅延読み込み
  • キー リクエストのプリロード
  • レンダリングを妨げるリソースの除外
  • 静的なアセットと効率的なキャッシュポリシーの配信
  • 第三者コードの影響を抑えてください
  • スクロールパフォーマンスを高める受動的なリスナーが使用されていません

また、 Lighthouse の数値については下記の結果でした。

Lighthouse の計測では Core Web Vitals で指標とされている LCP FID CLS 以外の項目も点数の計算対象になります。
First Contentful Paint Speed Index Time To Interective Total Blocking Time の項目が採点の基準として増えているので、これらの項目も意識して直していきました。

非常に悪い数値ですが、 Page Speed Insights の指摘点で改善できるものが多いので、ガンガンやっていきながら経過を見ていきます。

すぐにできそうなものの改善後

簡単にできそうな部分の改善を行ったところ、スコアは下記のようになりました。

点数がだいぶ上がりましたが、とくに画像周りの改善の効果が大きかったです。

理由としては、画像は比較的ファイルサイズが大きく、遅延読み込みをしないとファーストビューの時点でページ内すべての画像ファイルをダウンロードすることになってしまいます。

そうすると、本来表示させたいはずの重要なリソースのダウンロード速度が低下し LCPFCPSpeedIndex の項目に悪影響が出てしまいます。
また、画面外でのレンダリングも行われるため、処理が多くなることで TBT にも悪影響が出ます。

画像周りの改善により、これらの悪影響が改善されたことで多くの値が改善できました。

例) ダウンロードに1.78sかかっていた画像が

例) 他の画像のダウンロード(緑色の読み込み)がなくなるだけで、1.13sになります

複雑な課題の解決方法の調査

下記の指摘点については単純な修正のみでは改善しにくいものでした。

  • 使用していないJavaScriptの削減
  • 使用していないCSSの削除してください
  • 過大なDOMサイズの回避
  • メインスレッド処理の最小化
  • JavaScriptの実行にかかる時間の低減

とくにJavaScriptの削減は、レガシーなコードが多く混在しており、簡単には直せないものも数多くありました。

すべてを直すとすごく時間がかかるので、可能な限り現状のまま変更させないような改善ができないか調べたところ、 webpack-bundle-analyzer で不要なライブラリコードの削減 (momentの言語ファイルなど)CodeSpliting を利用すれば、大きな変更をせずに大きく改善できそうなのがわかりました。

同じようにしてその他のCSSやメインスレッド処理の指摘点についても、現状できることを調査して課題を細分化していきました。

  • 使用していないJavaScriptの削減
    • webpack-bundle-analyzerで不要なライブラリのコードを削除
    • codeSplitingの導入
    • 不要なコードの削除
    • など
  • 使用していないCSSの削除してください
    • 記事ページ専用のCSSを作成する
    • 不要なCSSの削除
    • など
  • 過大なDOMサイズの回避
    • for文内で使われている余分なHTMLタグの削除
    • 要素の削除・UIの変更
    • など
  • メインスレッド処理の最小化
    • reflowなどの再レンダリング処理を減らす
    • cssアニメーションの削除
    • 要素の削除・UIの変更
    • など
  • JavaScriptの実行にかかる時間の低減
    • for文などのリファクタリング
    • 遅延読み込みの導入
    • など

改善後のパフォーマンス計測で検証と分析を繰り返す

上記の施策についてはひたすら改善していったのですが、改善のBefore / Afterを見て、施策があってたかを確認、分析していきました。
分析には Chrome DevtoolPerformance を使って分析をします。

たとえば LCP の表示速度を早くするために、 LCP とされている画像をpreloadするように変更したのですが、点数が、ほとんど上がらないことがありました。

Performance 内の network を見てみると、preloadした画像の読み込みタイミングが仮説通りに早くなっているのは確認できました。

HTMLファイル(青部分)のダウンロード後すぐに画像のリクエストが始まってるのを確認できる

しかし、 LCP と判定されるタイミングを見てみると、
画像ダウンロード後すぐに LCP の判定はなく、しばらく時間がたってから LCP が表示されているのがわかりました。

赤枠部分が画像ダウンロードからLCP表示にかかる時間です

その間の処理を見てみると、 HTMLのパース(青い部分) と、 CSSのスタイル計算(紫の部分) の処理が発生しているのがわかりました。

このことから、preloadで目的の画像のダウンロードのタイミングを早くできたとしても、レンダリングするまでの他の処理が終わらないと LCP のレンダリングは早くできないことがわかりました。

この分析をもとに、次の仮説課題として HTMLのパースの時間CSSのスタイル計算の時間をなくすor短くする方法を議論して、 LCP の表示のタイミングを早くするように繰り返しました。

このように1つの仮説を改善して、分析し、次の仮説を改善するのをひたすら繰り返していきました。

改善していて効果的だったもの

改善を繰り返していくなかで下記の改善がとくに効果バツグンでした。

  • JavaScriptファイルの分割と遅延読み込み
  • 要素、DOM数の削減

JavaScriptファイルの分割、遅延読み込み

webpack でJavaScriptファイルを1つのファイルにバンドルすることで、ブラウザにキャシュさせる」
という思想でJavaScriptファイルの1ファイル運用を続けていたのですが、サービスに機能が増えるにつれてファイルサイズが大きくなりボトルネックになっていました。

原因としては、特定のページでしか使わない機能が、同じJavaScriptファイルに同梱されているのが原因でした。

そこでファイルサイズを小さくするためにCodeSpliting を使って、ファイルを分割しました。

上記を行った結果、JavaScriptのファイルサイズが 700kB → 150kB という大幅な削減ができました。

さらに分割したJavaScriptファイルはIntersectionObserver を使って、画面内に入ったタイミングで読み込ませるようにしました。

これにより、画像の遅延読み込みと同じように、画面外のものはファイルを読み込ませず、必要なタイミングで必要なJavaScriptファイルだけを読み込ませられるようにできました。

その結果、ダウンロードサイズの削減とJavaScriptの実行時間を減少させることができました。

要素、DOM数の削除

JavaScriptのファイルサイズがパフォーマンスに影響が出るのは当然と思っていましたが、HTMLのファイルサイズもパフォーマンスに大きな影響を出していました。

ファイルサイズが大きいと下記の要因で全体のパフォーマンスが著しく悪くなります

  1. HTMLのダウンロードに時間がかかる
    • とくにモバイル環境下はnetworkが強くないので顕著に悪くなります
  2. HTMLのパースに時間かかる
    • ファイルサイズが大きくなると、HTMLのパースが分割されてstyleの再計算が複数回実行されます

      分割してパースされている例

HTMLのファイルサイズを減らすためにはDOMの削減が必要なので、DOMの削減ができる方法をいくつか洗い出して、削れそうな方法をリストアップして段階的にDOMの数を減らしていきました。

減らすために実施した施策の中でも下記の3つが効果的でした。

  • pictureタグをつかったwebp/非webp画像の配信切り替えの廃止
  • 要素が多い箇所のDOMをlazyloadにして、window内に入るまでDOMを出力しない
  • UI要素の削除

pictureタグをつかったwebp/非webp画像の配信切り替えの廃止

webpだと画像のファイルサイズが、小さくなるメリットがありますが、対応していないブラウザがいくつかあります。

すべてのユーザーに画像を表示させて、webp対応しているブラウザにはwebpを、それ以外には非webpの画像を配信させるのが理想です。

パフォーマンス改善前はwebpを対応するために、すべての画像に下記のように pictureタグ で画像を指定していました。

<picture>
  <source type="image/webp" srcset="sample.webp">
  <img src="sample.jpg">
</picture>

しかし、この pictureタグ での指定、ページ内に画像が大量にあるとDOMの数が大量になってしまいます。

弊社の場合だとホテルの画像を大量に紹介したいケースがあり、すべての画像に対して pictureタグ で画像を指定していたのでDOMの数が非常に多くなっていました。

そこで、弊社の画像配信サーバーに、 アクセスしてきたユーザーのUAを見てwebpと非webpの切り替えを行える機能 を追加してもらいました。

この機能を使ったことで pictureタグ を使わずに imgタグ だけで最適な画像フォーマットを配信できるようになり、DOM数の大幅な削減ができました。

要素が多い箇所のDOMをlazyloadにして、画面内に入るまでDOMを出力しない

ホテルの空室がわかるカレンダーを記事でも見られるようにしているのですが、この部分をすべてHTMLファイルに出力していました。

DOM数を計算してみると 3ヶ月分の日付(約90日) x 紹介しているホテルの数(約20前後) のDOMが出力されていて、少なくとも1800個以上のDOMがこのカレンダーの箇所だけで出力されていました。
しかも空室かどうかの状況を取得するために、非同期でAPIから料金を取得して表示していました。

あまりにもDOM数が多く、しかも非同期で書き換わる部分だったので、この部分のDOMをすべて React に置き換えて、画面内に表示されるまではレンダリングしないようにしました。

DOMの生成をJavaScriptに任せたおかげで、閲覧時のHTMLパースの負荷がなくなり、HTMLのファイルサイズも大幅に減らすことができました。

UI要素の削除

DOM数の削減に、コンポーネント全体を囲っているdivなどを減らしたりもしましたが、DOMの数は中々減りませんでした。

このことを Yusuke Wada に相談したところ、UI要素を消してしまうのはどうかとアドバイスをもらいました。

DOM数の削減も頭打ちになりかけていたので、他部署にも相談をして進めていきました。
パフォーマンスのためとはいえ、UIを削除してしまうとコンバージョンや滞在時間などに関わるため、大きな影響を出さずに削除できそうな箇所を探しました。

そこで見つけたのが、ホテルの紹介で使っている画像一覧のカルーセルとサムネイル一覧です。

DOM数も非常に多く、必須の部分ではなかったため、この部分を廃止して1枚の画像だけを見せるようにしてもいいか相談し削除しました。

その結果、DOMの数は大幅に減らすことができ、さらにはカルーセルを生成するJavaScriptの処理もなくなったため、メインスレッドの処理も大幅に減りました。

要素、DOMの削除の完了後

上記を行った結果 9903 → 1815 というDOM数まで大きく削減できました。

大量にDOMを削減できた結果、HTMLのパースの回数も3回から1回に減りました。

before

多大なDOM数のせいでパースが分割されています

after

3つに分割されていたパース処理も、ほぼ1回のパースだけになりました

HTMLのパース回数が減ったことによって、 LCP が表示されるまでの時間の削減と、メインスレッド処理も大幅に減り、全体的な改善が大幅にできました。

見えてきた傾向と意識してきたこと

上記の施策を通して、 パフォーマンス改善において、とくに重要だと思ったのは下記の考え方でした。

  • 遅延表示と遅延実行は重要
    • ファーストビュー以外の画像や、画面外の不要な要素は遅延表示させる
    • JavaScriptの実行は必要なタイミングで必要なものだけ実行させる
  • ページ内の要素を少なくする
  • LCPの改善は修正しやすくてスコアも上がりやすい
  • 簡単なものをやるだけでもスコアはあがる
  • 1回の施策で大きく改善することは少ないので、少しずつ何回も改善させていく

モバイルでは低スペックでもパフォーマンスを出す必要があるため、

  • 画面外の処理を減らすこと
  • リソースのファイルサイズの縮小化
  • メインスレッドの処理を少なくする

などを、シビアに直す必要があり、遅延表示と遅延実行はとくに重要でした。

また、分析する際には LCP FID CLS の定義と評価方法を熟読するのが役に立ちました。

定義や採点の基準などがすべてまとまっていて、改善のヒントになることがいくつもありました。

中でもLCP については Lighthouse での採点配分が多く比較的直しやすいので、重点的に改善しました。

Lighthouse上に計算ツールのリンクがあるのでクリック

採点の計算ツールが使えるので分析に役立ちます

Lighthouse の計算ツールも採点配分が乗っていたり、点数の予測に計算ができたので、公式のツールはとても役立ちました。

やってきた施策

仮説通り大幅に改善した施策もありますが、中には改善に効果がなかった施策も数多くありました。
今後の改善のヒントとなるように、やってきた施策について下記にまとめてみました。

  • 画像をwebp化、トリミング、Lazyload
  • imgタグのサイズ指定
  • pictureタグの削除
  • 余分なpreloadの削除
  • LCP画像のpreload
  • iconフォントのpreload
  • 不要なcssの削除
  • ページ専用のcssを作成
  • facebookやtwitterなどの外部リソースの除外
  • SNSボタンの削除
  • Reactコンポーネントのdynamic import、Lazyload
  • reduxとsagaのdynamic import、Lazyload
  • webpack-bundle-analyzerで不要なライブラリのコードを削除
  • lodash、momentのimportを最適化
  • コンポーネントのレンダリングをlazyload化
  • assetsファイルのExpiresヘッダーの付加
  • assetsファイルのgzip圧縮
  • 余分なDOMの削除
  • カルーセル機能の削除
  • Varnish/FastlyでのHTMLキャッシュ
  • AMPページにAmpOptimizerの導入
  • http3の導入
  • Brotoli圧縮
  • ServiceWorkerでassetsとHTMLをキャッシュ
  • 画像をdecoding=async対応
  • prebid広告の並列読み込み
  • 広告の遅延読み込み
  • 次のページのprefetch
  • 計測ツールにCalibreの導入
  • 分析ツールにCloudflare Web Analyticsの導入
  • CWVの値をGoogleAnalyticsへ送信して分析データを集める
  • Instagramやtwitterのembedをbento-amp化
  • YouTubeのembedなどのiframeにloading=lazyを追加
  • flickrのembedにloading=lazyを追加
  • 外部ドメインのdns-prefetch
  • 遅延描画のコンポーネントに高さを指定
  • requestIdleCallbackでメイン以外の処理を遅延させる
  • スケルトンローダーの簡略化
  • inView処理をIntersectionObserverにする
  • JavaScriptで要素のサイズ変更処理をしないようにする
  • など、その他多数

取り組み前から現在の結果

取り組み前は目も当てられない点数でしたが、改善を進めたおかげで高得点をとれるようになりました。

取り組み前

取り組み後〜現在

まだまだ改善できるところはありますが、改善したパフォーマンスを下げないためにパフォーマンス監視ツールの calibre を導入しました。

毎週月曜日にパフォーマンスについてのMTGを行うことで、常にパフォーマンスの改善ができるようにしました。

まとめ

以上、長くなりましたが、トラベルブックのフロントエンドチームのパフォーマンス改善についてまとめてみました。

SEOのためではありましたが、ユーザー体験の向上につながるので、パフォーマンス改善は非常に大事だと思いましたし、実際にすごく早くなったのを見るとページの閲覧が楽しくなりました。

今回は Page Speed Insights での改善について書きましたが、ラボデータ のスコアを目標にあげていきました。
もう1つ重要なデータとして フィールドデータ というものがあり、現在はフィールドデータの改善を進めています。
さらに深堀りしたい方はこの記事も読んでみてください。

エンジニア募集中!

最後に、トラベルブックではエンジニアを募集中です。
パフォーマンス改善に興味があるかた、フロント、バックエンドにかかわらずご興味があればご応募ください!