Hello Tech

AutoReserve 等を開発する株式会社ハローのテックブログです。スタートアップの最前線から本質的な価値を届けるための技術を紹介します。

Web高速化1「メンバーを巻き込み、分析基盤を整える」

2023年もあと少しです。スパッと区切りをつけるよりも2024年にバトンを渡すような過ごし方をしたいなと思い、この記事を書き始めました。

2023年はAutoReserveにとって飛躍の年でした。海外レストランの予約開始をはじめとする多くの機能をリリースし、プレスリリースも多く出し、ユーザー数の大台を突破しました。

また飲食店向けに提供しているAutoReserve for Restaurantも今年大きな成長を遂げました。今年だけで30以上もの飲食店さまにご協力いただき導入事例を掲載させていただきました。

しかしその代償もありました。多くの機能をリリースし大きな成長を遂げた筋肉痛として「ページスピードの遅さ」が露見しました。さまざまな機能をスピーディにリリースしながらページスピードを高い水準で維持するのは至難の業です。

夏頃にオーガニック流入を増やすためにSEO周りの改善を始めたところ、ページスピードの遅さが原因の一つであることに気がつきました。Core Web VitalsがSEOの評価に影響し始めたからです(2021年5月より)。

「Webのページスピードを上げよう!」というWeb界隈の流れは2017年くらいから始まったと記憶しています。

俳優 阿部寛のホームページや、デベロッパーフォーラムのdev.toというサイトがいかに速いかを説明するのにPage Speed Insightsが使われて注目されました。

当時はデベロッパーならではの話題であったのが、「何秒遅くなると離脱率がx倍悪化する」というユーザービリティ観点にシフトし、Core Web Vitalsの登場によりSEOに直結するまでになりました。

参考: ページの読み込み時間が 1 秒から 10 秒に増加すると、モバイル サイト訪問者が直帰する確率は123%増加します

www.thinkwithgoogle.com

今や「Webサイトの表示速度はプロダクトチーム、会社として見過ごすことができない指標」となりました。

そこでハローではWeb高速化プロジェクトが立ち上がり、約5ヶ月間集中してサイトを速くする施策をし続けました。

正直なところその道のりは決して順風満帆なものではなく、「もうやれることないよ〜」と何度も壁に当たり、スコアもうなぎ登りになったわけではありませんでした。

それでも、CLSの対策(後述)をしたことでローディングUIが大幅に改善し、サイトのスピードが改善されたことで操作のサクサク感も大幅に増したと自信をもって言えます。

今年1年「ハローとしてどのようにWebの高速化に取り組み、どのような成果が得られたか」を書いていきます。1つの記事でとても収まるものではないので、いくつかの記事に分けて書いていきたいと思います。

まず今回の記事では「チーム全体でページスピードを意識し続ける」ことを念頭に、AutoReserveのページスピードの過去と現在をチームメンバー全員が正しく把握できるデータ基盤を整えた話をします。

エンジニアだけでなくプロダクトオーナーやPM、SEO担当の方などプロダクトに関わる方皆さんに見ていただけたらなと思っております。

Page Speed Insights・Lighthouse・Core Web Vitalsの理解

Web高速化をチームとして追おう!となりいろいろ調べていくと、この3つのワードが出てくるかと思います。

それぞれ何を指しているかを理解すると、今後混乱が防げるかと思うので簡単に説明します。

Lighthouse

Webサイトのページを評価するためのツールです。LighthouseはChrome Extensionを通して使うことができ、Page SpeedだけでなくアクセシビリティやSEOなどの分析をすることができます。

Lighthouseの「パフォーマンス」とPage Speed Insightsのスコアはほぼイコールですが、実行環境が異なります。Lighthouseは実行するPC、Page Speed Insightsは一般的なユーザーの環境を再現しています。

Lighthouseの「パフォーマンス」とPage Speed Insightsのスコアがイコールであると思っていればokです。

Page Speed Insights

