🌞

Lighthouse CIを使って継続的なパフォーマンスモニタリングを行う

2024/02/12に公開

はじめに

Webパフォーマンスとは、Webサイトやウェブアプリケーションがユーザーにどれだけ迅速に、効率的に、そして安定してサービスを提供できるかを測定する指標のことを指します。これにはページの読み込み速度、インタラクティブ性、安定性などが含まれます。ユーザー体験を最適化するためには、これらのパフォーマンス指標を継続的に監視し、必要に応じて改善することが重要です。本記事では、そのためのツールとして「Lighthouse CI」を使用して継続的なパフォーマンスモニタリングを行う方法について解説します。

どうしてパフォーマンスを改善することが重要なのか

誰しも必ず次のような場面に遭遇したことがあると思います。それは例えば、サイトに訪れたとき、5秒以上ずっと真っ白な画面やローディングが流れていたり、ボタンをクリックしたはずなのに何も反応しなかったり、画像の読み込みが遅く最後まで待っても代替テキストしか出てこなかった、というようなものです。これらのユーザーエクスペリエンスは、実際にサービスの評価や利益にも直結していることがわかっています。

https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale
上記のBBCによるニュースでは、サイトの読み込みに1秒かかるごとに10%のユーザーをさらに失うということを発表しています。
また、次の記事では楽天がWeb Vitalsという定量的なパフォーマンス指標の改善を図り、訪問者あたりの収益が53.37%、コンバージョン率(Webサイトやページを訪れたユーザーのうち、商品の購入や問い合わせなど、最終的な成果に至った人の割合)が 33.13%増加したとの結果が出ています。
https://web.dev/case-studies/rakuten?hl=ja

このように、Webサイトにおけるパフォーマンスはビジネスとも深く結びついており、継続的な監視と改善が必要です。

Core Web Vitals について

先ほど出てきたWeb Vitalsとは、Googleが設定しているWebサイトのパフォーマンスを定量的に測定できる指標のことです。その中でも、すべてのウェブページに適用される最も重要な指標としてLCP(Largest Contentful Paint)FID(First Input Delay)CLS(Cumulative Layout Shift)の3つがあります。

Core Web Vitalsの各指標の詳細
  • Largest Contentful Paint(LCP): 読み込みのパフォーマンスを測定します。優れたユーザー エクスペリエンスを提供するため、LCP はページの読み込みが最初に開始してから 2.5 秒以内に発生する必要があります。
  • First Input Delay(FID): インタラクティビティを測定します。優れたユーザー エクスペリエンスを提供するには、ページの FID を 100 ミリ秒以下にする必要があります。
  • Cumulative Layout Shift(CLS): 視覚的な安定性を測定します。優れたユーザー エクスペリエンスを提供するには、CLS を 0.1. 以下に維持する必要があります。

Lighthouse とは

上記のような指標の改善を目指すことはWebサイトのパフォーマンス、アクセシビリティ、SEO向上の施策の一つになります。
また、これらの指標を測定するにはいくつかの方法がありますが、より手軽で一般的なものとしてはChromeのデベロッパーツールで見れるLighthouseがあります。
例えば、https://web.dev/?hl=ja のページを測定してみると以下のような結果になりました。
web.devのホームページのLighthouseスコア

赤→黄色→緑の順に良いスコアとなっていきます。Webサイト全体の中でどのページが特にパフォーマンスのスコアが低いかなどを特定したり、その原因をやんわりと掴むのに良いツールです。

Lighthouse CIを使って継続的なパフォーマンス監視を行う

Lighthouse CIとは

https://github.com/GoogleChrome/lighthouse-ci

Lighthouse CI は、継続的インテグレーションで Lighthouse を使用するためのOSSです。例えば、GitHub ActionsのPRのワークフローの中に取り入れ、特定のURLに対してLighthouseを実行し、PRの変更によってパフォーマンスバジェットなどで決めた値を下回らないかなどをチェック項目として取り入れることができます。
また、チェックを使用しない場合も有効で、PRに対して発行されるHTMLのレポートを見ることでチームメンバーがパフォーマンスについて意識するきっかけを作ることができます。

