PageSpeed Insightsの点数はどのように計算されているか。100点をとるための条件

[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/?hl=ja)の採点ロジックについて詳しく調べてみたところ、意外なことがわかりました。

# 2020/6/3 追記

本家レポートより**PageSpeed Insightsのスコア改善が捗る裏レポートツール**を公開しました。合わせてご参考ください。
https://simulate.site/cheatspeed-insights/

# 2020/3/19 追記

Lighthouse 6へのバージョンアップでこのページにかかれている内容が一部変更される可能性があります。
こちらも併せてご参考ください。

[PageSpeed Insightsのスコア大変動か？ Lighthouse 6 で変わる採点ルールを先取りチェック！ \- アイデアマンズブログ](https://blog.ideamans.com/2020/03/pagespeed-lighthouse-6-draft.html)

# 点数に反映される監査項目は5つだけ

PageSpeed Insightsを実行するといろいろな指摘事項が出てきます。なので**指摘が多いと点数が下がる減点方式と思われがち**です(私もそう思っていました)。

実は、**PageSpeed Insightsの点数はたった5つの監査スコアの加重平均で計算されています**。

* `Interactive` インタラクティブになるまでの時間(重み 5)
* `Speed Index` 速度インデックス (重み 4)
* `First Contentful Paint` コンテンツの初回ペイント (重み 3)
* `First CPU Idle` CPUの初回アイドル (重み 2)
* `First Meaningful Paint` 意味のあるコンテンツの初回イベント (重み 1)

指摘項目はあくまで上記の指標を改善させるためのアドバイスです。**指摘をクリアしても直接点数が上がるわけではありません**。

逆にどんな手を使おうと上記の指標を改善すればPageSpeed Insightsの点数が上がります。**技術的に頑張るよりコンテンツを見直す方が効率よく点数を上げられる**場合が多いと思います。

# PageSpeed Insightsで満点をとる方法

もうひとつ、100点を取るための具体的な条件を特定しました。

以下のすべて条件を満たせば、PageSpeed Insightsの点数が必ず100点になります(Lighthouse 4.3.0時点)。

* `Interactive` インタラクティブになるまでの時間 `1900ms以下`
* `Speed Index` 速度インデックス `1950ms以下`
* `First Contentful Paint` コンテンツの初回ペイント `1350ms以下`
* `First CPU Idle` CPUの初回アイドル `1950ms以下`
* `First Meaningful Paint` 意味のあるコンテンツの初回イベント `1350ms以下`

3Gネットワーク相当の通信環境での条件なので容易ではありません。

# 採点ロジック

PageSpeed Insightsは内部で[Lighthouse](https://github.com/GoogleChrome/lighthouse)を利用しています。

LighthouseはGoogle Chromeチームが主導するオープンソースのWebページ監査ツールです。`Lighthouseのパフォーマンスカテゴリスコア = PageSpeed Insightsの点数`です。

Lighthouseのスコア計算を行っている部分を追ってみましょう。

## カテゴリスコアは監査スコアの加重平均

パフォーマンススコア(PageSpeed Insightsの点数)のようなカテゴリスコアの計算を行っているのが[scoring.js](https://github.com/GoogleChrome/lighthouse/blob/6a0d22ebe620dae1758910196c62c8f7e42310ef/lighthouse-core/scoring.js)です。

L75付近で監査項目のスコア`score`を、重み`weight`を加味して加重平均しています。

```javascript
      const scores = auditRefs.map(auditRef => ({
        score: resultsByAuditId[auditRef.id].score,
        weight: auditRef.weight,
      }));
      const score = ReportScoring.arithmeticMean(scores);
```

## 監査項目は5つ以外 weight=0

加重平均なので重み`weight`が0だと、その監査項目のスコアがどんなに高くても全体の点数にはまったく反映されません。

PageSpeed InsightsをAPI経由で実行するとJSONデータが得られます。上記の計算に使われている`lighthouseResult.categories.performance.auditRefs`を見ると、`weight`が0以外なのは5項目だけです。

```json
        "auditRefs": [
          {
            "id": "first-contentful-paint",
            "weight": 3,
            "group": "metrics"
          },
          {
            "id": "first-meaningful-paint",
            "weight": 1,
            "group": "metrics"
          },
          {
            "id": "speed-index",
            "weight": 4,
            "group": "metrics"
          },
          {
            "id": "interactive",
            "weight": 5,
            "group": "metrics"
          },
          {
            "id": "first-cpu-idle",
            "weight": 2,
            "group": "metrics"
          },
          {
            "id": "estimated-input-latency",
            "weight": 0,
            "group": "metrics"
          },
// ...略(以下のweightはすべて0)...
        ]
```

つまり、`first-contentful-paint`、`first-meaningful-paint`、`speed-index`、`interactive`、`first-cpu-idle`以外の監査スコアはいくら改善してもそれ自体では全体の点数は上がらないということです。

## 監査項目ごとの点数はどう計算されているか

`interactive`などの監査項目はいずれもタイミングに関する指標で、単位はミリ秒(ms)です(SpeedIndexは諸説ありますが、ここではms単位として扱われています)。

例えば重みが最も大きい`interactive`は[interactive.js](https://github.com/GoogleChrome/lighthouse/blob/95101fc5a908a84658749daf77c06d7c8d638caa/lighthouse-core/audits/metrics/interactive.js)において`Audit.computeLogNormalScore`メソッドでスコアリングされています。

```javascript
    return {
      score: Audit.computeLogNormalScore(
        timeInMs,
        context.options.scorePODR,
        context.options.scoreMedian
      ),
      rawValue: timeInMs,
      displayValue: str_(i18n.UIStrings.seconds, {timeInMs}),
      extendedInfo: {
        value: extendedInfo,
      },
    };
```

`Audit.computeLogNormalScore`を追っていくと、[audit.js](https://github.com/GoogleChrome/lighthouse/blob/f2bd3b6d473824a4e4c05a8a0844a0f3ecf119dc/lighthouse-core/audits/audit.js#L80)や、[statistics.js](https://github.com/GoogleChrome/lighthouse/blob/38831b7ff29863d9b93ab58f3dfba68545df6745/lighthouse-core/lib/statistics.js)で対数正規分布上のパーセンタイルでスコア計算をしているようです。

**2019/4/24追記** 難しくて執筆当時は端折ってしまいましたが、あとで調べたところ[対数正規分布](https://ja.wikipedia.org/wiki/%E5%AF%BE%E6%95%B0%E6%AD%A3%E8%A6%8F%E5%88%86%E5%B8%83)の分布関数が実装されていました。パフォーマンス指標は対数正規分布になるとみなし、累積分布関数である値が上位何%に位置するか(要は100人中の順位)求めます。1に対する補数がスコアになるという計算です。

逆算して満点を取る条件を見つけようとしたのですが断念…次のプログラムで50ms単位の閾値を探しました。それが冒頭の100点を取る条件です。

```javascript
// yarn add lighthouse tsv
const Interactive = require('lighthouse/lighthouse-core/audits/metrics/interactive'),
  SpeedIndex = require('lighthouse/lighthouse-core/audits/metrics/speed-index'),
  FirstContentfulPaint = require('lighthouse/lighthouse-core/audits/metrics/first-contentful-paint'),
  FirstCPUIdle = require('lighthouse/lighthouse-core/audits/metrics/first-cpu-idle'),
  FirstMeaningfulPaint = require('lighthouse/lighthouse-core/audits/metrics/first-meaningful-paint'),
  Audit = require('lighthouse/lighthouse-core/audits/audit'),
  Tsv = require('tsv')

const classes = [Interactive, SpeedIndex, FirstContentfulPaint, FirstCPUIdle, FirstMeaningfulPaint]

const rows = []
for (let ms = 0; ms < 30000; ms += 50) {
  const row = { ms }
  for (let cls of classes) {
    const opts = cls.defaultOptions
    row[cls.name] = Audit.computeLogNormalScore(
      ms,
      opts.scorePODR,
      opts.scoreMedian,
    )
  }
  rows.push(row)
}

process.stdout.write(Tsv.stringify(rows))
```

## 300サイトのデータで検算

以前、[通販上位300サイトについてPageSpeed Insightsを実行した](https://blog.ideamans.com/2019/04/ec-pagespeed-insights-deviations.html)データがあります。

このデータで検算したところ、`5項目のスコアの加重平均 = 全体の点数`で完全に一致しました。ここまでの説明で採点ロジックはほぼ間違いないと確認しました。

以上です。参考になれば幸いです。