Page Speed Insightsは、Webサイトのページスピードを100点満点で表すもっともメジャーな環境です。

Page Speed Insightsには5つの指標FCP, SI, LCP, TBT, CLSがあり、それぞれに配点が振り分けられています。最初の表示がめちゃくちゃ早くても、ロード完了が遅ければそれだけ減点されますし、どれだけ早くてもCLSが最悪なら75点以上獲得できないようになっています。

PSIは現在バージョン10であり、FCP 10点、SI 10点、LCP 25点、TBT 30点、CLS 25点となっています。

各指標に閾値が設定されており、例えばFCP 500ms以下なら10点、1440msなら6点というようにスコアに応じて点数が決定します。

Lighthouse v10における重みづけ

それぞれの指標が何を表しているのかについては、Googleが記事を出しているのでそちらを参照してください。

Lighthouse performance scoring  |  Chrome for Developers

Page Speed Insightsの点数改善は、CLSのみ全く別のアプローチとなっており、それ以外の指標は「表示を早くする」ことに注力すれば改善します。

ドキュメントには「FCP を改善する方法」「LCPを改善する方法」というように、それぞれの指標に対するアプローチが記載されています。

実際に指標と睨めっこしてみると、FCP, SI, LCP, TBTはともに相関関係にあり、「FCPだけを改善する」というよりは「○◯した結果FCPとLCPに良い影響があった」というような経過がありました。よって「この指標を改善するために◯◯をします」というアプローチをするよりも、「改善できるところを洗い出しそれをやった結果、あの指標が改善しました」みたいな流れになるのが自然かと思いました。

次の記事では「CLSを満点にするためにやったこと」、その次の記事では「表示を速くするためにやったこと」をお話しします。

Core Web Vitals

Core Web Vitals は、ページの読み込みパフォーマンス、インタラクティブ性、視覚的安定性に関する実際のユーザー エクスペリエンスを測定する一連の指標です。検索結果でのランキングを上げ、全般的に優れたユーザー エクスペリエンスを提供できるよう、サイト所有者の皆様には、Core Web Vitals を改善することを強くおすすめします。Core Web Vitals は、その他のページ エクスペリエンス要素とともに、Google のコア ランキング システムがランキングを決定する際に考慮する要素です。(Core Web Vitals と Google 検索の検索結果について より)

Core Web VitalsはLCP、CLS、FID(2024年3月からINPになるので、今からやるならINPを意識したほうがよい)の3つの要素から構成されます。Page Speed Insightsの数値を改善することがイコールCore Web Vitalsの結果を改善することにつながると考えて良いです。

しかしながら両者には大きな違いがあります。それは「実際のユーザーの行動データを参照しているかどうか」です。

Core Web Vitalsは実際のユーザーの行動データをもとに算出されます。これはChrome UX Reportを参照すればよくわかります。Core Web Vitalsはユーザーのネットワーク環境・デバイスのスペック等様々な要因が絡みます。それに対しPage Speed Insightsは、実際のユーザー環境の中でもごく一般的な環境を想定した仮想的な環境のもと実行されます。

最近のPage Speed Insightsでは、上部にCore Web Vitalsの指標、その下にPage Speed Insightsの計測結果が表示されるようになりました。

チーム全体でページスピードを意識するために必要な認識

ハローで高速化プロジェクトを進めるにあたり、ロール問わず知っておいた方がよい知識がいくつかあると実感しました。

中でも大切なものをピックアップして残しておきます。

①スタートが30点以下の場合、点数を上げるのは容易なことではない

Lighthouse Scoring Calculator というものがあります。これを使うと、たとえば「FCP 2,145msのときFCPは何点になるのか?」といったことをシミュレーションすることができます。以下は各指標の簡易的な配点表です。

