Lighthouse CI + Datadogを使ってパフォーマンスを継続的に計測する

## パフォーマンス改善について連載しています！

QiitaでWebパフォーマンスの改善に取り組みはじめました!
そこで、どんな観点で分析をして、どう改善しているのかをシリーズとして投稿しています。

ただ、パフォーマンス改善と言っても、関連する領域が広く、とても1記事にまとめられないので、複数記事に分けて連載していくことにしました。

今回は、パフォーマンスの計測についての説明をしていきます。

よろしければ他の記事も読んでみてください!

### シリーズ内容（予定）

1. :rocket: [【Core Web Vitals】Webパフォーマンスを改善するなら、まずは重要な指標について理解しよう](https://qiita.com/kyntk/items/7ae8763961b62111889e)
2. :rocket: パフォーマンスの計測について 【←今回はこれ】
3. :rocket: 【番外編】[開発者ツールのLighthouseを使いこなせてますか？ - Qiita](https://qiita.com/kyntk/items/80a88f0894657a815cf8)
4. :rocket: [LCPの改善でLighthouseだけ見てない？効果的に改善していくためのステップ](https://qiita.com/kyntk/items/1f69a80476294713082b)
5. FIDを改善する
6. CLSを改善する
7. キャッシュについて

もし興味のある方は、この記事をストックしていただくか、[Twitter（@kyntk_1128）](https://twitter.com/kyntk_1128)をフォローしていただければ記事の更新ごとに通知しますので、ぜひよろしくお願いします！

## この記事で説明すること

さて、[前回の記事](https://qiita.com/kyntk/items/7ae8763961b62111889e)で、パフォーマンス改善において**指標を決めて計測し続けること**が大事だと説明しましたが、今回はその計測し続ける方法について説明します。

計測の方法は様々ありますが、フィールド（リアルユーザー）環境、ラボ環境という2つの環境を組み合わせて計測していきます。

QiitaではChrome UX Reportを使ったリアルユーザー環境データ収集と、Lighthouse CI + Datadogを使ったラボ環境データの収集をし始めました。

それぞれの説明や、具体的な設定方法について説明していきます。
タイトルにある、Lighthouse CI + Datadogの計測方法だけを知りたい方は、「[本題](#本題)」からご覧ください。

:::note
以降は https://web.dev/vitals/ などを参考にまとめています
:::

## リアルユーザー環境とラボ環境について

https://web.dev/vitals-field-measurement-best-practices/

パフォーマンス計測の方法は、フィールド（リアルユーザー）環境、ラボ環境という2つに分類できます。計測をする上で、メリット・デメリットやそれぞれ適していることが異なるので、特徴を知った上で組み合わせることが重要です。

### ラボ環境とは

パフォーマンスを計測するための環境（サーバーなど）を用意し、毎回同じ条件で計測を行う方法です。

パフォーマンスを計測する条件・スペックなどを自由に操作することができます。
また、機能の本番リリース前にテストをすることも可能です。

### リアルユーザー環境とは

ラボ環境では様々な条件を設定して計測することができますが、「パフォーマンスの体感」はユーザーごとに様々で、ラボ環境での計測結果が実際のユーザーの体験と乖離してしまう可能性もあります。

例えば、ユーザーが使っているデバイスのスペックや、ネットワーク状況などによって、同じサイトでも体験は異なります。

[ユーザーを中心としたパフォーマンス指標](https://web.dev/i18n/ja/user-centric-performance-metrics/)にあるように、「パフォーマンス」を計測するときは、点数ではなく、実際のユーザー体験に紐付いた計測結果を重視すべきです。

そのため、実際のユーザーがサイトにアクセスしたときのデータも用いて分析を行います。

### リアルユーザーデータはブレが大きい

先程、パフォーマンスはユーザーごとに様々と書きましたが、リアルユーザーデータはネットワークやデバイスによってスコアがばらけます。
また、データを計測しようとしても、欠損する可能性もありえます。

そのため、一定期間以上、多くのデータを集めて分析をする必要があり、ラボ環境と比較して判断するまでに時間がかかります。

### ラボ環境の特徴

一方ラボ環境は、毎回同じ状態で計測できる環境を用意することで、ネットワークやハードウェアのスペック、その他のリソースの競合といった、ブレにつながるものを除外することができます。
また、計測をするときに、一度に複数回計測をすることで、よりスコアのブレを減らすことができます。

一方で、アクセスしてくるユーザーとラボ環境のスペックなどに乖離が生まれていると、ラボ環境で計測しているのにユーザー体験の悪化に気づけないということもありえます。

その他にも、ログイン済みと未ログインの状態でパフォーマンスが変わるときなどに、網羅的な計測ができていない可能性もありますし、Webサイト上でのインタラクションに起因した体験の悪化を検知できない可能性もあります。

そのため、アクセスしてくるユーザーを知った上でラボ環境の計測条件を決めたり、リアルユーザー環境を組み合わせた複合的な分析をしていく必要があります。

### 両環境ともにスコアがブレる条件もある

ラボ環境はリアルユーザー環境と比較するとスコアのブレが少ないですが、依然としてスコアがブレる可能性もあります。

例えば、サイトがABテストをしていることで、計測のタイミングで表示される内容が変わったり、Webサーバーの負荷状況によってスコアが変わったりする可能性があります。

継続的に計測し続けて、外れ値などに左右されない分析をしていく必要があります。

スコアの変動性についてはLighthouseのドキュメントにまとまっています。

https://github.com/GoogleChrome/lighthouse/blob/master/docs/variability.md

## 手軽に計測をし始める① リアルユーザーモニタリング

これからは具体的に、リアルユーザー環境とラボ環境を計測し続ける手軽な方法について説明します。

まずリアルユーザー環境についてですが、[WebVitalsの測定を開始する](https://web.dev/vitals-measurement-getting-started/) には、PageSpeed InsightsやSearch Console、CrUX dashboardなどが乗っています。

これらはどれも[Chrome User Experience Report](https://developers.google.com/web/tools/chrome-user-experience-report)という、Chromeユーザーの統計データを使用しています。

他にもJavaScriptを使ってカスタムメトリクスを計測することもできますが、手軽に始めるということで、[CrUX Dashboard on Data Studio](https://web.dev/chrome-ux-report-data-studio-dashboard/)を使うことにしました。

### Chrome UXレポートを使ったダッシュボード

Chrome UXレポートの結果をData Studioを使って可視化し、ダッシュボードを手軽に作成することができます。

https://web.dev/chrome-ux-report-data-studio-dashboard/

以下のようなデータが見れます。（画像は[web.dev](https://web.dev/chrome-ux-report-data-studio-dashboard/)から引用）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/979e68e2-5bfb-3b49-6e6c-5b51fefd3f39.png)

設定方法は、[web.dev](https://web.dev/chrome-ux-report-data-studio-dashboard/)にありますが、Data StudioのCommunity Connectorsを使用します。

その後Data Studio上でダッシュボードを自由にカスタマイズすることができます。

これを使うと、数分で、サイトのCore Web Vitalsやその他の指標を可視化することができます。

:::note warn
注意点としては、メトリクスは月次で集計されて翌月第2火曜日に公開されるので、結果を分析するのに最長1ヶ月程度かかります。
:::

# 本題

タイトルにある、Lighthouse CI + Datadogの監視設定はここから説明します。

## 手軽に計測をし始める② ラボ環境

Chrome UXレポートは便利ですが、高速なPDCAを回す時などはラボ環境のデータのほうが適しています。

ラボ環境でパフォーマンスを計測し続けるにはLighthouse CIが便利です。

Qiitaでは普段からDatadogを使用しているので、Lighthouse CIで取得したメトリクスをDatadogへ送信することで、Datadogのダッシュボードを用いてパフォーマンスを確認することができます。

### Lighthouse CI

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

Lighthouse CIは、GitHub Actionsなどを用いて、個別でサーバー構築をすることなしに、Lighthouseの実行と監視ができるツールです。

QiitaではGitHub Actionsを用いて定期実行をしています。

実行設定は各種CIによるのですが、Pull Requestごとに実行したり、cronで定期実行したり自由に計測することができます。

また、Lighthouseの実行設定をチューニングすれば、計測するときのスペックなども制御することができます。

:::note warn
パブリックストレージでの実行結果の保存期間は3日から5週間程度のみです
:::

Lighthouse CIを使うと実行結果をパブリックストレージに保持することができるのですが、一定期間後に消えてしまいます。
そのため、過去データを分析に使いたい場合は、[Lighthouse CI Server](https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/server.md)などを別途用意する必要があります。

Qiitaでは、Lighthouseの実行結果をDatadogに送信して、Datadogで分析をしています。

ここからはメトリクスを取得するための設定を説明していきます。

#### Lighthouse CIの設定をする

GitHub Actionsを使ってLighthouse CIの設定をします。

[lighthouse-ci-action](https://github.com/treosh/lighthouse-ci-action) を使って以下のように設定するとGitHub ActionsでLighthouseを実行することができます。

```yml:.github/workflows/lighthouse.yml
name: Lighthouse CI
on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v3
      - name: Audit URLs using Lighthouse
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://qiita.com
```

その他に`temporaryPublicStorage`を有効化することで、先程のパブリックストレージへ結果のアップロードもしてくれます。

#### Lighthouseの実行結果からメトリクスを取得する

次にDatadogにメトリクスを送信したいので、Lighthouseの実行結果から必要なメトリクスを取得します。

先程の設定をすると、`.lighthouseci`ディレクトリに、Lighthouse Result Object （LHR）がJSONで出力されています。
LHRについては以下のドキュメントに説明されています。

https://github.com/GoogleChrome/lighthouse/blob/master/docs/understanding-results.md

このJSONをパースし、必要なデータを取得します。

```ts:main.ts
import fs from 'fs'
import path from 'path'
import { getScoreData } from './lib/getScoreData'

// Output directory of lighthouse ci result
const directoryPath = path.join(__dirname, '.lighthouseci')
const jsonFileRegExp = /lhr.*\.json/

const main = () => {
  fs.readdir(directoryPath, (err, files) => {
    if (err) {
      console.error(err)
      process.exitCode = 1
      return
    }

    files
      .filter((file) => jsonFileRegExp.test(file))
      .map((file) => {
        const data = JSON.parse(
          fs.readFileSync(path.join(directoryPath, file), 'utf-8')
        )
        const metrics = getScoreData(data)
        // Datadogにメトリクスを送信
      })
  })
}

main()
```

JSONから必要なメトリクスを取得する処理は以下のように実装しています。

```ts:lib/getScoreData.ts
type Metrics = {
  'lighthouse.fcp': number
  'lighthouse.lcp': number
  'lighthouse.si': number
  'lighthouse.tti': number
  'lighthouse.tbt': number
  'lighthouse.cls': number
  'lighthouse.fid': number
  'lighthouse.initialResponse': number
}

export const getScoreData = (data: any): Metrics => {
  const {
    'first-contentful-paint': fcp,
    'largest-contentful-paint': lcp,
    'speed-index': si,
    interactive: tti,
    'total-blocking-time': tbt,
    'cumulative-layout-shift': cls,
    'max-potential-fid': fid,
    'server-response-time': initialResponse,
  } = data.audits

  return {
    'lighthouse.fcp': Math.round(fcp.numericValue),
    'lighthouse.lcp': Math.round(lcp.numericValue),
    'lighthouse.si': Math.round(si.numericValue),
    'lighthouse.tti': Math.round(tti.numericValue),
    'lighthouse.tbt': Math.round(tbt.numericValue),
    'lighthouse.cls': Math.round(cls.numericValue),
    'lighthouse.fid': Math.round(fid.numericValue),
    'lighthouse.initialResponse': Math.round(initialResponse.numericValue),
  }
}
```

#### Datadogにメトリクスを送信する

取得した結果をDatadogに送信するためにmain.tsを書き換えます。

```diff_typescript:main.ts
import fs from 'fs'
import path from 'path'
import { getScoreData } from './lib/getScoreData'
+import { DDClient } from './lib/ddClient'
+
+interface Metrics {
+  [metric: string]: number
+}
+
+interface APIClient {
+  sendMetrics: (metrics: Metrics) => void
+}

const directoryPath = path.join(__dirname, '.lighthouseci')
const jsonFileRegExp = /lhr.*\.json/

-const main = () => {
+const main = (client: APIClient) => {
  fs.readdir(directoryPath, (err, files) => {
    if (err) {
      console.error(err)
      process.exitCode = 1
      return
    }

    files
      .filter((file) => jsonFileRegExp.test(file))
      .map((file) => {
        const data = JSON.parse(
          fs.readFileSync(path.join(directoryPath, file), 'utf-8')
        )
        const metrics = getScoreData(data)
+        client.sendMetrics(metrics)
      })
  })
}

+const [_0, _1, ...option] = process.argv
+const dryRun = option.includes('-d')
+const client = new DDClient(dryRun)

-main()
+main(client)
```

そしてDatadogにメトリクスを送信します。
メトリクス送信部分の実装はこちらの記事を参考に実装しました。

https://zenn.dev/ryo_kawamata/articles/webperf-watcher

実装したnode version は18です。

```ts:lib/ddClient.ts
interface Metrics {
  [metric: string]: number
}

export class DDClient {
  private apiUrl: string
  private dryRun: boolean

  constructor(dryRun: boolean) {
    this.dryRun = dryRun
    this.apiUrl = this.getApiUrl()
  }

  async sendMetrics(data: Metrics) {
    // https://docs.datadoghq.com/ja/api/latest/metrics/#submit-metrics
    const requestBody = JSON.stringify({
      series: Object.entries(data).map(([metric, value]) => ({
        metric,
        points: [[`${Math.floor(Date.now() / 1000)}`, value]],
        tags: ['source:lighthouse_ci'],
        type: 'gauge',
      })),
    })

    this.post(requestBody)
  }

  private getApiUrl() {
    const apiKey = process.env.DD_API_KEY
    if (apiKey === undefined && !this.dryRun) {
      throw new Error('env: DD_API_KEY is not set.')
    }
    return `https://api.datadoghq.com/api/v1/series?api_key=${apiKey}`
  }

  private async post(body: string) {
    const func = this.dryRun ? console.log : fetch
    return func(this.apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body,
    })
  }
}
```

そしてGitHub Actionsで実行するようにします。

```diff_yaml:.github/workflows/lighthouse.yml
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: '18'
+          cache: 'yarn'
      - name: Audit URLs using Lighthouse
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://qiita.com
          temporaryPublicStorage: true
+      - run: yarn install
+      - run: yarn esbuild main.ts --bundle --outfile=main.js --platform=node --minify
+      - name: Send metrics to Datadog
+        run: node main.js
+        env:
+          DD_API_KEY: ${{ secrets.DD_API_KEY }}
```

これでLighthouseの監視結果をDatadogで確認することができます。

## Performance budgets

[Performance budgetsとは](https://web.dev/performance-budgets-101/)各種メトリクスに設定する上限値です。

Lighthouseを使えば、ある閾値を超えたときに検知して通知をすることもできます。
設定方法は以下の記事を参考に、`budget.json`を設定し、読み込みます。

https://web.dev/use-lighthouse-for-performance-budgets/

Qiitaでは、まだ設定しておらず、パフォーマンスを一定改善した後に、悪化していないかを検知するために設定しようと考えています。

## まとめ

今回は、パフォーマンス改善において重要な、**指標を決めて計測し続ける**ために、その計測方法について説明しました。

これらを計測して、課題がありそうなポイントの当たりをつけたり、定期的なパフォーマンスの監視に活用していきます。

ただ、今回設定した方法では、具体的な解決策につながる詳細な分析は不十分です。
そのため、今後はボトルネックの見つける方法などについて、より詳しく説明していこうと思います。

このシリーズはまだまだ続く予定ですので、LGTMとストック、[Twitter（@kyntk_1128）](https://twitter.com/kyntk_1128)での続報をお待ち下さい!

## 参考

- [User-centric performance metrics](https://web.dev/i18n/en/user-centric-performance-metrics/)
- [Getting started with measuring Web Vitals](https://web.dev/vitals-measurement-getting-started/)
- [lighthouse/variability.md at master · GoogleChrome/lighthouse](https://github.com/GoogleChrome/lighthouse/blob/master/docs/variability.md)
- [Chrome User Experience Report  |  Chrome UX Report  |  Google Developers](https://developers.google.com/web/tools/chrome-user-experience-report)
- [Using the CrUX Dashboard on Data Studio](https://web.dev/chrome-ux-report-data-studio-dashboard/)
- [treosh/lighthouse-ci-action: Audit URLs using Lighthouse and test performance with Lighthouse CI.](https://github.com/treosh/lighthouse-ci-action)
- [lighthouse/understanding-results.md at master · GoogleChrome/lighthouse](https://github.com/GoogleChrome/lighthouse/blob/master/docs/understanding-results.md)
- [Puppeteer +Lighthouse +GitHubActionsで認証付きWebアプリのWebperfを定期計測](https://zenn.dev/ryo_kawamata/articles/webperf-watcher)