パフォーマンス最適化
実際のユーザーシグナルに基づくエビデンス主導のパフォーマンス最適化。読み込み速度、ランタイム応答性、リソース配信に焦点を当てます。
動作の仕組み
- ページが実行可能な場合は、測定ワークフローを読み、フィールド+ラボのベースラインを確立してから編集を開始します。
- 悪化している実際のユーザーのCore Web Vitalsを優先します。DevToolsパフォーマンストレースとその焦点を絞ったインサイトを使用して原因を特定します。
- 測定されたボトルネックに関連するコードまたはアセットのみを検査し変更します。
- 同等のラボ測定を再実行し、前後の値、条件、不確実性を報告します。フィールド検証は、十分な新しいユーザーデータが到着するまで保留されます。
実行可能なページが存在しない場合は、静的インスペクションを実行しますが、発見事項は仮説と呼び、測定された回帰とは呼びません。各影響度の高い仮説を検証できるコマンドまたはブラウザワークフローを含めてください。
パフォーマンストレースを記録し、焦点を絞ったインサイトを公開するブラウザツールを推奨します。Chrome DevTools MCPでは、performance_start_trace と performance_analyze_insight を使用してください。パフォーマンス以外のLighthouseカテゴリをカバーする lighthouse_audit を経由してパフォーマンスをルーティングしないでください。
初期パフォーマンス予算
予算は、製品のターゲットデバイス、ネットワーク、ページタイプ、ユーザージャーニーを反映する必要があります。以下の値は、典型的なコンテンツページまたはコマースページの初期ガードレールであり、普遍的な合格/不合格基準ではありません。既存のプロジェクト予算が定義されている場合は、それを維持してください。
| リソース | 予算 | 根拠 |
|---|---|---|
| 総ページサイズ | < 1.5 MB | 制約のあるターゲットネットワークでの転送時間とデータコストを制限します。代表的なページで調整してください。 |
| JavaScript (圧縮後) | < 300 KB | 解析と実行コストを保護します。 |
| CSS (圧縮後) | < 100 KB | レンダリングブロッキング作業を制限します。 |
| 画像 (ファーストビュー) | < 500 KB | 可能性のあるLCPリソースを保護します。 |
| フォント | < 100 KB | クリティカルなフォント転送を制限します。 |
| サードパーティ | < 200 KB | プロダクトの制御外のコードを制限します。 |
クリティカルレンダリングパス
サーバーレスポンス
- TTFB < 800ms。 Time to First Byteは高速であるべきです。CDN、キャッシュ、効率的なバックエンドを使用してください。
- 圧縮を有効にする。 テキストアセットにはGzipまたはBrotli。Brotli推奨(15-20%小さい)。
- HTTP/2 または HTTP/3。 多重化により接続オーバーヘッドを削減します。
- エッジキャッシング。 可能な場合はHTMLをCDNエッジでキャッシュします。
- 測定されたドキュメントレイテンシにはEarly Hints(HTTP 103)を検討する。 トレースがHTML生成の遅延と安定したクリティカルサブリソースを示している場合は、同じリクエストからの通常の最終レスポンスの前に、
Linkヘッダーを含む中間103レスポンスを送信します。HTTP/2以降を使用してください。CDNは、以前の200レスポンスのLinkヘッダーから103を合成するか、オリジン/エッジハンドラーが直接それを送出できます。サポートされていないクライアントは最終レスポンスに進みますが、現在のブラウザとインフラストラクチャのサポートを確認してください。ヒントは、証明されたクリティカルなプリロードまたはプリコネクトに限定してください:不正確なヒントは帯域幅を無駄にします。Cloudflareは、人工的な画像主体のテストで20〜30%のLCP改善を報告しています。これはベンダーのケーススタディとして扱い、期待される改善として扱わず、自身の結果を測定してください。MDNの103実装例とCloudflareの研究を参照してください。
リソース読み込み
必要なオリジンへのプリコネクト:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
クリティカルリソースのプリロード:
トレースで発見が遅れることが確認されたリソースのみをプリロードしてください。各プリロードは帯域幅を競合し、不要な高優先度リクエストはLCPを遅らせる可能性があります。
<!-- LCP画像 -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<!-- クリティカルフォント -->
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
可能性の高い次ナビゲーションをプリレンダリングするには、Speculation Rules APIを使用します:
<script type="speculationrules">
{
"prerender": [{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}]
}
</script>
moderate は、eagerモードよりも強い意図シグナルを待ちます。予測ヒット率、転送バイト数、サーバーコストを測定してください。誤ったプリレンダーは、使用されないナビゲーションとほぼ同じです。トレードオフと分析に必要な prerenderingchange ゲーティングについては、core-web-vitals → LCP を参照してください。
非クリティカルCSSの遅延読み込み:
<!-- クリティカルCSSをインライン化 -->
<style>/* ファーストビューのスタイル */</style>
<!-- 非クリティカルCSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
JavaScript最適化
非必須スクリプトの遅延読み込み:
<!-- パーサーブロッキング (回避) -->
<script src="/critical.js"></script>
<!-- 遅延 (推奨) -->
<script defer src="/app.js"></script>
<!-- 非同期 (独立したスクリプト用) -->
<script async src="/analytics.js"></script>
<!-- モジュール (デフォルトで遅延) -->
<script type="module" src="/app.mjs"></script>
コード分割パターン:
// ルートベースの分割
const Dashboard = lazy(() => import('./Dashboard'));
// コンポーネントベースの分割
const HeavyChart = lazy(() => import('./HeavyChart'));
// 機能ベースの分割
if (user.isPremium) {
const PremiumFeatures = await import('./PremiumFeatures');
}
ツリーシェイキングのベストプラクティス:
// ❌ ライブラリ全体をインポート
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 必要なものだけをインポート
import debounce from 'lodash/debounce';
debounce(fn, 300);
画像最適化
フォーマット選択
| フォーマット | ユースケース | ブラウザサポート |
|---|---|---|
| AVIF | 写真、最高の圧縮率 | 92%以上 |
| WebP | 写真、優れたフォールバック | 97%以上 |
| PNG | 透明度のあるグラフィック | ユニバーサル |
| SVG | アイコン、ロゴ、イラスト | ユニバーサル |
レスポンシブ画像
<picture>
<!-- モダンブラウザ向けAVIF -->
<source
type="image/avif"
srcset="hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<!-- WebPフォールバック -->
<source
type="image/webp"
srcset="hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<!-- JPEGフォールバック -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
width="1200"
height="600"
alt="ヒーロー画像"
loading="lazy"
decoding="async">
</picture>
LCP画像の優先順位付け
<!-- ファーストビューのLCP画像: eager読み込み、高優先度 -->
<img
src="hero.webp"
fetchpriority="high"
loading="eager"
decoding="sync"
alt="ヒーロー">
<!-- ファーストビュー以下の画像: 遅延読み込み -->
<img
src="product.webp"
loading="lazy"
decoding="async"
alt="商品">
フォント最適化
読み込み戦略
/* システムフォントスタックをフォールバックとして */
body {
font-family: 'Custom Font', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
}
/* 不可視テキストの防止 */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* または非クリティカルにはoptional */
font-weight: 400;
font-style: normal;
unicode-range: U+0000-00FF; /* ラテン文字にサブセット化 */
}
クリティカルフォントのプリロード
<link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>
バリアブルフォント
/* 複数のウェイトの代わりに1ファイル */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
キャッシュ戦略
Cache-Controlヘッダー
# HTML (短いまたはキャッシュなし)
Cache-Control: no-cache, must-revalidate
# ハッシュ付き静的アセット (不変)
Cache-Control: public, max-age=31536000, immutable
# ハッシュなし静的アセット
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
# APIレスポンス
Cache-Control: private, max-age=0, must-revalidate
サービスワーカーキャッシュ
// 静的アセットはキャッシュファースト
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image' ||
event.request.destination === 'style' ||
event.request.destination === 'script') {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('static-v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
}
});
ランタイムパフォーマンス
レイアウトスラッシングの回避
// ❌ 複数のリフローを強制
elements.forEach(el => {
const height = el.offsetHeight; // 読み取り
el.style.height = height + 10 + 'px'; // 書き込み
});
// ✅ 読み取りをバッチ処理し、次に書き込みをバッチ処理
const heights = elements.map(el => el.offsetHeight); // すべて読み取り
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // すべて書き込み
});
高コスト操作のデバウンス
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
// スクロール/リサイズハンドラをデバウンス
window.addEventListener('scroll', debounce(handleScroll, 100));
requestAnimationFrameの使用
// ❌ カクツキの原因になる可能性あり
setInterval(animate, 16);
// ✅ ディスプレイリフレッシュと同期
function animate() {
// アニメーションロジック
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
長いリストの仮想化
// 100アイテムを超えるリストの場合、表示されているアイテムのみレンダリング
// react-window、vue-virtual-scrollerなどのライブラリ、またはネイティブCSSを使用:
.virtual-list {
content-visibility: auto;
contain-intrinsic-size: 0 50px; /* 推定アイテム高さ */
}
View Transitionsによるスムーズなナビゲーション
View Transitions API を使用すると、ブラウザは単一のGPUコンポジットスナップショットを使用して2つのDOM状態間をクロスフェード(またはカスタムアニメーション)できます。ダブルレンダリングやレイアウトスラッシングは発生せず、スナップショットはCLSにカウントされません。
同一ドキュメント (SPAスタイル) — Baseline 2026:
// ビューを切り替えるDOMミューテーションをラップする
function navigate(newView) {
if (!document.startViewTransition) return swapDOM(newView);
document.startViewTransition(() => swapDOM(newView));
}
クロスドキュメント (MPAスタイル) — Chromium安定版、他ではプログレッシブエンハンスメント:
/* 送信元と送信先の両方のページで */
@view-transition { navigation: auto; }
これで統合は完了です。同一オリジンのナビゲーションが自動的にフェードするようになります。特定の要素を共有要素トランジション(例:サムネイルがヒーローに拡大する)にオプトインするには、それらに一致するview-transition-nameを指定します:
.product-thumb[data-id="42"], .product-hero { view-transition-name: product-42; }
これをSpeculation Rules(上記)と組み合わせると、インスタントでアニメーション化されたナビゲーションを実現できます。
サードパーティスクリプト
読み込み戦略
// ❌ メインスレッドをブロック
<script src="https://analytics.example.com/script.js"></script>
// ✅ 非同期読み込み
<script async src="https://analytics.example.com/script.js"></script>
// ✅ インタラクションまで遅延
<script>
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
const script = document.createElement('script');
script.src = 'https://widget.example.com/embed.js';
document.body.appendChild(script);
observer.disconnect();
}
});
observer.observe(document.querySelector('#widget-container'));
});
</script>
ファサードパターン
<!-- インタラクションまで静的なプレースホルダーを表示 -->
<div class="youtube-facade"
data-video-id="abc123"
onclick="loadYouTube(this)">
<img src="/thumbnails/abc123.jpg" alt="ビデオタイトル">
<button aria-label="ビデオを再生">▶</button>
</div>
測定
URLが実行可能な場合は常に、測定ワークフローを使用してください。これは、Chrome DevTools MCPルーティング、CrUXとフォールバックソース、再現可能なラボ条件、およびコンパクトなエビデンスフォーマットを定義しています。
| メトリクス | 種類 | 解釈 |
|---|---|---|
| p75でのLCP、INP、CLS | フィールド | ユーザーアウトカムのCore Web Vitals。合格/不合格の優先順位付けに使用します。 |
| トレース内のLCP、CLS | ラボ | 1回のナビゲーションにおける再現可能な診断値。 |
| TBT | ラボ | メインスレッドブロッキングの診断とINPの大まかなプロキシ。フィールドINPではありません。 |
| FCP、Speed Index | ラボ | 読み込みの診断。Core Web Vitalsではありません。 |
生のPerformanceObserverスニペットは現在のブラウザセッションには役立ちますが、それ自体は実際のユーザーデータではありません。ユーザーが本番テレメトリを必要とする場合は、ファーストパーティRUMリファレンスを読み、手作りのメトリクス実装よりもweb-vitalsを優先してください。
参考情報
Core Web Vitals固有の最適化については、Core Web Vitalsを参照してください。