Percentage First Contentful Paint (10) Speed Index (10) Largest Contentful Paint (25) Total Blocking Time (30)
100% 1,000 1,000 1,000 0
90% 1,800 3,387 2,500 200
80% 2,145 4,074 2,938 292
70% 2,434 4,654 3,300 383
60% 2,712 5,215 3,645 483
50% 3,000 5,800 4,000 600
40% 3,319 6,451 4,389 746
30% 3,697 7,228 4,848 941
20% 4,196 8,258 5,447 1,235
10% 5,000 9,933 6,400 1,800
0% 6,000 12,000 8,000 3,000

Largest Contentful Paintの列を例にとると、1,000ms以下で100%の25点、3,300msで70%の17.5点を獲得できることが分かります。80%から40%の間は約350msで区切られていますが、40%未満になるとその間隔が広くなります。

8,000msより遅い場合は1点も獲得できません。高速化プロジェクト発足時、AutoReserveのLCPは20,000msでした。13,600msも短縮して6400msにしないと点数をもらえないことを意味しています。

0点から100点は決して等間隔ではなく、ざっくり以下のように道のりの辛さが分かれると思っています。

めちゃ大変: 0-30 or 40

頑張ればすぐに結果が出る: 40-80

大変: 80-100

私はこの現状を見て「点数を上げてみせます!」とはとても言えず、「今現在あまりにも遅いのでちょっとやそっとの改善では点数が動きません。点数ではなく秒数の改善を見てほしいです、そっちの方が健康的です」と正直に言いました。(ちなみにプロジェクト開始直後のAutoReserveは5~10点くらいでした)

もしページスピード改善プロジェクトを牽引することになったときは、現状把握を正しく行い、現実的な目標を立てることがとても大切です。一歩ずつ進め、ページスピードが着々と改善していくことをチーム全体で共有し実感していくことが、パフォーマンスを意識したプロダクト開発文化の醸成につながると思っています。

②Core Web Vitalsの改善はタイムラグがある

「Page Speed InsightsとCore Web Vitalsは実行環境が異なる」ということを上段で述べましたが、決定的な違いは結果の即時性にあります。

上段で述べたようにPage Speed Insightsはリアルタイムの計測ですが、Core Web Vitalsはユーザーの行動に基づいて集計されます。そのため結果の反映にラグがあります。

Core Web Vitalsの結果を見る方法は2通りあります。1. Chrome UX Report2. Google Search Consoleの2つです。

Chrome UX Reportは月次でのレポーティングのみなのでリアルタイムさは皆無、Google Search ConsoleはCore Web Vitalsに問題ありなURLの数と、グループごと(URLのパターンによって自動で分別される)の数値しか分かりません。

そのためページスピードの改善経過を正しく把握するには、Page Speed Insightsの結果を追い続けるのが一番です。

しかしながらSEOの担当者は、SEOに直接影響しているCore Web Vitalsの経過を知りたいはずです。Core Web Vitalsの経過を高頻度で把握するには、現状ではChrome UX Report APIを使うのが一番です。下記に記載したGoogle Apps ScriptのコードでChrome UX Report APIも扱っているので参考にしてみてください。

③UI変更の必要がありうる

これは特にCLSに関する話ですが、デザインの変更なしではCLSの改善が進まない場合があります。ということはデザイナーやPMを巻き込んだ改善になる可能性が高いです。

CLSについては https://web.dev/articles/cls?hl=ja を参照してください。

例えばファーストビュー(ロード時に一番最初に表示される領域)に、「ある特定のユーザーグループにだけ動的にバナーを出したい」場合です。CLSはロード開始時とロード終了時のUIのズレを数値化したものです。

CLSの改善方法は「動的コンテンツの表示領域をあらかじめ確保しておく」ことです。単純にその領域を空白にしておくだけでも良いですし、より洗練されたユーザー体験を提供ためにローディングスケルトンを用意するのも良いです(次の記事で詳しく触れます)。しかしこの例のような不確定要素には対応できません。

今回のケースで完全にCLSを改善するためには、「バナーそのものを取り除く」か「別のバナーを表示して、バナー領域が使われない可能性をなくす」対応が必要です。