Lighthouse CI Server を使う

上記のように毎回発行されるレポートを都度見られるようにするだけでもパフォーマンス改善への第一歩になりますが、Lighthouse CIで提供されているLighthouse CI Serverを使うことでPRごとの各指標の遷移をグラフ確認できるようになり、ダッシュボードとして視覚的にわかりやすくスコアを確認できるようになります。

Lighthouse CI Server のダッシュボードの例
出典:https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/server.md#overview

今回はこのLighthouse CI Serverを立てて、GitHub Actionsを使ってPR単位でLighthouseを実行し、継続的なスコアの監視ができるように実装していきます。

Lighthouse CI Server の構築

まず、Lighthouse CI ServerのREADMEのようなものを見て見ると、LHCI ServerのデプロイのためにはHerokuやDockerでできそうなことがわかります。

See the Heroku and docker recipes for more examples on how to deploy the LHCI server.

また、LHCIを使ったサーバー構築にはNode.jsとDatabase Storage(sqlite, mysql, or postgresql)が必要とのことです。
https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/server.md#requirements

今回は自分のブログサイトを対象にLHCIサーバーを立てたく、個人開発の領域のためあまりお金をかけたくありません。そのため、有料になってしまったHerokuではなくDockerを使ってなるべく安くサーバーを立てられる方法を探します。
Herokuの代わりとしてよくCloud Runでデプロイする方法が挙げられており、無料枠もあるのでCloud Runでデプロイすることに決定します。
しかしここで一つ懸念点があり、Cloud Runはステートレスなサービスであり、各リクエストは新しいコンテナインスタンスで処理されるため、コンテナ内に保存されたデータはリクエストが終了すると消えてしまいます(データの永続化ができない)。今回は各PRで生成されたスコアを蓄積する必要があるため、ただCloud Runでデプロイするだけではダメそうです。
そこで調べてみると、LitestreamというOSSを使えばSQLite のデータベースファイルをAmazon S3 や Google Cloud Storage などのオブジェクトストレージにリアルタイムでレプリケートすることができるようです。これでデータの永続化を達成できそうです!

https://zenn.dev/kou_pg_0131/articles/google-cloudrun-litestream#litestream-とは?

上記の記事ではCloud Storageを使っているのですが、無料枠が少なくはみ出そうです。そこで、AWS S3互換であり無料枠が大きいCloudflare R2はどうかと思い調べると使えそうだったので、こちらを使うことにします!

LHCIサーバーの最終的なプロジェクトは以下で見れます。
https://github.com/yajihum/lhci

Cloudflare R2 で新しいバケットを作成する

以下の手順で新しいバケットの成とAPIトークンの発行を行います。

  1. Cloudflareのアカウントを持っていない方は新しく作り、ログインします
  2. 左のサイドバーがらR2のリンクをクリックします
  3. 右上にある青い「バケットを作成する」ボタンをクリックし、バケット名を好きなものにし、位置情報はAsia-Pacificにして作成ボタンを押します
  4. バケット一覧に戻り、「R2 API トークンの管理」のリンク先に飛びます
  5. 「API トークンを作成する」ボタンをクリックします
  6. 適当なトークン名を入力し、「管理者読み取りと書き込み」にチェックを入れ後はデフォルトの設定にし、「API トークンを作成する」ボタンをクリックします
  7. 表示されたアクセスキーとシークレットアクセスキー、エンドポイントをメモして控えておきます(👁️アクセスキーとシークレットアクセスキーは閉じたら二度と見られないため注意)
    APIトークン情報の例
    これでCloudflare R2の設定は大体完了です!

package.json の作成

LHCIサーバーはNode.js環境で実行されます。package.jsonを用意します。
以下のレシピにあるpackage.jsonをそのままコピーして作成します。

