web-performance-web-performance について
Core Web Vitals、バンドル予算、実行時のパターンを扱う、Web 開発者向けのパフォーマンスガイドです。最適化の前に計測してボトルネックを特定する方針です。
主な機能
- 01Core Web Vitals の指標と改善
- 02バンドル予算と CI チェック
- 03ルート単位のコード分割
- 04長いリストの仮想スクロール
- 05デバウンスと選択的なメモ化
活用シーン
- 01ページ読み込みの遅さの調査
- 02操作遅延や過剰な再レンダリングの特定
- 03バンドルの CI 制限の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-performance-web-performanceインストール後、Claude Code で/web-performance-web-performanceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/code-splitting.mdcore.mdimage-optimization.mdweb-vitals.md詳細
Web パフォーマンスのパターン
クイックガイド: ページの速さを決める数値は3つです。LCP は2.5秒未満、INP は200ミリ秒未満、CLS は0.1未満です。最初の2つに影響するのはバンドルサイズなので、予算を設定しましょう。メインバンドルは gzip 圧縮後で約200 KBを目安にし、ルートごとに分割します。最適化の前に計測し、本番環境でもう一度計測してください。ラボのスコアと実際のユーザーのセッションでは結果が異なるためです。React Compiler が有効な場合、メモ化は自動で行われます。手動で
useMemoを記述するなら、その根拠となるプロファイルが必要です。
詳細なリソース:
- examples/core.md — メモ化、仮想スクロール、デバウンス
- examples/code-splitting.md — 遅延読み込みルート、動的インポート、ツリーシェイキング、予算の適用
- examples/web-vitals.md — LCP、INP、CLS のパターンとフィールド計測
- examples/image-optimization.md — フォーマット、レスポンシブな画像ソース、遅延読み込み
- reference.md — しきい値と予算の表、フォーマットの比較
該当する項目
- 問題がバンドルにある場合 — 初回読み込みが遅い、メインのチャンクが大きい、依存関係が重い場合は、 examples/code-splitting.md を参照してください。
- 問題が実行時にある場合 — リストの動きがぎこちない、操作への反応が遅い、再レンダリングが頻発する場合は、examples/core.md を参照してください。
- まだ何も計測していない場合 — 上記の2つでは対処法が異なり、推測では誤った対処を選んでしまうため、まず計測してください。examples/web-vitals.md を参照してください。
<critical_requirements>
パフォーマンスを最適化する前に
まずプロファイルを取り、変更を加える前にボトルネックを特定してください。 最適化を行うたびにコードの読みやすさは損なわれます。もともと遅くなかったコードを最適化しても、何も改善しません。推測を具体的な目標に変えるのは、プロファイラー、バンドル分析、またはフィールド計測です。
機能を追加する前に予算を文書化してください。 CI に設定されたバンドルサイズの上限と Core Web Vitals の目標値があれば、新たに追加する依存関係を評価する基準になります。後から追加しても、すでに基準をどれだけ超えているかが分かるだけです。
ラボでのテストだけでなく、実際のセッションを計測してください。 ラボの条件はデバイス、ネットワーク、コールドキャッシュがそれぞれ1つだけですが、フィールドデータにはユーザーが実際に使うデバイスの分布が反映されます。重要なページほど、この2つの結果は大きく異なります。
ルートに到達した時点で、そのルートのコードを読み込んでください。 ルート境界での分割は、ほとんどのアプリケーションで最も大きな削減効果が得られる方法です。ユーザーが開かないページのために、すべてのユーザーが読み込みを負担せずに済みます。
</critical_requirements>
自動検出: Core Web Vitals、LCP、INP、CLS、TTFB、バンドルサイズ、バンドル予算、コード分割、遅延読み込み、ツリーシェイキング、メモ化、React Compiler、仮想化、仮想スクロール、デバウンス、スロットル、パフォーマンス予算、フィールド計測、RUM
対象範囲:
- Core Web Vitals:それぞれの指標が計測するものと、値に影響する要因
- バンドル予算と CI での適用
- コード分割、動的インポート、ツリーシェイキング
- レンダリングのコスト:メモ化、仮想化、デバウンス、メインスレッド外での処理
- 画像のサイズとフォーマットの選択
対象外:
- バンドラーの設定 — チャンク分割の戦略と分析結果は、使用するバンドラーによって異なります。このスキルでは、目標とする数値を決めます。
- サーバーの応答時間 — TTFB はここで扱うすべての要素より上流にあり、キャッシュ、圧縮、オリジンのレイテンシーは配信レイヤーの対象です。
- フレームワークレベルのレンダリング — ページをサーバーでレンダリングするか、ストリーミングするか、静的にするかはフレームワークの判断であり、それぞれ LCP に異なる影響を与えます。
- データのフェッチとキャッシュ — 不必要に繰り返されるリクエストはフェッチレイヤーの問題ですが、ここでは INP に影響するものとして現れます。
## 基本方針
パフォーマンス改善が失敗するのは、遅くもなかったものを最適化するか、予算を設定せずに機能をリリースし、あらゆる場所で一斉にページが遅くなってから気づくかのどちらかです。どちらも技術ではなく、計測の失敗です。
そのため、順序は決まっています。まず予算を設定し、次に構築し、それから計測し、計測で特定されたものを最適化します。
<decision_framework>
労力をかけるべきところ
Is the problem measured?
├─ NO → Measure it. A profiler for runtime, a bundle report for size,
│ field data for what users actually experience.
└─ YES → Is it load or interaction?
├─ Load (LCP, first paint) → How big is the initial download?
│ ├─ Over budget → Split by route, defer heavy dependencies,
│ │ drop or replace the largest one
│ └─ Within budget → It is the critical path: preload the LCP image,
│ remove render-blocking resources, cut TTFB
└─ Interaction (INP, jank) → What is holding the main thread?
├─ A long task → Break it up, or move it to a worker
├─ Re-rendering → Profile the tree, then memoise what the profile named
└─ Too many DOM nodes → Virtualise the list
メモ化すべきかどうか: React Compiler を使っている場合、たいていの答えは「コンパイラーがすでに行っています」です。使っていない場合は、変更のない props で頻繁に再レンダリングされ、レンダリングに実際の時間がかかるコンポーネントをメモ化します。それ以外はメモ化しません。比較処理自体にもコストがかかるからです。
仮想化すべきかどうか: およそ 100 行を超えると、コストの原因は DOM になり、仮想化が有効になります。それ以下では逆効果です。また、どちらの場合も、ページ内検索とネイティブのスクロールアンカリングが使えなくなります。
</decision_framework>
主なパターン
パターン 1:バンドルの予算
予算はアーティファクトごとに設定して CI で適用するため、依存関係によってチャンクが倍増した場合、本番環境で判明するのではなく、プルリクエストの段階で失敗します。
export const BUNDLE_SIZE_BUDGETS_KB = {
MAIN_BUNDLE_GZIPPED: 200,
VENDOR_BUNDLE_GZIPPED: 150,
ROUTE_BUNDLE_GZIPPED: 100,
TOTAL_INITIAL_LOAD_GZIPPED: 500,
CRITICAL_CSS_INLINE: 14, // fits in the first TCP round trip
} as const;
数値は好みではなく、低速な接続でのダウンロード時間に基づいています。表とその根拠はreference.mdに、適用方法はexamples/code-splitting.mdに記載されています。
パターン 2:Core Web Vitals
指標ごとに原因は異なるため、「速くする」という単一のタスクで複数の指標が改善することはほとんどありません。
| 指標 | 測定対象 | 主な原因 |
|---|---|---|
| LCP | 読み込み | 最適化されていないヒーロー画像、レンダリングを妨げる CSS や JS、遅い TTFB |
| INP | インタラクティブ性 | メインスレッド上の長時間タスク、JavaScript の過剰な使用 |
| CLS | 視覚的な安定性 | 寸法の指定がない画像、後から挿入されるコンテンツ、フォントの切り替え |
しきい値と対処法はreference.mdに、パターンはexamples/web-vitals.mdに記載されています。
パターン 3:ルート単位のコード分割
各遅延読み込みルートは独自のチャンクになり、そのルートに到達したときに取得されます。
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/dashboard"));
const Reports = lazy(() => import("./pages/reports"));
// Whatever the router hands you, the lazy component renders under one Suspense boundary
<Suspense fallback={<PageLoader />}>
{currentPage === "dashboard" ? <Dashboard /> : <Reports />}
</Suspense>;
ルート、重い機能モジュール、ダイアログ、画面下部のセクションを分割します。画面上部に表示されるコンポーネント、エラーバウンダリー、読み込み状態はメインバンドルに残します。これらを遅延読み込みすると、クリティカルパスに余分なラウンドトリップが加わるためです。
コード全文:examples/code-splitting.md
パターン 4:重い依存関係の読み込みを遅らせる
1 つの操作でしか必要とされない大きなライブラリは、ファイルの先頭ではなく、その操作の内部でインポートします。
// "chart-library" stands for whichever heavy dependency the path actually needs
async function renderChart(container: HTMLElement, data: ChartData) {
const { createChart } = await import("chart-library");
return createChart(container, data);
}
この経路を実行しないユーザーには、ライブラリ全体のサイズ分が節約されます。
パターン 5:戦略的なメモ化
まずプロファイリングします。React Compiler を使っている場合は、ほとんどがすでに自動で処理されています。
// Worth it: sorting thousands of rows on every keystroke elsewhere in the tree
const sortedRows = useMemo(
() => [...rows].sort((a, b) => compareValues(a[sortColumn], b[sortColumn])),
[rows, sortColumn],
);
// Not worth it: the comparison costs more than the work
const doubled = useMemo(() => value * 2, [value]);
コード全文:examples/core.md
パターン 6:長いリストの仮想化
コレクション全体ではなく、表示領域内だけをレンダリングし、リストがどれだけ長くなっても DOM のサイズを一定に保ちます。
const visible = rows.slice(startIndex, endIndex);
<div style={{ height: rows.length * ROW_HEIGHT_PX }}>
<div style={{ transform: `translateY(${startIndex * ROW_HEIGHT_PX}px)` }}>
{visible.map((row) => (
<Row key={row.id} row={row} />
))}
</div>
</div>;
コード全文:examples/core.md
パターン 7:入力に応じた処理のデバウンス
キー入力ごとではなく、入力が止まってからコストの高い処理を実行します。以下の useDebounced は useRef と setTimeout を使った 12 行ほどのコードで、examples/core.mdに全文が記載されています。
const debouncedSearch = useDebounced(performSearch, SEARCH_DEBOUNCE_MS);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
debouncedSearch(event.target.value);
};
最終的な値だけが重要な場合(検索、自動保存、検証など)はデバウンスします。中間の値も重要ですが頻度を抑えたい場合(スクロール、サイズ変更、ポインタートラッキングなど)はスロットリングします。
コード全文:examples/core.md
<red_flags>
注意すべき問題
実行時に問題が発生します:
- 上位に
Suspense境界がない遅延読み込みコンポーネントは、Reactが待機せずにエラーをスローします。 - メモ化フックの依存配列が不安定だと、レンダーごとに新しいオブジェクトやインライン関数が作られ、メモ化が機能せず、比較処理のコストが発生し続けます。
- エラーバウンダリーやローディング状態を遅延読み込みすると、障害時に表示するはずのもの自体がまだダウンロード中になります。
予想外の動作:
React.memoは浅い比較を行うため、レンダーごとに新しいオブジェクトになるプロパティがあると、まったく効果がなくなります。- メモ化には比較処理と参照の保持にコストがかかります。数ミリ秒未満の処理では、かえって損になります。
- 分割すると、チャンク数とランタイムが増えて全体のバイト数は増えますが、それでも初回読み込みでは効果があります。
- ラボスコアと実フィールドのデータでは、測定対象となるユーザー層が異なります。フィールドでINPが赤なのに監査結果が緑になるのは矛盾ではなく、よくあることです。
- 仮想スクロールはページ内検索、画面外の行へのアンカーリンク、ネイティブのスクロール位置復元に支障をきたします。
- 遅延読み込みコンポーネントはサーバーレンダリングされたHTMLに含まれないため、画面上部に表示されるコンテンツは読み込み後に位置がずれます。
- 名前空間インポート(
import _ from "…")もrequire()もツリーシェイキングを妨げます。ディレクトリ全体を再エクスポートするバレルファイルも同様です。 - 圧縮方式を明示しなければ、バジェットは意味をなしません。200 KBのバジェットでも、未圧縮、gzip圧縮、Brotli圧縮ではそれぞれ異なる上限になります。
- AVIFはすべての環境で利用できるわけではありません。WebPまたはJPEGのソースを含まない
<picture>は、一部のユーザー環境で表示に失敗します。