AutoReserveのレストランページも同様の問題を抱えていました。レストランページの最上部にて、画像が存在する場合は表示するが、存在しない場合は表示しないという実装をしていました。これはCLSに大きな影響を与えます。

AutoReserveが持っているレストラン情報において画像があるレストランは約半数でした。画像があるページの方がリッチなページと捉えられることが多いため、画像部分にスケルトンを実装する方向にしました。画像ありページのCLSは死守しようというわけです。

このように、CLSを改善するためにはエンジニアだけでなくデザイナーやPMを巻き込んで進行することでお互い納得のいく選択をすることができると感じました。

④同一URLグループ内のページ全て改善しないとCore Web Vitalsに改善が見られない

上述のレストランページにおける画像スケルトン対応ですが、画像なしのページはCLSが悪いままなので、No Image画像を表示したいと思いました。

しかしこの時点では対応しませんでした。No Image画像を表示することがデザイン的に好ましくないという声が上がったためです。

CLS改善の経過を見守ると、Core Web Vitals上のCLSは思ったほど改善しませんでした。下記は月毎のCLSのOK割合を示したものです。緑色のGoodの割合が増えるほど良いCLSのページが存在することを示します。反映までのタイムラグがあるので気長に待っていましたが、約10%程度しか改善しませんでした。

Core Web VitalsはURLのグループごとで判定されます。今回改善を行ったのはレストランページなので、CLSがGoodとPoorのページが同一グループに分類されます。明確な理由は分かりませんでしたが、同一グループ内でGoodとPoorが混在する場合は悪い方に引っ張られるのかなと感じました。このままではあまり良くないのではないかと話し合い、No Image画像の表示を決定しました。

No Image画像の実装後明らかにCLSが改善したため、「同一URLグループ内のページ全て改善しないとCore Web Vitalsに改善が見られない」という仮説は一定の信憑性が得られたといってよいと思います。

Page Speed InsightsとCore Web Vitalsの数値を可視化する

ここからは実際に分析環境を作っていきます。データを集計するためのコードは下記に記載したので、基本的にぽちぽちしてれば出来上がるはずですのでぜひ試してみてください!

Page Speed Insights API・Google Spread Sheet・Google Apps Script・Looker Studio(旧Google Data Portal)の4つを使い、デイリーで重要ページのPage Speedを可視化していきます。

1. 事前準備

この可視化プロジェクトは無料で完結しますが、Page Speed Insights・Core Web VitalsのAPIを使うためにそれぞれのAPIキーが必要です。以下のリンクを参照し、APIキーを取得してください。

それぞれのAPIキーはGoogle Cloudプロジェクトに紐づくため、必要に応じてプロジェクトを作成する必要があります。クレジットカード等の登録は必要なく、無料で取得可能です。詳しくは下記のドキュメントを参照してください。

2. Page Speed insightsの結果をGoogle Spread Sheetに出力する

「Google Apps Scriptで、Page Speed Insights APIをfetchしその結果をSpread Sheetに入れる」という流れを作ります。基本的に以下をコピペすれば使うことができます。

// type定義が長くなったのでここでは割愛します。詳しくはリポジトリを参照してください。

const PAGE_SPEED_INSIGHTS_API_ROOT =
  'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'
const API_KEY = 'YOUR_PAGESPEED_API_KEY' // https://developers.google.com/speed/docs/insights/v5/get-started?hl=ja#APIKey を参照

各シートのヘッダーを定義しておく
const INITIAL_COLUMN_NAMES = [
  '日付',
  'Performance',
  'FCP score',
  'SI score',
  'LCP score',
  'TBT score',
  'CLS score',
  'FCP time(ms)',
  'SI time(ms)',
  'LCP time(ms)',
  'TBT time(ms)',
  'CLS value',
]

