なぜあなたのウェブサイトは遅いのか

@mizchi | Developper Summit 2025

自己紹介

  • https://x.com/mizchi
  • Node.js とフロントエンドの専門家
  • 経歴
    • ゲームクライアント開発
    • Electron アプリ開発
    • サードパーティスクリプト
    • フリーランス(2回目)
  • 現在: 1ヶ月でパフォチュする傭兵
    • Core Web Vitals
    • CI/CD

今日のスコープ

  • 話すこと
    • 主にフロントエンド/アプリケーション視点での計測
    • とくにエンドユーザーから見たウェブパフォーマンス体験
  • 話さないこと
    • 各クラウドやDBに特化したチューニング
      • 結果として観測できるが、最初からターゲットにはしない

パフォーマンス傭兵を始めた経緯

  • 前職でサードパーティがサイト全体に与える影響(CWV)について調査
    • とにかくいろんなサイトを外部から計測
    • 真の問題を特定しても「弊社とは無関係」で打ち返す以外なく、歯痒い
  • コスパよく直せる部分をみんな放置している!!!
    • 非機能要件の優先度が上がらないのはわかるが、「完全に無視」 が多数
  • 2024~ フリーランスで一ヶ月で直して回る

今日の発表内容

  • 現代のウェブパフォーマンス
  • 計測方法/計測手順
  • 事例紹介: 現場では何が起きていたか

現代のウェブパフォーマンス

Web Vitals / Core Web Vitals | 2021~

ウェブで優れたユーザー エクスペリエンスを実現するために重要と考えられる品質シグナルについて、統一的な目安を提供する Google による取り組み
Web Vitals | Articles

ざっくり WebVitals の指標

  • FCP: First Contentful Paint
    • 最初に意味のあるコンテンツが描画されるまでの時間
  • LCP: Largest Contentful Paint
    • 最終的に表示されるコンテンツが描画されるまでの時間
  • TBT: Total Blocking Time
    • 初期化までにユーザー操作をブロックした時間
  • CLS: Cumulative Layout Shift
    • コンテンツが確定するまでにレイアウトシフトが発生した数/割合
  • INP: Interaction to Next Paint
    • ウェブサイトがユーザー操作に応答するまでの時間

なんで WebVitals を見るのか

  • 定量化されたUX指標として便利
    • 初期リクエスト(HTML)だけではエンドユーザー体験を計測できない
    • 計測方法/ツールが公開されており、手元で再現できる
  • サーバーサイド視点の RUM/APM/OpenTelemetry とは別の視点
    • 動的リクエスト/静的アセット/CPU評価のラウンドトリップ結果
  • 万能な指標ではない
    • 本当はウェブサイトに応じた適切な指標があるはずだが、議論の土台にはなる
    • (本音: Google クローラに都合がいい指標だとは思う)

フィールドデータ vs ラボデータ

  • フィールドデータ
    • 実際のエンドユーザー環境からサンプリングしたもの
    • SEO に使われる Core Web Vitals
  • ラボデータ
    • 計測目的でエンドユーザー環境をエミュレートしたもの
    • 開発者の手元の DevTools | Lighthouse はあくまでラボデータなのに注意

Web Vitals の測定を始める | web.dev
ラボデータと実環境データに相違が生じる理由(および対処方法) |  web.dev

WebVitals を計測しよう!

手順

  • 心構え
  • フィールドデータを確認
  • プロダクション計測
    • 手元のラボデータがフィールドデータを再現しているかを確認
    • Lighthouse でマクロ傾向, DevTools でブレークダウン
  • 修正作業
    • ローカルビルド環境への計測、で問題傾向を再現
    • ソースコード書き換えて、 DevTools 上の計測値を比較

WebVitals 計測方法

Page Speed Insights

alt text

ブレークダウン / フィールドデータ => Lighthouse

  • 今わかってる問題
    • ドメイン全体としてのフィールドデータの傾向
  • 次に欲しいデータ
    • URL ごとの Lighthouse レポート
    • できれば異なる時間帯でほしい。混雑時間で傾向が変わる

DevTools > Lighthouse

  • Chrome DevTools 上で Web Vitals を計測するツール
  • 低速環境をエミュレーション

DevTools > Lighthouse > Performance Result

Lighthouse の診断を読む

  • スコアを下げてる問題一覧
  • 調査にきっかけにはなる
  • 問題の根深さはそれぞれ
  • 問題解決後に見返すのがおすすめ

DevTools > Lighthouse > See calcurator

※重みは定期的に見直される

ブレークダウン: Lighthouse から DevTools Perfomance

  • 今わかってること
    • フィールドデータの傾向
    • 手元の Lighthouse のスコア/診断結果 (ラボデータ)
  • 次に欲しいデータ
    • 何の処理が Lighthouse の減点を発生させているか?

DevTools > Performance で Lighthouse 計測環境を再現

