「ユーザーが感じるパフォーマンスを数値化したのがCore Web Vitalsだ」と佐藤CTOは言った。「サーバーサイドのレイテンシだけ見ていては、本当のユーザー体験は分からない。」
1. Core Web Vitals の3指標
| 指標 | 意味 | 良好 | 改善が必要 | 不良 |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | 最大コンテンツの描画時間 | < 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | インタラクション応答性 | < 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | レイアウトのずれ | < 0.1 | 0.1-0.25 | > 0.25 |
LCP の改善ポイント
// LCP に影響する要素
// 1. <img> 要素(最も多い原因)
// 2. <video> のポスター画像
// 3. background-image を持つ要素
// 4. テキストブロック要素
// LCP 最適化チェックリスト
const lcpOptimizations = [
'リソースの発見を早める(preload, fetchpriority="high")',
'TTFB を短縮する(CDN, サーバーサイドキャッシュ)',
'レンダリングブロックリソースを排除する(CSS/JS の遅延読み込み)',
'画像を最適化する(WebP/AVIF, 適切なサイズ, lazy loading は LCP 画像に使わない)',
];
<!-- LCP画像の最適化例 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<img src="/hero.webp" alt="Hero" width="1200" height="600"
fetchpriority="high" decoding="async" />
<!-- LCP画像には lazy loading を使わない! -->
INP の改善ポイント
// INP = イベントハンドラの処理時間 + レンダリング時間
// 200ms以内に次のフレームを描画する必要がある
// 長いタスクを分割する
function processLargeList(items: any[]) {
const CHUNK_SIZE = 50;
let index = 0;
function processChunk() {
const end = Math.min(index + CHUNK_SIZE, items.length);
for (let i = index; i < end; i++) {
processItem(items[i]);
}
index = end;
if (index < items.length) {
// メインスレッドに制御を戻す
requestIdleCallback(processChunk);
}
}
processChunk();
}
CLS の改善ポイント
/* CLS を防ぐ: 画像やiframeに明示的なサイズを指定 */
img, video { aspect-ratio: attr(width) / attr(height); width: 100%; height: auto; }
/* フォントの読み込みによるレイアウトシフトを防ぐ */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* または optional */
size-adjust: 100%;
ascent-override: 90%;
descent-override: 20%;
}
2. 測定ツール
| ツール | 種類 | 用途 |
|---|---|---|
| Lighthouse | ラボデータ | 開発中の品質チェック |
| WebPageTest | ラボデータ | 詳細な描画タイムライン分析 |
| CrUX (Chrome UX Report) | フィールドデータ | 実ユーザーの統計データ |
| web-vitals ライブラリ | フィールドデータ | 自前のRUM収集 |
// web-vitals ライブラリでフィールドデータを収集
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric: { name: string; value: number; id: string }) {
fetch('/api/analytics/web-vitals', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
url: window.location.href,
timestamp: Date.now(),
}),
keepalive: true,
});
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
3. フィールドデータ vs ラボデータ
| 観点 | ラボデータ | フィールドデータ |
|---|---|---|
| 収集方法 | シミュレーション環境で測定 | 実ユーザーのブラウザから収集 |
| 再現性 | 高い(同条件で再測定可能) | 低い(ネットワーク・デバイス多様) |
| 代表性 | 低い(固定条件のみ) | 高い(実際のユーザー分布) |
| 活用場面 | 開発中のデバッグ・最適化 | 本番のSLO監視・レポート |
| ツール例 | Lighthouse, WebPageTest | CrUX, RUM (web-vitals) |
両方を組み合わせることが重要。ラボで問題を発見・修正し、フィールドで効果を検証する。
まとめ
| トピック | 要点 |
|---|---|
| LCP | 2.5秒以内。画像最適化、preload、TTFBの短縮が鍵 |
| INP | 200ms以内。長いタスクの分割、メインスレッドの空き確保 |
| CLS | 0.1以下。明示的サイズ指定、フォント最適化 |
| 測定 | ラボ(Lighthouse)とフィールド(RUM)の両方を活用 |
チェックリスト
- Core Web Vitalsの3指標とその閾値を説明できる
- LCP/INP/CLSそれぞれの改善手法を知っている
- web-vitalsライブラリでRUMデータを収集できる
- ラボデータとフィールドデータの使い分けを理解した
次のステップへ
Core Web Vitalsの基礎を学んだ。次は バンドル最適化とコード分割 で、JavaScriptの配信を最適化しよう。
推定読了時間: 30分