const CORE_WEB_VITALS_API_ROOT = 'https://chromeuxreport.googleapis.com/v1/records:queryRecord' // https://developer.chrome.com/docs/crux/api?hl=ja#crux-api-keyを参照
const CORE_WEB_VITALS_API_KEY = 'YOUR_CORE_WEB_VITALS_API_KEY'

class SpreadSheet {
  private spreadsheet: GoogleAppsScript.Spreadsheet.Spreadsheet

  constructor() {
    this.spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
  }

  get urlListSheet() {
    return this.spreadsheet.getSheetByName('URLs')
  }

  sheetByName(name: string) {
    const sheet = this.spreadsheet.getSheetByName(name)

    if (sheet != null) {
      return sheet
    }

    const newSheet = this.spreadsheet.insertSheet(name)

    // newSheetの一行目に初期値を入れる
    const range = newSheet.getRange(1, 1, 1, INITIAL_COLUMN_NAMES.length)
    range.setValues([INITIAL_COLUMN_NAMES])

    return newSheet
  }
}

const getTargetUrls = () => {
  const spreadsheet = new SpreadSheet()
  const sheet = spreadsheet.urlListSheet

  if (sheet == null) {
    return []
  }

  // シート「URLs」のA,B列の値を取得
  const range = sheet.getRange(1, 1, sheet.getLastRow(), 2)

  return range.getValues().map((v) => ({
    name: v[0] as string,
    url: v[1] as string,
  }))
}

const getPageSpeedResults3Times = (url: string): PageSpeedModel[] => {
  // UrlFetchAppのfetchAllを使い、3回fetchし、その平均値を取得する
  Logger.log(`3 times fetch ${url}`)

  const responses = UrlFetchApp.fetchAll([
    `${PAGE_SPEED_INSIGHTS_API_ROOT}?url=${url}&category=performance&strategy=mobile&key=${API_KEY}`,
    `${PAGE_SPEED_INSIGHTS_API_ROOT}?url=${url}&category=performance&strategy=mobile&key=${API_KEY}`,
    `${PAGE_SPEED_INSIGHTS_API_ROOT}?url=${url}&category=performance&strategy=mobile&key=${API_KEY}`,
  ])

  return responses.map((v) => JSON.parse(v.getContentText()) as PageSpeedModel)
}

const getPageSpeedResult = (url: string): PageSpeedModel => {
  Logger.log(`fetch once ${url}`)
  const response = UrlFetchApp.fetch(
    `${PAGE_SPEED_INSIGHTS_API_ROOT}?url=${url}&category=performance&strategy=mobile&key=${API_KEY}`,
    {}
  )

  const pagespeedResult = JSON.parse(
    response.getContentText()
  ) as PageSpeedModel

  return pagespeedResult
}