末尾の Lighthouse のエミュレーション数値を確認

DevTools > Performance

リロードしながら計測する

DevTools > Performance

  • とにかく情報量が多い
    • 主に Main と Network が大事
  • 左上(起点)から右下(結果)に読む
  • 「何が確定したら、次の処理に始まるんだっけ?」
    • 例: POST /api/auth => POST /api/profile => JS: render(...)

DevTools > Performance 計測結果

DevTools > Performance > Main

UIスレッドで起きるイベントのタイムライン

DevTools > Performance > Main

処理タスクの処理時間の内訳

DevTools > Performance > Main

関数単位の処理時間の内訳

DevTools > Network

Performance で処理順をしてから、ウォーターフォールを確認

最後の最後に、コード修正

  • 手元に素材が揃っていることが前提
    • 問題が再現するラボデータの設定
    • DevTools 上で問題の処理が特定できている
  • ローカル開発環境で問題を再現を試みる
    • 手元で同じ問題を発生させて、それを潰す
    • 再現しない場合、モックを作ったり遅延コードを挿入する
  • (コード修正方法は言語/フレームワーク依存なので略)
  • デプロイ後、本番環境の Lighthouse でスコア変動を確認

計測方法: まとめ

  • とにかく思い込みを捨てる
  • 段階的にブレークダウン
    • フィールドデータからスコア傾向を確認
      • LCP/CLS 何点?
    • Lighthouse でスコア内訳を確認
      • フィールドデータを再現してる?
      • LCP として判定された要素は?
    • DevTools で問題の発生経路を追跡
      • LCP 要素を表示するまでに、どんな実行パスを通るか
      • ソースコード上どこに該当するか

とにかく DevTools と仲良くなるのが大事

事例紹介: 現場では何が起こっているか

現場では何が起こっているか

  • ここまで計測して何がわかるかを紹介した
  • 2024/08~ で直近で請け負った仕事を解説
  • 免責
    • 問題が発生していることは、その会社に問題があるということを意味しない
    • むしろ課題を認識でき、解消可能と認識できている時点で、上澄み

補足: CI/CD 改善もやる

  • フロントエンドの経験則
    • アセットのビルド時間は、バンドルサイズに比例しがち
    • 最終バンドルサイズ≒UXパフォーマンス(主にTBT)
  • 開発環境のビルド速度は、最終的な変更リードタイムに直結

事例紹介

※許諾を得て公開

事例: 株式会社ギックス様(1)

  • 主要な構成
    • Next.js
    • PandaCSS
    • npm workspaces
  • 発生課題:
    • Vercel CI で next build が遅い
    • 確率的に OOM で落ちる

事例: 株式会社ギックス様(2)

  • 調査
    • Next.js の .next/trace のログを調査
    • どうやら postcss の処理時間が長そう
  • 問題の特定
    • 共通設定の PandaCSS が、プロジェクト全体の **/*.(ts|tsx) を走査
    • 分割されたNext.jsアプリケーション単位(packages/x-app) 関わらず、あらゆるビルドフェーズで全ファイルを静的解析
config/
  panda-config.ts     # include: ['**/*.(ts|tsx)']
