📉

AutoWebPerf を使って CoreWebVitals やページパフォーマンスの悪化を検知してみる

2021/07/27に公開

AutoWebPerf とは?

AutoWebPerf (AWP) は Web ページのパフォーマンスデータを Chrome UX Report(CrUX)・PageSpeed Insights(PSI)・WebPageTest から集め、JSON やスプレッドシート等に集約してくれる便利なツールです

web.dev でアーキテクチャが紹介されています [1]

AWP アーキテクチャ1

Gatherers → AWP Engine → Connectors という流れに見えますが、実際はテスト対象のページのリストも Connectors を経由して AWP Engine にインプットできるため下の図のほうが分かりやすいかもしれません

AWP アーキテクチャ2

パフォーマンステスト対象のページリストをスプレッドシートから取得 → リスト内のページの CrUX パフォーマンスデータを取得 → 結果をスプレッドシートに書き出す、なんてことができたりします

パフォーマンスの悪化を検知しよう

今回はこの AutoWebPerf を使ってパフォーマンスの悪化を検知するリグレッションテストのような仕組みを作ってみたいと思います

どうやるか

パフォーマンステスト対象のページ群に対してパフォーマンスデータを取得し、前回の値と比較、閾値を超えていれば Slack に通知するなどの任意のアクションを実行することでデグレーションに気付けるようにします

なるべく安定していて再現性のあるパフォーマンスデータを使いたいので、PSI のラボデータを使います(と言ってもそこそこブレます)

PSI のラボデータ・フィールドデータの違いについてはこちらが参考になります
ざっくり言うと

https://developers.google.com/speed/docs/insights/v5/about?hl=ja

  • ラボデータ: 統一された環境でのシミュレーションによって得られたデータ
  • フィールドデータ: 実際のユーザー達が体験したスコアをまとめたデータ

といった感じです

基準を決める

悪化を検知するため、監視する指標とその閾値を決めます

https://github.com/GoogleChromeLabs/AutoWebPerf/blob/stable/src/gatherers/psi.js#L51-L81

AWP のソースコード中に登場する lighthouse. で始まる指標が PSI ラボデータで得られる値です
ここから監視したい指標を選びます。今回は SpeedIndex と CoreWebVitals の1つである LargestContentfulPaint を監視してみようと思います

閾値についてはサイトの特性や指標をどれくらい重視するか等によって色々決め方はあると思いますが、この記事では「前回から 50% 以上悪化したら検知する」というのをやってみます
ラボデータは統一された環境でのシミュレーションですが、とは言えそこそこブレるので遊びをもたせた閾値を設定するのが良いと思います

今回の条件をまとめるとこんな感じです

  • 監視する指標: SpeedIndex, LargestContentfulPaint
  • 検知する条件: 50%以上悪化した場合

対象のページを決める

次にテスト対象のページを決めます
サイト内のページ数が少ない場合は全ページを対象にしても良いと思いますが、ページ数的に現実的ではない場合はページの種類ごとに代表的なページを数ページずつ抽出して計測するのが良いと思います

例えば EC サイトなら

  • 商品一覧ページ 1つ
  • 商品個別ページ 3つ
  • 検索ページ 1つ
  • etc.

のような感じでしょうか

これらのページリストを AWP が読み込める形で用意します
JSON 等も選べますが今回はスプレッドシートにまとめる方法でやろうと思います

AWP 側でスプレッドシートの例を用意してくれているのでこちらを参考に対象ページをまとめます
https://docs.google.com/spreadsheets/d/1c3k9eEVg12Atoa72tglVVBg--o0CEMkOF3JCaLlxzeo/edit#gid=1079019487

以下の3つのシートを作ります

シート名 説明
Tests ページリストをまとめたシート。AWP のサンプルの Tests-PSI を参照
Results 測定結果が書き込まれるシート。AWP のサンプルの Results-PSI を参照
Previous 後々使うシート。パフォーマンスの差分を検知するため前回のスコアを書き込んでいく。空で OK

「Tests」シートは以下のようにしてみました。label の名前はなんでも良いですが一意になるようにしてください

スプレッドシート例

PSI の API キーを取得する

AWP から PSI のデータを取得するために API キーを取得しておきます
API キーが無くても実行はできますが、キー無しだとすぐに 429 レスポンスが返ってきて Quota exceeded となってしまうため API キーを取得しておきましょう

https://developers.google.com/speed/docs/insights/v5/get-started

パフォーマンスの悪化を検知する

準備ができたのでデグレーションを検知するコードを書いていきたいと思います
コードについてはこちらの記事で丁寧に解説されているので細かい実装内容についてはこちらを参考にしてみてください
https://zenn.dev/tiwu_dev/articles/1738fd34f7fd5a

処理としては

  1. AWP を使ってパフォーマンスデータを集める
  2. スプレッドシートの Previous シートに書き込んでおいた前回データと比較する
  3. 閾値を超える悪化が見られた場合は通知する
  4. 今回の値を Previous シートに書き込む

という流れで行っていきます

では実装例です

const AutoWebPerf = require('awp/src/awp-core');
const { GoogleSpreadsheet } = require('google-spreadsheet');

// サービスアカウントの JSON キーがあるパス
const serviceAccount = './service-account.json';