const getCoreWebVitals = () => {
  // Core Web Vitalsのレポートはドメインごとに生成されます。対象のwebsiteに書き換えてください
  const origin = 'https://autoreserve.com'

  Logger.log(`fetch ${origin} as core web vitals`)

  const spreadSheet = new SpreadSheet()
  const sheet = spreadSheet.sheetByName('Core Web Vitals')

  const body = {
    origin,
    "formFactor": "PHONE"
  }

  const options = {
    method: 'post' as any,
    contentType: 'application/json',
    payload: JSON.stringify(body)
  }

  // POST APIを実行
  const response = UrlFetchApp.fetch(`${CORE_WEB_VITALS_API_ROOT}?key=${CORE_WEB_VITALS_API_KEY}`, options)
  const corewebVitals = JSON.parse(response.getContentText()) as TCoreWebVitals

  const { metrics } = corewebVitals.record

  const date = new Date()
    .toLocaleDateString('ja-JP', {
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    })
    .split('/')
    .join('/')

  Logger.log(metrics.first_contentful_paint)

  const values = {
    fcp: metrics.first_contentful_paint.percentiles.p75,
    fid: metrics.first_input_delay.percentiles.p75,
    inp: metrics.interaction_to_next_paint.percentiles.p75,
    lcp: metrics.largest_contentful_paint.percentiles.p75,
    cls: metrics.cumulative_layout_shift.percentiles.p75,
    ttfb: metrics.experimental_time_to_first_byte.percentiles.p75,
    fcpGreen: metrics.first_contentful_paint.histogram[0].density,
    fcpYellow: metrics.first_contentful_paint.histogram[1].density,
    fcpRed: metrics.first_contentful_paint.histogram[2].density,
    fidGreen: metrics.first_input_delay.histogram[0].density,
    fidYellow: metrics.first_input_delay.histogram[1].density,
    fidRed: metrics.first_input_delay.histogram[2].density,
    inpGreen: metrics.interaction_to_next_paint.histogram[0].density,
    inpYellow: metrics.interaction_to_next_paint.histogram[1].density,
    inpRed: metrics.interaction_to_next_paint.histogram[2].density,
    lcpGreen: metrics.largest_contentful_paint.histogram[0].density,
    lcpYellow: metrics.largest_contentful_paint.histogram[1].density,
    lcpRed: metrics.largest_contentful_paint.histogram[2].density,
    clsGreen: metrics.cumulative_layout_shift.histogram[0].density,
    clsYellow: metrics.cumulative_layout_shift.histogram[1].density,
    clsRed: metrics.cumulative_layout_shift.histogram[2].density,
    ttfbGreen: metrics.experimental_time_to_first_byte.histogram[0].density,
    ttfbYellow: metrics.experimental_time_to_first_byte.histogram[1].density,
    ttfbRed: metrics.experimental_time_to_first_byte.histogram[2].density,
  }

  // valuesをシート最後の行に追加
  sheet.appendRow(
    Object.values({
      date,
      ...values,
    })
  )
}

