Lighthouse CIを使って継続的なパフォーマンスモニタリングを行う
はじめに
Webパフォーマンスとは、Webサイトやウェブアプリケーションがユーザーにどれだけ迅速に、効率的に、そして安定してサービスを提供できるかを測定する指標のことを指します。これにはページの読み込み速度、インタラクティブ性、安定性などが含まれます。ユーザー体験を最適化するためには、これらのパフォーマンス指標を継続的に監視し、必要に応じて改善することが重要です。本記事では、そのためのツールとして「Lighthouse CI」を使用して継続的なパフォーマンスモニタリングを行う方法について解説します。
どうしてパフォーマンスを改善することが重要なのか
誰しも必ず次のような場面に遭遇したことがあると思います。それは例えば、サイトに訪れたとき、5秒以上ずっと真っ白な画面やローディングが流れていたり、ボタンをクリックしたはずなのに何も反応しなかったり、画像の読み込みが遅く最後まで待っても代替テキストしか出てこなかった、というようなものです。これらのユーザーエクスペリエンスは、実際にサービスの評価や利益にも直結していることがわかっています。
また、次の記事では楽天がWeb Vitalsという定量的なパフォーマンス指標の改善を図り、訪問者あたりの収益が53.37%、コンバージョン率(Webサイトやページを訪れたユーザーのうち、商品の購入や問い合わせなど、最終的な成果に至った人の割合)が 33.13%増加したとの結果が出ています。
このように、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サイト全体の中でどのページが特にパフォーマンスのスコアが低いかなどを特定したり、その原因をやんわりと掴むのに良いツールです。
Lighthouse CIを使って継続的なパフォーマンス監視を行う
Lighthouse CIとは
Lighthouse CI は、継続的インテグレーションで Lighthouse を使用するためのOSSです。例えば、GitHub ActionsのPRのワークフローの中に取り入れ、特定のURLに対してLighthouseを実行し、PRの変更によってパフォーマンスバジェットなどで決めた値を下回らないかなどをチェック項目として取り入れることができます。
また、チェックを使用しない場合も有効で、PRに対して発行されるHTMLのレポートを見ることでチームメンバーがパフォーマンスについて意識するきっかけを作ることができます。
Lighthouse CI Server を使う
上記のように毎回発行されるレポートを都度見られるようにするだけでもパフォーマンス改善への第一歩になりますが、Lighthouse CIで提供されているLighthouse CI Serverを使うことでPRごとの各指標の遷移をグラフ確認できるようになり、ダッシュボードとして視覚的にわかりやすくスコアを確認できるようになります。
出典: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)が必要とのことです。
今回は自分のブログサイトを対象にLHCIサーバーを立てたく、個人開発の領域のためあまりお金をかけたくありません。そのため、有料になってしまったHerokuではなくDockerを使ってなるべく安くサーバーを立てられる方法を探します。
Herokuの代わりとしてよくCloud Runでデプロイする方法が挙げられており、無料枠もあるのでCloud Runでデプロイすることに決定します。
しかしここで一つ懸念点があり、Cloud Runはステートレスなサービスであり、各リクエストは新しいコンテナインスタンスで処理されるため、コンテナ内に保存されたデータはリクエストが終了すると消えてしまいます(データの永続化ができない)。今回は各PRで生成されたスコアを蓄積する必要があるため、ただCloud Runでデプロイするだけではダメそうです。
そこで調べてみると、LitestreamというOSSを使えばSQLite のデータベースファイルをAmazon S3 や Google Cloud Storage などのオブジェクトストレージにリアルタイムでレプリケートすることができるようです。これでデータの永続化を達成できそうです!
上記の記事ではCloud Storageを使っているのですが、無料枠が少なくはみ出そうです。そこで、AWS S3互換であり無料枠が大きいCloudflare R2はどうかと思い調べると使えそうだったので、こちらを使うことにします!
LHCIサーバーの最終的なプロジェクトは以下で見れます。
Cloudflare R2 で新しいバケットを作成する
以下の手順で新しいバケットの成とAPIトークンの発行を行います。
- Cloudflareのアカウントを持っていない方は新しく作り、ログインします
- 左のサイドバーがら
R2
のリンクをクリックします - 右上にある青い「バケットを作成する」ボタンをクリックし、バケット名を好きなものにし、位置情報はAsia-Pacificにして作成ボタンを押します
- バケット一覧に戻り、「R2 API トークンの管理」のリンク先に飛びます
- 「API トークンを作成する」ボタンをクリックします
- 適当なトークン名を入力し、「管理者読み取りと書き込み」にチェックを入れ後はデフォルトの設定にし、「API トークンを作成する」ボタンをクリックします
- 表示されたアクセスキーとシークレットアクセスキー、エンドポイントをメモして控えておきます(👁️アクセスキーとシークレットアクセスキーは閉じたら二度と見られないため注意)
これでCloudflare R2の設定は大体完了です!
package.json の作成
LHCIサーバーはNode.js環境で実行されます。package.jsonを用意します。
以下のレシピにあるpackage.jsonをそのままコピーして作成します。
lighthouserc.json の作成
レシピにあるlitehouserc.jsonをそのままコピーして作成します。
ここでポート9001を返してやり取りを行うということを少し頭の片隅に入れておきます。
Configurationファイルについては以下で詳しく書かれています。
また、sqlDatabasePath
にあるように/data/lhci.db
にdbファイルが作成されます。
Dockerfile の作成
まずはローカルでサーバーを動かして確認するためにイメージをビルドするための
Dockerfileを作成していきます。(先ほどのkokiさんの記事を参考にさせていただきました🙏)
LHCIサーバーのDockerfileにあるものと組み合わせてビルドステージとランタイムステージの2つに分けています。
特に苦戦したところは、LitestreamからR2のバケットへアクセスする際に証明書関係のエラー[1]が出てしまい、コンテナを起動できなかったので、以下のように証明書のインストールを行う必要がありました。
# 証明書のインストール
RUN apt-get update && apt-get install -y ca-certificates
litestream.yml
とrun.sh
は次以降で説明します。
litestream.yml の作成
litestream.ymlは以下のようになっています。
環境変数を通してR2のエンドポイントやアクセスキーなどを代入しています。参照する環境変数を置いておく.env
ファイルも作成します。
.env.sample
ファイルを.env
に名前を変え、エンドポイントURL・バケット名・アクセスキー・シークレットアクセスキーのキーに対してCloudflare R2で新しいバケットを作成するセクションでメモした値をそれぞれ代入します。
この.env
ファイルの参照はローカルでDockerを使用するとき用で、Cloud Runにコンテナをデプロイするときは別で環境変数の設定が必要です。
.env
ファイルに書かれている内容は外部に漏れるとまずいので、.gitignore
ファイルをルートに新しく作成し、.env
ファイルをGit管理の対象外にしておきます。
また、データベースのファイル先は/data/lhci.db
となっており、これは先ほど作成したlighthouserc.json
のsqlDatabasePath
として指定したパスです。
その他、細かな設定は以下を参照してください。
run.sh ファイルの作成
run.shファイルは以下のように作成します。
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
というイメージでコンテナを起動することを意味しています。
ローカルでDockerを立ち上げて動作を確認する
ここでローカルでDockerを立ち上げて動作確認ができる状態になったためDocker Desktopアプリケーションを起動し、以下のコマンドでイメージのビルドとコンテナを立ち上げます。
docker image build -t lhci .
chmod +x ./docker-container-run.sh
./docker-container-run.sh
open http://localhost:9001/app/
イメージ↓
コンテナ↓
lhci
という名前のイメージと、そのイメージを持つコンテナが作成されていることがわかります。
コンテナに移動し、詳しくみていきましょう。
Files
のタブを開き、data
の中身を見てみるとlhci.db
が存在していることがわかります。
これはlighthouserc.json の作成で見た、sqlite
のdbファイルの保存先sqlDatabasePath
の値と同じです。
また、/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
にあります。
最後にコンテナイメージを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
サービス名やリージョンについて聞かれるので好きなように設定してください。
そうすると以下のようにCloud Runのページにサービスが作成されているはずです。
作成したサービスのページに飛ぶと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
ファイルを作成し、中身を以下のようにします。
-
ci > collect > url
:http://localhost:3000
から始まる、テスト対象のパスを記入します。例えば、/blog
ページをテストしたい場合はhttp://localhost:3000/blog
を追加します -
numberOfRuns
:各URLに対してLighthouseのテストを行う回数です。数が多いほど時間がかかりますが結果が安定するようになります -
serverBaseUrl
:Cloud Runにデプロイした時のリンクを記入します
GitHub Actions の設定
- プロジェクトルートに
.github/workflows/ci.yml
を作成します -
ci.yml
の中身を以下のようにします(npmの部分は好きなパッケージマネージャーに変えてください)
-
LHCI_TOKEN
をGitHub Actionsのシークレットに登録する
アプリケーションのGitHubページの/settings/secrets/actions
に移動して、「New repository secret」ボタンをクリックします。そして、Name
フィールドにLHCI_TOKEN
、Secret
フィールドに先ほどメモしたビルドトークンを代入し保存します。
これでアプリケーションにpushするたびにLHCIのワークフローが実行され、Cloud Runにデプロイしたリンク先でLighthouseスコアが見れるようになりました!
実際にLHCIサーバーを見てみる
自分のブログサイトで試しに数回計測してみると確かに蓄積されたデータを見れることがわかります!これで運用できそうです😳
R2とCloud Runの料金を見てみる
気になる料金ですが、10~20回くらいアクセスしたり、数回デプロイを試した時の金額を試しに見てみます。
Cloudflare R2に関しては無料枠が大きすぎるため、まだまだオーバーする気配がなく個人利用であれば安心して無料で使えそうです。Cloud StorageやS3を使わずに基本的にR2で済ませたほうが全然良いですね〜
無料枠は以下のようになっています。
- ファイルサイズ:10GB/月まで
- クラスA: 1億回/月のリクエストまで
- クラスB:10億回/月のリクエストまで
まだまだ余裕あるので大丈夫そうですね。
Cloud Runはまだ数円しかかかっていないのでそこまで高くならなそうでよかったです。
節約するために
「適当にワークフロー回してたらこんなにお金かかってた😵」のような状態を避けるためにこまめにコストを確認することが必要です。
特にワークフローを実行するブランチを限定したり、R2オブジェクトのライフサイクルを設定し一定期間で削除することでオブジェクトのサイズを減らしたりなどの工夫をやるといいかも知れません。
終わりに
Webパフォーマンスの改善のためのLighthouseCI実装手順について説明しました。
特にligthouse-ciの公式ページにない実装だったので、皆さんの参考になれば嬉しいです!
当方諸々初心者なところがあるので間違っている部分や「もっとこうしたほうがいい!」みたいなところがあればコメントするかGitHubでPRを送っていただけると助かります!
そして今回もCloudflareは神ということを実感しました😊
みんなもLitestreamを使うときはCloud StorageやS3ではなくR2を使おう😊
参考
-
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" ↩︎
Discussion