// PSI の API キー
const psiApiKey = 'xxxxx';

// スプレッドシートの ID
// https://docs.google.com/spreadsheets/d/xxxxx/edit の xxxxx の部分
const sheetId = 'xxxxx';

// 監視する指標
const targetMetrics = ['SpeedIndex', 'LargestContentfulPaint'];

// 前回の値を書き込んでおくシート名
const sheetTitle = 'Previous';

// 閾値。今回は 50% 以上の悪化を検知したいので 0.5 とする
const threshold = 0.5;

async function main() {
  // PSI スコアを取得する
  const awp = new AutoWebPerf({
    tests: {
      connector: 'sheets',
      path: `${sheetId}/Tests`,
    },
    results: {
      connector: 'sheets',
      path: `${sheetId}/Results`,
    },
    envVars: {
      SERVICE_ACCOUNT_CREDENTIALS: serviceAccount,
      PSI_APIKEY: psiApiKey,
    },
  });
  const awpResult = await awp.run();

  // 対象となるメトリクスを収集する
  // currentMetrics に以下のようなデータを詰める
  // {
  //   'ラベル1': {
  //     SpeedIndex: 100,
  //     LargestContentfulPaint: 100
  //   },
  //   'ラベル2': {
  //     SpeedIndex: 100,
  //     LargestContentfulPaint: 100
  //   }
  // }
  const currentMetrics = {};
  awpResult.results.forEach((r) => {
    const filteredMetrics = {};
    const metrics = r.psi.metrics.lighthouse;
    targetMetrics.forEach((m) => {
      filteredMetrics[m] = metrics[m];
    });

    const label = r.label;
    currentMetrics[label] = filteredMetrics;
  });

  // 前回のデータを Previous シートから取得
  const doc = new GoogleSpreadsheet(sheetId);
  await doc.useServiceAccountAuth(require(serviceAccount));
  await doc.loadInfo();

  const sheet = doc.sheetsByTitle[sheetTitle];

  // previousMetrics は以下のようなデータになる
  // [
  //   {
  //     label: 'ラベル1',
  //     SpeedIndex: '200',
  //     LargestContentfulPaint: '200'
  //   },
  //   {
  //     label: 'ラベル2',
  //     SpeedIndex: '200',
  //     LargestContentfulPaint: '200'
  //   }
  // ]
  const previousMetrics = await getPreviousMetrics(sheet);

  // 比較する

  if (previousMetrics) {
    previousMetrics.forEach((previousMetric) => {
      const label = previousMetric.label;
      const currentMetric = currentMetrics[label];
      if (!currentMetric) {
        return;
      }

      targetMetrics.forEach((targetMetric) => {
        // スプレッドシートから取得した値は文字列となるため parseInt する
        const previousScore = parseInt(previousMetric[targetMetric]); 
        const currentScore = currentMetric[targetMetric];

        if (currentScore / previousScore > 1 + threshold) {
          // 閾値を超えた場合に Slack に通知する等のアクションをする
	  // 今回の例では単にログを出力する
          console.log(`${label}${targetMetric} が悪化しています! ${previousScore} -> ${currentScore}`);
        }
      });
    });
  }

  // 今回の値を Previous シートに書き込む
  const rowsToAdd = Object.keys(currentMetrics).map((label) => {
    return { label, ...currentMetrics[label] };
  });

  await writeCurrentMetrics(sheet, ['label', ...targetMetrics], rowsToAdd);
}

// 前回の値をシートから取得する関数
async function getPreviousMetrics(sheet) {
  try {
    await sheet.loadHeaderRow();

    const headers = sheet.headerValues;
    const rows = await sheet.getRows();
    const output = [];

    rows.forEach((row) => {
      const obj = headers.reduce((acc, header) => {
        return {
          ...acc,
          [header]: row[header],
        };
      }, {});
      output.push(obj);
    });

    return output;
  } catch {
    // ヘッダーが存在しない空シート
    return null;
  }
}

// 今回の値をシートに書き込む関数
async function writeCurrentMetrics(sheet, headers, metrics) {
  await sheet.clear();
  await sheet.setHeaderRow(headers);
  await sheet.addRows(metrics);
}

main();

実行結果

今回は閾値を超えた場合にコンソールに出力するようにしました
閾値を超えたデグレーションが発生した場合以下のようなログが出ます

❗ 商品一覧ページ の SpeedIndex が悪化しています! 700 -> 1403

出力先を Slack にしたりするしたりすることでデグレーションに気付きやすい仕組みを作ることができます

おわりに

AWP を使ったパフォーマンスの悪化を検知する方法を紹介しました
上で紹介したコードを定期実行するようにすることで監視の仕組みを実現できます
こちらの記事では Firebase Functions を使った例が紹介されています

https://zenn.dev/tiwu_dev/articles/1738fd34f7fd5a#firebase-functions-で定期的なテスト

今回の仕組みを発展させることで、原因となったコミットの特定なんかもできそうです
記事中では過去1回分の結果と比較していますが、例えば過去5回分の平均と比較するなどした方が数値のブレに強くなりそうですね

参考

脚注
  1. Automating audits with AutoWebPerf https://web.dev/autowebperf/#architecture-overview ↩︎

Discussion