function batch() {
  const spreadSheet = new SpreadSheet()

  // 検査対象のURL一覧を取得
  const urlList = getTargetUrls()

  urlList.forEach((v, index) => {
    const sheet = spreadSheet.sheetByName(v.name)

    const isAutoReserveUrl = v.url.includes('autoreserve.com')

    const date = new Date()
      .toLocaleDateString('ja-JP', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
      })
      .split('/')
      .join('/')

    let values: TValue

    // 私たちはベンチマークとするサイトも記載している。3回の平均値を取るのは、私たちのページのみ
    if (isAutoReserveUrl) {
      const pagespeedResults = getPageSpeedResults3Times(v.url)

      const vArray: TValue[] = []
      pagespeedResults.forEach((result) => {
        const { categories, audits } = result.lighthouseResult

        vArray.push({
          performance: (categories.performance.score * 100).toFixed(0),
          fcpScore: (
            audits['first-contentful-paint'].score *
            categories.performance.auditRefs.filter(
              (v) => v.id === 'first-contentful-paint'
            )[0].weight
          ).toFixed(1),
          siScore: (
            audits['speed-index'].score *
            categories.performance.auditRefs.filter(
              (v) => v.id === 'speed-index'
            )[0].weight
          ).toFixed(1),
          lcpScore: (
            audits['largest-contentful-paint'].score *
            categories.performance.auditRefs.filter(
              (v) => v.id === 'largest-contentful-paint'
            )[0].weight
          ).toFixed(1),
          tbtScore: (
            audits['total-blocking-time'].score *
            categories.performance.auditRefs.filter(
              (v) => v.id === 'total-blocking-time'
            )[0].weight
          ).toFixed(1),
          clsScore: (
            audits['cumulative-layout-shift'].score *
            categories.performance.auditRefs.filter(
              (v) => v.id === 'cumulative-layout-shift'
            )[0].weight
          ).toFixed(1),
          fcpTime: audits['first-contentful-paint'].numericValue.toFixed(1),
          siTime: audits['speed-index'].numericValue.toFixed(1),
          lcpTime: audits['largest-contentful-paint'].numericValue.toFixed(1),
          tbtTime: audits['total-blocking-time'].numericValue.toFixed(1),
          clsValue: audits['cumulative-layout-shift'].numericValue.toFixed(1),
        })
      })

      // vArrayのkeyそれぞれの平均値を計算。vArray.lengthで割る
      values = {
        performance: (
          vArray.reduce((acc, cur) => acc + parseInt(cur.performance), 0) /
          vArray.length
        ).toFixed(0),
        fcpScore: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.fcpScore), 0) /
          vArray.length
        ).toFixed(1),
        siScore: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.siScore), 0) /
          vArray.length
        ).toFixed(1),
        lcpScore: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.lcpScore), 0) /
          vArray.length
        ).toFixed(1),
        tbtScore: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.tbtScore), 0) /
          vArray.length
        ).toFixed(1),
        clsScore: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.clsScore), 0) /
          vArray.length
        ).toFixed(1),
        fcpTime: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.fcpTime), 0) /
          vArray.length
        ).toFixed(1),
        siTime: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.siTime), 0) /
          vArray.length
        ).toFixed(1),
        lcpTime: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.lcpTime), 0) /
          vArray.length
        ).toFixed(1),
        tbtTime: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.tbtTime), 0) /
          vArray.length
        ).toFixed(1),
        clsValue: (
          vArray.reduce((acc, cur) => acc + parseFloat(cur.clsValue), 0) /
          vArray.length
        ).toFixed(1),
      }
    } else {
      const pagespeedResult = getPageSpeedResult(v.url)

      const { categories, audits } = pagespeedResult.lighthouseResult

      values = {
        performance: (categories.performance.score * 100).toFixed(0),
        fcpScore: (
          audits['first-contentful-paint'].score *
          categories.performance.auditRefs.filter(
            (v) => v.id === 'first-contentful-paint'
          )[0].weight
        ).toFixed(1),
        siScore: (
          audits['speed-index'].score *
          categories.performance.auditRefs.filter(
            (v) => v.id === 'speed-index'
          )[0].weight
        ).toFixed(1),
        lcpScore: (
          audits['largest-contentful-paint'].score *
          categories.performance.auditRefs.filter(
            (v) => v.id === 'largest-contentful-paint'
          )[0].weight
        ).toFixed(1),
        tbtScore: (
          audits['total-blocking-time'].score *
          categories.performance.auditRefs.filter(
            (v) => v.id === 'total-blocking-time'
          )[0].weight
        ).toFixed(1),
        clsScore: (
          audits['cumulative-layout-shift'].score *
          categories.performance.auditRefs.filter(
            (v) => v.id === 'cumulative-layout-shift'
          )[0].weight
        ).toFixed(1),
        fcpTime: audits['first-contentful-paint'].numericValue.toFixed(1),
        siTime: audits['speed-index'].numericValue.toFixed(1),
        lcpTime: audits['largest-contentful-paint'].numericValue.toFixed(1),
        tbtTime: audits['total-blocking-time'].numericValue.toFixed(1),
        clsValue: audits['cumulative-layout-shift'].numericValue.toFixed(1),
      }

    }

    // valuesをシート最後の行に追加
    sheet.appendRow(
      Object.values({
        date,
        ...values,
      })
    )

    Logger.log(`${v.name} score ${values.performance}`)
    Logger.log(`Done: ${index + 1} / ${urlList.length}`)
  })
}

実装にあたり、Google Apps Scirptのオンラインエディタでコーディングするのはいろいろとやりづらいので、Clasp(TypeScriptをGoogle Apps Scriptにcompileするなどできる)を使って、TypeScriptの恩恵を受けながら実装しました。

Claspを使うとローカルのプロジェクトとGoogle Apps Script, Spread Sheetを紐づけたりできるので、ローカルからpushするだけでリリースが完了します。

Google Apps Scriptにはスケジューリング機能があるため(これがめちゃ便利)、毎日1時に実行するように設定してください。


以下READMEの抜粋です。

準備

npm install -g @google/clasp

Claspの準備