packages/
  shared/src/**.ts    # Inline CSS を含まないが、常に解析される
  x-app/**.tsx        # ビルド時に x-app 以外の shared も y-app もスキャン
  y-app/**.tsx        # 同上

事例: 株式会社ギックス様(3)

  • 発生過程の調査
    • git log と PR から変更前後の意図を調査
    • おそらく全アプリケーションで共通UIコンポーネントを切り出したかった
    • 即座に問題にならず、プロジェクトの行数が増えることで顕在化
  • 教訓
    • 最終的な修正は一行だが、起きてた問題は甚大
    • 問題が仕込まれたタイミングと、実際に火を吹いたタイミングがズレていたので、トップダウンの再計測でないと発見できなかった

事例: 株式会社ハウテレビジョン様(1)

  • 技術構成
    • Next.js
    • Hasura
    • GraphQL
  • 依頼
    • Lighthouse の点が悪くなってしまっており、改善したい

事例: 株式会社ハウテレビジョン様(2)

  • 計測
    • Chrome Dev Tools で修正
  • 問題
    • GraphQL のリクエストが多すぎる!(150>)

alt text

事例: 株式会社ハウテレビジョン様(3)

  • 発生の過程
    • GraphQL のスキーマから react-query のTSコードを生成したが、その多用でリクエスト断片化
      • useUserProfile(), useUserName() 等がクライアントN+1に
    • 無限スクロールの仕様が、バッチ可能なはずの処理を複雑化
  • 修正
    • GraphQL Client を改造し、時系列でクライアントバッチ
  • 教訓
    • GraphQL はコロケーション技術なのに、それを活かせない技術を選択していた
    • 無限スクロールの複雑さのメンタル負荷が、他の問題を見えなくしていた
full

事例紹介: サードパーティスクリプト提供会社(1)

  • 技術スタック
    • サードパーティスクリプト
    • Chrome Extension
  • 依頼:
    • ブラウザ内の高速なテキスト探索を実装してほしい
    • Playwright から抽出したモジュールを使っている

事例紹介: サードパーティスクリプト提供会社(2)

  • 発生していた問題
    • DOM木構造を全探索してテキストをインデックス (16ms)
      • [...document.querySelectorAll()].forEach(...)
    • CPU処理: lodash.isElement(el) が(直感に反して)重い (33ms)
      • el instanceof Element ではなく, isPlainObject(el) && ...
    • MutationObserver でDOM変更が起きるたびに↑が発生

alt text

事例紹介: サードパーティスクリプト提供会社(3)

  • 問題の修正
    • インデックス対象を事前に絞り込んだテキストに限定: (16s -> 1ms)
    • isElement を素朴に再実装(33ms -> 0.5ms)
  • 問題の発生過程
  • 学び
    • よく使われてる OSS でも、常にパフォーマンスが良いわけではない
    • ブラウザのテキスト検索(Ctrl-F) はよく出来てる...

事例:株式会社スタディスト様

  • 技術構成
    • Vue.js
    • Rails
    • OpenAPI/SwaggerUI
  • 依頼:
    • 社内にフロントエンドの専門家がおらず、技術スタックを更新するのが不安

事例: 株式会社スタディスト様(2)

  • 調査
    • 自分ならこうする、という視点でリファクタを試行
    • 結果、 何を更新してもツールチェインが連鎖的に壊れる
      • 推移的依存関係が裏でデッドロック
    • 特定状況のみで動く細かいハックが相互に絡まっていた
      • docker volume / セットアップ手順の副作用に依存したパス解決
  • 修正
    • 短期では直せないので、問題解消のためのロードマップを作成
    • ハックが横行する原因だった Rails 内 Node.js を別コンテナに分離

事例: 株式会社スタディスト様(3)

  • 発生原因の調査
    • 最初に作ったスタックを様々な事情で更新できなかった
      • 社内事情によるリソース投下判断
      • メンバー移動もあった
  • 学び
    • 放置されたレガシー同士で環境を固定する引力が働いた

事例: LAPRAS株式会社様(1)

  • YouTube Live の公開勉強会形式で開催
  • ソースコード無しの外部の監視のみで問題を特定していく

事例: LAPRAS株式会社様(2)

  • 見つかった問題
    • main要素のロード開始前の初期 viewport に footer が含まれた
      • header
      • main (lazy)
      • footer
    • 優先順位が低いはずの footer の構成要素が優先的にロード(CLS)
  • 修正
    • 初期表示に footer を非表示にすると、画面チラツキとCLSスコアが改善

パフォーマンス傭兵通しての学び

OSS ライブラリへの過度な信頼

  • 「よく使われているライブラリだから問題ないはず」からの思考停止
    • 今回だと lodash/playwright/swagger/react-query
  • ライブラリの設計思想を汲み取らないままハックすると、何かしら起きる

非直感的なボトルネック

  • 事前のボトルネック予想は外れる
    • 再計測するところからスタート
    • ドメイン知識はあまり役に立たない(=> ソースコード不要)
  • コード量と発生する問題の大きさが、事前の予想に反する
    • 1行のコードが致命傷なこともあるし、1万行が負荷にならないこともある
    • 開発者は実装難易度によるメンタル負荷がバイアスになっている
    • 「隣のプロジェクトの人に計測してもらう」のがいいかも
  • 傾向
    • 「問題の深刻さ」はプロジェクトのコード行数に比例していない
    • 「問題の発生確率」が、コード行数に比例している傾向はありそう

開発組織文化の問題

  • 「ハック」文化の行き過ぎ (ベンチャーに多い)
    • 直近の問題を踏み倒すために、安易なハック(モンキーパッチ等)が横行
    • 一度でもライブラリ警告を無視しているプロジェクトは、大抵全部を無視
    • 「悪い習慣/手癖」による累積的な問題は、発火すると即座に修正できない
    • 「標準に従って、ここでは何も作らない」という判断が理想
  • ビジネス vs 開発の風通しの悪さ (大企業に多い)
    • 「動かせない悪い仕様」がダーティハックを誘発
    • 開発側から「実装上の痛みがない仕様」を提案できないと、落とし所を作れない

最後に

  • とにかく雑でも計測してみることが大事
  • 自分のバイアス
    • 今回の内容はモダンフロントエンド系に偏っている
      • Node.js/React の人と認識されているので...
    • 「遅いから見て」と言われるものは、 Wordpress | jQuery が多い
      • 問題のあるテンプレートを使いまわしてるWeb制作が多そう。。。
  • 本当に倒すべき対象は WebFonts …

おわり

CRUX