https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/recipes/docker-server/package.json

lighthouserc.json の作成

レシピにあるlitehouserc.jsonをそのままコピーして作成します。
https://github.com/yajihum/lhci/blob/main/lighthouserc.json#L1-L12

ここでポート9001を返してやり取りを行うということを少し頭の片隅に入れておきます。
Configurationファイルについては以下で詳しく書かれています。
https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md

また、sqlDatabasePathにあるように/data/lhci.dbにdbファイルが作成されます。

Dockerfile の作成

まずはローカルでサーバーを動かして確認するためにイメージをビルドするための
Dockerfileを作成していきます。(先ほどのkokiさんの記事を参考にさせていただきました🙏)
https://github.com/yajihum/lhci/blob/main/Dockerfile#L1-L32

LHCIサーバーのDockerfileにあるものと組み合わせてビルドステージとランタイムステージの2つに分けています。
特に苦戦したところは、LitestreamからR2のバケットへアクセスする際に証明書関係のエラー[1]が出てしまい、コンテナを起動できなかったので、以下のように証明書のインストールを行う必要がありました。

# 証明書のインストール
RUN apt-get update && apt-get install -y ca-certificates

litestream.ymlrun.shは次以降で説明します。

litestream.yml の作成

litestream.ymlは以下のようになっています。
https://github.com/yajihum/lhci/blob/main/litestream.yml#L1-L9

環境変数を通してR2のエンドポイントやアクセスキーなどを代入しています。参照する環境変数を置いておく.envファイルも作成します。
.env.sampleファイルを.envに名前を変え、エンドポイントURL・バケット名・アクセスキー・シークレットアクセスキーのキーに対してCloudflare R2で新しいバケットを作成するセクションでメモした値をそれぞれ代入します。
この.envファイルの参照はローカルでDockerを使用するとき用で、Cloud Runにコンテナをデプロイするときは別で環境変数の設定が必要です。
.envファイルに書かれている内容は外部に漏れるとまずいので、.gitignoreファイルをルートに新しく作成し、.envファイルをGit管理の対象外にしておきます。
https://github.com/yajihum/lhci/blob/main/.gitignore#L1-L2

また、データベースのファイル先は/data/lhci.dbとなっており、これは先ほど作成したlighthouserc.jsonsqlDatabasePathとして指定したパスです。

その他、細かな設定は以下を参照してください。
https://litestream.io/reference/config/

run.sh ファイルの作成

run.shファイルは以下のように作成します。
https://github.com/yajihum/lhci/blob/main/run.sh#L1-L11

Litestreamでリストア(データベースファイルの復元)→レプリケート(データベースの内容を復元)してLHCIサーバーをポート9001でnpm startして起動します。

npm startは先ほど作成したpackage.jsonに記載されており、lhci server --config=./lighthouserc.jsonを行うスクリプトです。このコマンドがLHCIサーバーをlighthouserc.jsonを設定ファイルとして起動するという意味になります。

docker-container-run.sh の作成

ローカルでコンテナを起動する際のスクリプト(docker-container-run.sh)を以下のように作成します。

ここではオプションとして、9001のポート.envファイルを環境変数が書かれたファイルとして指定し、lhciというイメージでコンテナを起動することを意味しています。
https://github.com/yajihum/lhci/blob/main/docker-container-run.sh#L1-L3

ローカルでDockerを立ち上げて動作を確認する

ここでローカルでDockerを立ち上げて動作確認ができる状態になったためDocker Desktopアプリケーションを起動し、以下のコマンドでイメージのビルドとコンテナを立ち上げます。

docker image build -t lhci .
chmod +x ./docker-container-run.sh
./docker-container-run.sh
open http://localhost:9001/app/

イメージ↓
Docker DesktopのImageリスト

コンテナ↓
Docker DesktopのContainerリスト

lhciという名前のイメージと、そのイメージを持つコンテナが作成されていることがわかります。

