📊

Webアプリのパフォーマンスを(お金をかけずに)定点観測する方法

はじめに

Webアプリケーションの開発者の方であれば、自身の開発したアプリのパフォーマンスを気にしたことがあるのではないかと思います。

実際にChromeに同梱されている「Lighthouse」やオンラインサービスの「PageSpeedInsights」、「webpagetest」といったツールを使って計測したことがある方も多いのではないでしょうか。

ただ、単発で計測するのが簡単ですが、長期的な動向を見たいという場合これらのツールだけで測るのは難しいかと思います。

そこで、Webアプリのパフォーマンスを長期的に「定点観測」する取り組みを行ったのでご紹介したいと思います。

どのように計測するか

主にChromeのデベロッパーツール内のLighthouseを使用し手動で計測するなかで、以下の要素がパフォーマンスに影響を与えていると考えアプリケーションの代表的なページのURL(10個ほど)に対してそれぞれのパターンについて計測することにしました。

  • モバイルかPCか
    • ページレイアウトが異なるので、それぞれ計測する必要があるため
  • ブラウザキャッシュが存在するかしないか
    • jsファイルなどの静的ファイルはブラウザキャッシュできるようにしているため、1度目のアクセスより2度目以降のブラウザキャッシュが効いているときのアクセスの方が有利と考えたため
  • 3rdパーティスクリプトを読み込むか読み込まないか
    • GoogleTagManagerにより3rdパーティスクリプトを読み込んでおり、それによりパフォーマンスに影響を与えていると考えたため

つまり1つのページに対して、2×2×2の8パターンで計測してみようということになります。(対象ページが10ページなら80回の計測になります)

さすがに手動で測るのはつらいのでシェルスクリプトを組んで計測することを考えます。

LighthouseCLIで計測する