Spreadsheetを作成するGoogleアカウントを選択します

clasp login

このプロジェクトに紐づいたSheetを作成します

clasp create --type sheets

Install deps

yarn

反映

(tscしなくても勝手にトランスパイルしてくれます)

yarn push

データを集めたいURLをSpread Sheetに入力する

  • URLsという名前のシートを作成します
  • A列に名前、B列にURLを入力してください
TOP https://autoreserve.com/ja/
ホーム https://autoreserve.com/ja/jp
検索 https://autoreserve.com/ja/?keyword=%E6%96%B0%E5%AE%BF%E9%A7%85
レストラン詳細 https://autoreserve.com/ja/restaurants/ygJncRkni7pVoBoMWHXg

A列の名前でシートが作成され、そのシートにB列のURLの結果が挿入されるようになります。

Apps Scriptにスケジュールトリガーをつける

画面左のメニューから「トリガー」を選択、トリガー一覧画面を表示し「トリガーを追加」をクリック。

以下のように入力して保存してください。

  • 実行する関数 batch
  • 実行するデプロイを選択 Head
  • イベントのソースを選択 時間主導型
  • 時間ベースのトリガーのタイプを選択 日付ベースのタイマー
  • 時刻を選択 午前0時 ~ 1時

これでスケジュールの設定は完了です。

詳しくはリポジトリを参照してください。https://github.com/hello-ai/pagespeed_aggregator

3. Looker Studioでデータを可視化する

Google Spread Sheetに貯められたデータを可視化するために使います。無料で使うことができます。

下記に私たちが実際に作ったレポートテンプレートを公開しているのでぜひみてみてください(データはサンプルのものを使っています)。

https://lookerstudio.google.com/u/0/reporting/93ddda7c-74b7-4c7a-9024-dd4e2dd00a7a/page/Ud4WD

また、上記「2. Page Speed insightsの結果をGoogle Spread Sheetに出力する」を完了していれば、皆さんのデータをすぐに可視化することも可能です。簡単な手順をご説明します。

①サンプルレポートのPreviewを表示する

下記を開いてください

https://lookerstudio.google.com/u/0/reporting/93ddda7c-74b7-4c7a-9024-dd4e2dd00a7a/page/Ud4WD/preview

②「自分のデータを使用」をクリック

③データソースを自分のSpread Sheetに置き換える

実際に自分が使っているデータに置き換えてグラフやテーブルを表示することができます。私たちはAutoReserveの4ページを対象にしているので、その4ページ(A~Dにあたる)とCore Web Vitalsのシートを選択してデータを置換してください。

④「編集して共有」をクリック

すべてのデータソースを自分たちのものに切り替えたら「編集して共有」をクリックしてください。そうすると、皆さんのデータソースが適用された「Sample Page Speed Report」のレポートが作成されます!「Sample Page Speed Report のコピー」というレポート名になるはずなので任意の名前に変えてください。

これで皆さん専用のPage Speed Reportの完成です。

AutoReserveのチームではこのレポートを見ながら改善の経過を辿り、大きな変化があったところでどのような変更が加えられたかをチェックしています。

「コミットごとにPage Speed InsightsのAPIを叩いて可視化する」ようなことも考えたのですが、それほど頻繁な変化があるわけでもないので日時のレポートにとどめています。

ページスピードに大きな影響を及ぼすJavaScriptのBundle Sizeのチェックはコミットごとに行っています。こちらについては次の次の記事で詳しく触れていきます。


今回は「メンバーを巻き込み、分析基盤を整える」をテーマに、ページスピード周りの解説や、チームメンバー全員が知っておくべき周辺知識、ページスピードレポートの作成まで触れました。皆さんのプロダクトがよりよくより速いものになるための参考になれば幸いです。最後までご覧いただきましてありがとうございました。

ハローではAutoReserveのページスピードを100点に近づけるために力を貸してくれるエンジニアを募集しています。

採用情報 - 株式会社ハロー