コンテナに移動し、詳しくみていきましょう。
Filesのタブを開き、dataの中身を見てみるとlhci.dbが存在していることがわかります。
/dataの中にlhci.dbが存在している

これはlighthouserc.json の作成で見た、sqliteのdbファイルの保存先sqlDatabasePathの値と同じです。

また、/usr/srcを見てみるとlhciのフォルダーがあることがわかります。
/usr/srcの中にlhciフォルダーが存在している

ローカルでLHCI Serverにアクセスする

http://localhost:9001/app/projects
上記のURLにアクセスしてみると「Welcome to Lighthouse CI!」というメッセージのサイトが立ち上がっているはずです。
これがLHCI Serverになります。
まだ、プロジェクトを作成していないので他に何も表示されないですが、正しくコンテナが起動することを確認できたため、Cloud Runにデプロイしてみます。

Cloud Run にデプロイする

GCPでアカウント登録をしていない場合は行い、適当な新しいプロジェクトを作成します。

続いて、以下のコマンドを実行し、イメージのビルドを行います。

gcloud builds submit --tag gcr.io/PROJECT_ID/lhci

PROJECT_IDには先ほど作成したプロジェクトのIDを入れます。
ビルドしたイメージはGCPのArtifact Resistryにあります。
GCPのArtifact Resistryにgcr.ioのリポジトリが存在している

最後にコンテナイメージをCloud Runにデプロイします。
.envファイルに記載した環境変数をそれぞれ代入することに注意してください。

gcloud run deploy --image gcr.io/PROJECT_ID/lhci --platform managed --port 9001 --set-env-vars R2_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com/,R2_BUCKET=xxxxx,R2_ACCESS_KEY_ID=yyyyy,R2_SECRET_ACCESS_KEY=zzzzz

サービス名やリージョンについて聞かれるので好きなように設定してください。
https://cloud.google.com/run/docs/locations?hl=ja

そうすると以下のようにCloud Runのページにサービスが作成されているはずです。
Cloud Runのページにlhciのサービスができている

作成したサービスのページに飛ぶとhttps://~.a.run.appというリンクがあるのでそこにアクセスしてみると、先ほどローカルで動作確認した時と同じ画面が出ていると思います。

これでLHCIサーバーのデプロイは完了です!!

LHCI プロジェクトの作成

LHCIサーバーで管理する、LHCIでテストしたいアプリケーションのプロジェクトをあげる必要があります。

以下のコマンドをそれぞれ実行して新しいプロジェクトを作成します。

npm install -g @lhci/cli@0.13.x
lhci wizard
? Which wizard do you want to run? new-project
? What is the URL of your LHCI server? https://your-lhci-server.example.com/
? What would you like to name the project? My Favorite Project
? Where is the project's code hosted? https://github.com/yajihum/lhci
  • Which wizard do you want to run?new-projectを選択します
  • What is the URL of your LHCI server?:Cloud runで先ほど作成されたリンク先を書きます
  • What would you like to name the project?:LHCIサーバー上で管理するtきのプロジェクト名です
  • Where is the project's code hosted?:アプリケーションのソースコードがあるリンクを書きます(そこまで重要じゃないので適当でOK)

上記を実行した後、次のようにビルドトークンとアドミントークンが生成されるのでそれぞれメモしておきます。

Created project My Favorite Project (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)!
Use build token XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX to connect.
Use admin token XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX to manage the project.

アプリケーションでGitHub ActionsのワークフローにLHCIを取り入れてテストを行う

いよいよ終盤に近づいてきました。今回は手軽に導入できるGitHub Actionsを使ってLHCIのワークフローを実行しようと思います。

lighthouserc.js の作成