Lighthouseをコマンドラインで実行できるツール(https://github.com/GoogleChrome/lighthouse/)が公開されているので、これを使用してシェルスクリプトを書いてみます。

例として当社のコーポレートサイト(https://www.forcia.com/)の3つのページに対して、前述した8パターンで計測し結果をCSV形式で出力するようにしたものが以下になります。

set -eu

# 計測対象のタイトルとURLのリスト
# サンプルとして当社のコーポレートサイト
urllist=(
    "toppage https://www.forcia.com/"
    "servicepage https://www.forcia.com/service/"
    "blogpage https://www.forcia.com/blog/"
)

# chromeプロセスがあれば念のためkillする
ps aux | grep chrome | grep -v grep | awk '{ print "kill -9", $2 }' | sh
# chromeをバックグラウンドで9222ポートで起動しておく
nohup /usr/local/bin/chrome-debug --port=9222 --headless &

# 計測する
# 3rdパーティ有無×モバイルorPC×キャッシュ有無の計8パターンについて計測する
with3rdparty_or_without3rdparty=("with3rdparty" "without3rdparty")
pc_or_mobile=("pc" "mobile")
nocache_or_withcache=("nocache" "withcache") # nocacheを先に実行してブラウザキャッシュを効かせておく
for item in "${urllist[@]}" ; do
    for with3rdparty_without3rdparty in "${with3rdparty_or_without3rdparty[@]}" ; do
        for pc_mobile in "${pc_or_mobile[@]}" ; do
            for nocache_withcache in "${nocache_or_withcache[@]}" ; do

                # 計測対象のURLとタイトルを取得
                set ${item}
                TITLE=${1}
                URL=${2}

                # テストIDを決める
                YYYYMMDD_HHMMSS=`date "+%Y%m%d_%H%M%S"`
                YYYYMMDD=`date "+%Y/%m/%d"`
                HHMMSS=`date "+%H:%M:%S"`
                TESTID=${YYYYMMDD_HHMMSS}_${TITLE}_${pc_mobile}_${with3rdparty_without3rdparty}_${nocache_withcache}

                # 実行オプション
                OPTION="--port=9222 --quiet --only-categories=performance --output=json,html" # 値の取得だけならjsonだけでよいが、ここではhtmlも出力する
                if [ ${nocache_withcache} = "withcache" ]; then
                    # キャッシュありの場合はストレージを初期化しない
                    OPTION=${OPTION}" --disable-storage-reset=true"
                fi
                if [ ${pc_mobile} = "pc" ]; then
                    # pcの場合はpresetを設定
                    OPTION=${OPTION}" --preset=desktop"
                fi
                if [ ${with3rdparty_without3rdparty} = "without3rdparty" ]; then
                    # 3rdpartyを除外する場合はgoogletagmanagerをブロック
                    OPTION=${OPTION}" --blocked-url-patterns='www.googletagmanager.com'"
                fi

                # 結果格納用ディレクトリ作成
                mkdir -p ./result/${TESTID}

                # Lighthouseの実行
                /usr/local/bin/lighthouse ${URL} ${OPTION} \
                    --output-path=./result/${TESTID} \
                    -GA=./result/${TESTID} # -GAでトレース情報も出力する

                # 出力ファイルをjqコマンドで読み込みCSVへ出力
                JQ_FORMAT='['
                JQ_FORMAT=${JQ_FORMAT}'.audits["first-contentful-paint"].numericValue,'
                JQ_FORMAT=${JQ_FORMAT}'.audits["largest-contentful-paint"].numericValue,'
                JQ_FORMAT=${JQ_FORMAT}'.audits["speed-index"].numericValue,'
                JQ_FORMAT=${JQ_FORMAT}'.audits["total-blocking-time"].numericValue,'
                JQ_FORMAT=${JQ_FORMAT}'.audits["cumulative-layout-shift"].numericValue,'
                JQ_FORMAT=${JQ_FORMAT}'.audits["server-response-time"].numericValue'
                JQ_FORMAT=${JQ_FORMAT}']|@csv'

                result=$(cat ./result/${TESTID}.report.json | jq -r -c ${JQ_FORMAT}) # JSONファイルは.report.jsonという拡張子で出力される
                echo ${YYYYMMDD}","${HHMMSS}","${TITLE}","${pc_mobile}","${with3rdparty_without3rdparty}","${nocache_withcache}","${result} >> ./result.csv
            done
        done
    done
done

# chromeをkillする
ps aux | grep chrome | grep -v grep | awk '{ print "kill -9", $2 }' | sh

実行してみて結果を見てみましょう。

result.csv には以下のようにカンマ区切りで計測結果が出力されるはずです。

$ cat result.csv 
2025/07/18,17:36:54,toppage,pc,with3rdparty,nocache,1198.863,1386.837,2246.675121809698,8.5,0.0022335464641526563,8.659000000000002
2025/07/18,17:37:16,toppage,pc,with3rdparty,withcache,463.0231,505.0231,1757.1385604224288,7,0.0022335464641526563,8.373000000000001
2025/07/18,17:37:37,toppage,mobile,with3rdparty,nocache,10114.359400000001,22140.95335,10365.920073369647,102.3239999999978,0,8.344
2025/07/18,17:37:59,toppage,mobile,with3rdparty,withcache,2851.7682999999997,3635.38309052124,4280.70018921042,50.06119999999987,0.21540341902223678,10.823
2025/07/18,17:38:20,toppage,pc,without3rdparty,nocache,1170.4542000000001,1466.0339000000001,2115.9346744839445,85.64774999999986,0.005699958130420885,8.315000000000001
2025/07/18,17:38:40,toppage,pc,without3rdparty,withcache,731.3667810424804,811.4657810424804,731.3667810424804,0,0.0022335464641526563,8.378
2025/07/18,17:39:01,toppage,mobile,without3rdparty,nocache,9334.045999999997,20075.559499999996,9393.776749660872,7.5,0,8.465
2025/07/18,17:39:22,toppage,mobile,without3rdparty,withcache,2516.7574810424803,3437.98037135315,4189.4861062784585,0,0.21540341902223678,8.783
<以下略>

またresultディレクトリ配下にjson,htmlなどが出力されていることが確認できます。

$ ls -lF result
drwxr-xr-x 2 forcia forcia    117  7月 18 17:37 20250718_173654_toppage_pc_with3rdparty_nocache/
-rw-r--r-- 1 forcia forcia 829916  7月 18 17:37 20250718_173654_toppage_pc_with3rdparty_nocache.report.html
-rw-r--r-- 1 forcia forcia 763831  7月 18 17:37 20250718_173654_toppage_pc_with3rdparty_nocache.report.json
<以下略>

さらに「ブラウザキャッシュがちゃんと効いてるか」、「googletagmanagerがブロックされているか」がちゃんと機能しているか確認してみましょう。

ブラウザキャッシュ有効かつ3rdパーティ無効とした20250718_173840_toppage_pc_without3rdparty_withcacheのようなディレクトリ配下にdefaultPass.trace.jsonというファイルが出力されますので、これをchromeのDevelperToolのパフォーマンスタブにドラッグアンドドロップして開きます。

「ネットワーク」の部分でキャッシュ可能なコンテンツ、例えばcssを指定すると「キャッシュから:はい」となっていることが確認できます。

キャッシュ

同じ要領でgoogletagmanagerをブロックした場合は「ネットワーク」の部分にgoogletagmanager由来のjsが取得されていないことが確認できます。

LighthouseCLIの結果を見る

計測できたので計測結果を見てみます。

LCP,FCPなどの数値だけであればCSVファイルだけを見ればよいですが、いつものLighthouseのレポート形式でhtmlファイルにも出力しているのでどんな感じになっているか見てみます。

すると何かおかしいことに気づきます。

全体のスコアを見ると結構いいスコアなのですが、

スコア

LCPの内訳をみると「Render Delay」がなぜかマイナス値になっていて辻褄が合っていないように見えます。

LCP

調べてみると、以下のissueがヒットしました。
https://github.com/GoogleChrome/lighthouse/issues/16213
Lighthouseの仕様(不具合)のようで、おそらく以下のようになっているようです。

  • LCP値は実測値ではなくシミュレーション値で計算している
  • サーバの応答(TTFB)が遅い場合にシミュレーション値と実測値の乖離が生じる場合がある
  • 「Render Delay」はLCPのシミュレーション値とTTFBの実測値から逆算するためマイナスになる場合がある

Lighthouse以外のツールの検討

今回のケースではLighthouseのスコアは信頼できないと判断し、別のツールを使用して計測することを考えました。

webpagetest

まず、webpagetestの利用を検討しました。webpagetestは有償のオンラインサービスですがPrivate Instanceとしてローカルに計測環境を作ることができるはずです。

実際GitHubを見ると、計測環境が一式コンテナ化されていて簡単に環境を作れるように見えました。

https://github.com/catchpoint/WebPageTest/tree/master/docker/local

しかし、やってみても動かないようでした…。あまりトラブルシューティングに時間をかけたくなかったのでwebpagetestをローカルで動かすのはあきらめることにしました。

sitespeed.io

色々調べるうちに「sitespeed.io」というツールがあることが分かりました。

https://www.sitespeed.io/

トップページのサンプルにあるように、計測環境がコンテナ化されていてdocker runコマンド1行で計測ができるようです。

docker run --rm -v "$(pwd):/sitespeed.io" sitespeedio/sitespeed.io:38.0.0 https://www.sitespeed.io/

また、ブラウザキャッシュや3rdパーティスクリプトのブロックなどやりたいことはできるようでしたので、これを使って同じように計測結果をCSVに出力するシェルスクリプトを作ってみることにしました。

set -eu

# 計測対象のタイトルとURLのリスト
# サンプルとして当社のコーポレートサイト
urllist=(
    "toppage https://www.forcia.com/"
    "servicepage https://www.forcia.com/service/"
    "blogpage https://www.forcia.com/blog/"
)

# 計測する
# 3rdパーティ有無×モバイルorPC×キャッシュ有無の計8パターンについて計測する
with3rdparty_or_without3rdparty=("without3rdparty" "with3rdparty")
pc_or_mobile=("pc" "mobile")
nocache_or_withcache=("nocache" "withcache")
for item in "${urllist[@]}" ; do
    for with3rdparty_without3rdparty in "${with3rdparty_or_without3rdparty[@]}" ; do
        for pc_mobile in "${pc_or_mobile[@]}" ; do
            for nocache_withcache in "${nocache_or_withcache[@]}" ; do
                
                # 計測対象のURLとタイトルを取得
                set ${item}
                TITLE=${1}
                URL=${2}

                OPTION="-n 1" # iterationsは1回

                # テストIDを決める
                YYYYMMDD_HHMMSS=`date "+%Y%m%d_%H%M%S"`
                YYYYMMDD=`date "+%Y/%m/%d"`
                HHMMSS=`date "+%H:%M:%S"`
                TESTID=${YYYYMMDD_HHMMSS}_${TITLE}_${pc_mobile}_${with3rdparty_without3rdparty}_${nocache_withcache}

                if [ ${nocache_withcache} = "withcache" ]; then
                    # キャッシュありの場合はオプション設定
                    OPTION=${OPTION}" --preURL "${URL}
                fi
                if [ ${pc_mobile} = "mobile" ]; then
                    # mobileの場合はオプション設定
                    OPTION=${OPTION}" --mobile"
                fi

                if [ ${with3rdparty_without3rdparty} = "without3rdparty" ]; then
                    # 3rdpartyを除外する場合はgoogletagmanagerをブロック
                    OPTION=${OPTION}" --block www.googletagmanager.com"
                fi

                docker run --rm -v "$(pwd):/sitespeed.io" sitespeedio/sitespeed.io:37.6.0 \
                    --outputFolder result/${TESTID} \
                    ${OPTION} \
                    ${URL} \
                    --plugins.add analysisstorer # JSON形式で出力
                
                # 出力ファイルをjqコマンドで読み込みCSVへ出力
                JQ_FORMAT='['
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.cumulativeLayoutShift.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.ttfb.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.largestContentfulPaint.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.firstContentfulPaint.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.firstInputDelay.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.interactionToNextPaint.median,'
                JQ_FORMAT=${JQ_FORMAT}'.googleWebVitals.totalBlockingTime.median'
                JQ_FORMAT=${JQ_FORMAT}']|@csv'

                result=$(cat ./result/${TESTID}/data/browsertime.summary-total.json | jq -r -c ${JQ_FORMAT})
                echo ${YYYYMMDD}","${HHMMSS}","${TITLE}","${pc_mobile}","${with3rdparty_without3rdparty}","${nocache_withcache}","${result} >> ./result.csv
                
            done
        done
    done
done

実行してみて結果を見てみましょう。

result.csv には以下のようにカンマ区切りで計測結果が出力されるはずです。

2025/07/18,18:15:23,toppage,pc,without3rdparty,nocache,0.0035,44,688,688,0,336,221
2025/07/18,18:16:02,toppage,pc,without3rdparty,withcache,0,1044,1500,1500,0,,92
2025/07/18,18:16:49,toppage,mobile,without3rdparty,nocache,0.2288,42,1700,992,0,,165
2025/07/18,18:17:22,toppage,mobile,without3rdparty,withcache,0.2288,30,552,452,0,,66
2025/07/18,18:18:02,toppage,pc,with3rdparty,nocache,0.0035,36,960,960,0,120,399
2025/07/18,18:18:39,toppage,pc,with3rdparty,withcache,0,21,644,644,0,,119
2025/07/18,18:19:24,toppage,mobile,with3rdparty,nocache,0.2288,35,1612,584,0,,519
2025/07/18,18:19:58,toppage,mobile,with3rdparty,withcache,0.2288,23,848,540,0,304,64

また各テストごとにディレクトリが作成されhtmlファイルが出力されていることも確認できます。

$ ll result/20250718_181802_toppage_pc_with3rdparty_nocache/
合計 140
-rw-r--r-- 1 forcia forcia 13067  7月 18 18:18 assets.html
drwxr-xr-x 2 forcia forcia    59  7月 18 18:18 css
drwxr-xr-x 2 forcia forcia  4096  7月 18 18:18 data
-rw-r--r-- 1 forcia forcia 18169  7月 18 18:18 detailed.html
-rw-r--r-- 1 forcia forcia 14302  7月 18 18:18 domains.html
-rw-r--r-- 1 forcia forcia 26320  7月 18 18:18 help.html
drwxr-xr-x 3 forcia forcia  4096  7月 18 18:18 img
-rw-r--r-- 1 forcia forcia  9527  7月 18 18:18 index.html
drwxr-xr-x 2 forcia forcia  4096  7月 18 18:18 js
drwxr-xr-x 3 forcia forcia    28  7月 18 18:18 pages
-rw-r--r-- 1 forcia forcia  3124  7月 18 18:18 pages.html
-rw-r--r-- 1 forcia forcia  2727  7月 18 18:18 settings.html
-rw-r--r-- 1 forcia forcia 26465  7月 18 18:18 toplist.html

3rdパーティスクリプトの除外やブラウザキャッシュが効いてるかも見てみましょう。
index.htmlから計測結果を見ることができますが、sitespeed.ioではwaterfallも出力されるのでそれを見てみましょう。

  • ブラウザキャッシュなし・3rdパーティあり

ブラウザキャッシュなし・3rdパーティあり

  • ブラウザキャッシュあり・3rdパーティなし

ブラウザキャッシュあり・3rdパーティなし

違いは一目瞭然で、キャッシュ可能な静的ファイルや3rdパーティスクリプトが読み込まれていないことが分かります。

計測結果を集計する

CSV形式で出力したので、原始的ですがExcelで開けば集計などは簡単にできます。

例えば以下のようにピボットテーブルでページごとにアクセス形態別のパフォーマンスの傾向などを見ることができるので、どのページのどのケースについてを改善するべきかの検討に使用しています。

集計結果

まとめ

webパフォーマンスの計測を自動化して定点観測する、というと大掛かりな仕組みが必要なのではと思ったりしましたが、実際やってみると結構簡単にできてしまうと感じました。

このような方法が読者の方のWebアプリのパフォーマンス改善に役に立てば幸いです。

この記事を書いた人

樫木 淳
Lighthouseはあまり信用しないことにしました

FORCIA Tech Blog

Discussion