このブログサイトはNetlifyにホストしてるのですが、Deploy Previewsという機能を使ってCI回すようにしてみました。

Deploy Previewsとは

Netlifyのソース元としてGitHubと連携させておくと、GitHub上でプルリクエストを作った時にそのフィーチャーブランチをNetlify上にプレビュー環境として自動的にデプロイしてくれる仕組みのことです。これを使って本番にデプロイする前にNetlify上の環境で変更を確認できるというやつですね。こちらのNetlifyブログで紹介されています。

Netlifyはこういう仕組みがあるのが便利ですよね。

プレビュー環境へのCI

せっかく簡単にプレビュー環境を作ってくれるので、その環境に対して継続的にパフォーマンステストを行うようにしてみます。今回使ったのはsitespeed.ioです。sitespeed.ioはウェブサイトのパフォーマンスを計測したり、アクセシビリティの観点からの評価、ベストプラクティスに従っているかどうかなどを得点化してくれるツールです。実際にこのブログサイトに対して実行すると、下画像のようなレポートをHTMLで出力してくれます。

sitespeed.ioの実行結果

sitespeed.ioの実行結果

このようにHTMLとしてレポートを出力してくれるのは、人間が結果の詳細を見るときには良いのですが、CIに組み込もうとすると扱いづらいですね。

sitespeed.ioはCIで使うケースに対応するために、パフォーマンスバジェットを下回ったら実行失敗とすることが出来ます。

パフォーマンスバジェットとは

パフォーマンスバジェットについてはGoogle Developersブログの説明がわかりやすかったので引用します。

自らのウェブサイトに設けた、パフォーマンスに関わるさまざまなメトリクスの上限をパフォーマンスバジェットと呼びます。JavaScript ファイルやページ全体のファイルサイズ、ローディングまでにかかる時間、Lighthouse のスコアとメトリクスは多岐にわたりますが、UX 上重要であるものや、運用のしやすいものをバジェットとして定義するとよいでしょう。

ここで説明されているように、例えばサイトを読み込んだ時の転送サイズは最大でも400KB以内とするであるとか、First Paintまでにかかる時間を500msにすると言ったように、メトリクスの上限を設定し、その上限の範囲内かどうかを継続的にチェックする考え方です。

sitespeed.ioでは、各種メトリクスの上限を下のようなjsonファイルに書いておくと、このバジェットを満たしているかどうかをチェックしてくれる --budget.configPath というオプションがあります。これを使うとCIに組み込むのが簡単になります。

{
  "budget": {
    "requests": {
        "total": 100
    },
    "transferSize": {
        "total": 400000
    },
    "thirdParty": {
        "requests": 6
    },
    "score": {
      "accessibility": 100,
      "bestpractice": 69,
      "privacy": 73,
      "performance": 92
    }
  }
}

CIプロセスの実装

フィーチャーブランチへのコミットをトリガーにしてプレビュー環境を作るのは、NetlifyのDeploy Previewsを有効化すれば自動でやってくれます。今回やる必要があるのはプレビュー環境がデプロイされたというイベントをトリガーにしてsitespeed.ioを起動するという所です。

そして、幸いなことにsitespeed.ioはGitHub Actions用のコンテナを用意してくれているので、GitHub Actionsを使ってCIを実装してみます。

まず、Netlifyのプレビュー環境のURLを取得する必要があります。このURLは、NetlifyがGitHub側のコミットステータスを変更する時のイベントに入っているので、そこから取得することが出来ます。そして、そのURLに対してパフォーマンスバジェットファイルを指定してsitespeed.ioを実行するという流れになります。

というわけで、こんなワークフローになりました。

workflow "Sitespeed.io" {
  on = "status"
  resolves = ["Run sitespeed.io"]
}

action "Filter job" {
  uses = "yuichielectric/bin/filter@master"
  args = "state success"
}

action "Extract staging URL" {
  uses = "yuichielectric/export-action@master"
  needs = ["Filter job"]
  args = ".target_url /github/workspace/url"
}

action "Run sitespeed.io" {
  uses = "docker://sitespeedio/sitespeed.io:8.8.3-action"
  args = "/github/workspace/url -n 1 --budget.configPath /github/workspace/.github/budget.json"
  needs = ["Extract staging URL"]
}

まず、コミットステータスが変更される際には status というイベントが発火するので、そのイベントをトリガーとしてワークフローを起動します。

ただ、この status イベントはジョブが成功したときだけではなくて実行中ステータスになったときにも発火します。なので、まず最初のFilter jobアクションで、イベントのPayload中の state という値が success のときだけ処理を進めるようにフィルタリングしています。ちなみに今回使っている status イベントのPayloadの中身はこちらにドキュメントがあります。

次にプレビュー環境のURLを取得するための Extract staging URL アクションを実行します。プレビュー環境のURLは前述の通り、Payloadの中で targer_url という名前で渡されてきます。このアクションはイベントのPayloadのJSONファイルに対して、argsで1つ目の引数として指定されている .target_url を使って jq .target_url を実行し、その結果を2つ目の引数で指定した /github/workspae/url というファイルに書き込むという処理を行います。

そして、最後の Run sitespeed.io アクションで、実際にsitespeed.ioを実行します。sitespeed.ioは対象のサイトのURLが書かれたファイルをそのまま渡すことができるので、 args では Extract staging URLアクションで生成したファイルのパスをそのまま指定しています。それに加えて、バジェットファイルを指定しています。このバジェットファイルは、GitHubリポジトリに.github/budget.jsonとしておいておけば、GitHub Actionsが自動的に/github/workspace下にマウントしてくれます。

今後

今回の仕組みでパフォーマンスバジェットを満たしていない場合には失敗ステータスにさせるという所は出来たのですが、sitespeed.ioのログにはどのメトリクスによって失敗したのかのみが出力されるため、ログだけからでは失敗した詳細な原因を把握することが出来ません。sitespeed.ioのHTMLレポートファイルを見ればもう少し詳細がわかりますが、GoogleのChromeチームが開発しているLighthouseというツールだと下画像のように具体的にどの項目が失敗していて、どのように直すとよいのか書かれている記事へのリンクも表示してくれたりするので、ここまでやれると失敗した時の原因が簡単にわかって良いなと思います。こういった形にできないものか、もう少し考えてみたいと思います。

Lighthouseの実行結果

Lighthouseの実行結果

まとめ

ウェブサイトに対して継続的パフォーマンステストを行う仕組みを、Netlify + sitespeed.io + GitHub Actionsを使って作ってみました。budget.jsonさえ用意すれば他のプロジェクトでも使い回せる形なので、是非使ってみて下さい。このブログに対してだと2分弱で実行が終わります。

また、Netlify以外のサービスやツールを使ってプレビュー環境を作っている場合も、同じ仕組みで使い回せるはず(プレビュー環境のURLの取得の仕方は変更の必要があるかもしれません)。