ワークフローを取り入れてLighthouseスコアの集計を行いたいアプリケーションのプロジェクトのルートにligthouserc.jsファイルを作成し、中身を以下のようにします。
https://github.com/yajihum/lhci/blob/main/app/lighthouserc.js#L1-L16

  • ci > collect > urlhttp://localhost:3000から始まる、テスト対象のパスを記入します。例えば、/blogページをテストしたい場合はhttp://localhost:3000/blogを追加します
  • numberOfRuns:各URLに対してLighthouseのテストを行う回数です。数が多いほど時間がかかりますが結果が安定するようになります
  • serverBaseUrl:Cloud Runにデプロイした時のリンクを記入します

GitHub Actions の設定

  1. プロジェクトルートに.github/workflows/ci.ymlを作成します
  2. ci.ymlの中身を以下のようにします(npmの部分は好きなパッケージマネージャーに変えてください)

https://github.com/yajihum/lhci/blob/main/app/.github/workflows/ci.yml#L1-L22

  1. LHCI_TOKENをGitHub Actionsのシークレットに登録する
    アプリケーションのGitHubページの/settings/secrets/actionsに移動して、「New repository secret」ボタンをクリックします。そして、NameフィールドにLHCI_TOKENSecretフィールドに先ほどメモしたビルドトークンを代入し保存します。
    GitHub Actionsのシークレットの設定画面

これでアプリケーションにpushするたびにLHCIのワークフローが実行され、Cloud Runにデプロイしたリンク先でLighthouseスコアが見れるようになりました!

実際にLHCIサーバーを見てみる

自分のブログサイトで試しに数回計測してみると確かに蓄積されたデータを見れることがわかります!これで運用できそうです😳
yajihum.devのLighthouseスコア遷移画面

R2とCloud Runの料金を見てみる

気になる料金ですが、10~20回くらいアクセスしたり、数回デプロイを試した時の金額を試しに見てみます。

Cloudflare R2に関しては無料枠が大きすぎるため、まだまだオーバーする気配がなく個人利用であれば安心して無料で使えそうです。Cloud StorageやS3を使わずに基本的にR2で済ませたほうが全然良いですね〜
Cloudflare R2のlighthouse-ciバケットの利用データ

無料枠は以下のようになっています。

  • ファイルサイズ:10GB/月まで
  • クラスA: 1億回/月のリクエストまで
  • クラスB:10億回/月のリクエストまで

https://developers.cloudflare.com/r2/pricing/#r2-pricing

まだまだ余裕あるので大丈夫そうですね。

Cloud Runはまだ数円しかかかっていないのでそこまで高くならなそうでよかったです。
Cloud Runの利用明細

節約するために

「適当にワークフロー回してたらこんなにお金かかってた😵」のような状態を避けるためにこまめにコストを確認することが必要です。
特にワークフローを実行するブランチを限定したり、R2オブジェクトのライフサイクルを設定し一定期間で削除することでオブジェクトのサイズを減らしたりなどの工夫をやるといいかも知れません。

終わりに

Webパフォーマンスの改善のためのLighthouseCI実装手順について説明しました。
特にligthouse-ciの公式ページにない実装だったので、皆さんの参考になれば嬉しいです!
当方諸々初心者なところがあるので間違っている部分や「もっとこうしたほうがいい!」みたいなところがあればコメントするかGitHubでPRを送っていただけると助かります!

そして今回もCloudflareは神ということを実感しました😊
みんなもLitestreamを使うときはCloud StorageやS3ではなくR2を使おう😊

参考

https://web.dev/learn/performance/why-speed-matters?hl=ja

https://blog.koh.dev/2020-10-24-lighthouse-ci/

https://pragmaticpineapple.com/deploying-and-configuring-lightouse-ci-server/

https://zenn.dev/kou_pg_0131/articles/google-cloudrun-litestream

https://web.dev/articles/lighthouse-ci?hl=ja

脚注
  1. level=ERROR msg="failed to run" error="cannot fetch generations: RequestError: send request failed\ncaused by: Get "https://xxxxxxx.r2.cloudflarestorage.com/lighthouse-ci?delimiter=%2F&prefix=generations%2F": tls: failed to verify certificate: x509: certificate signed by unknown authority" ↩︎

GitHubで編集を提案

Discussion