フロントエンド Core Web Vitals 最適化:Lighthouseスコアを50から95+に向上させる実践ガイド

性能优化

なぜ Core Web Vitals が SEO と収益に直接影響するのか

2026年、Google は Core Web Vitals を検索ランキングのコアシグナルとして3年以上採用しています。INP(Interaction to Next Paint)が FID に代わる新しい指標となり、3つの指標——LCP、CLS、INP——がサイトのトラフィックとコンバージョンを直接左右しています。

指標パフォーマンス SEO ランキングへの影響 直帰率 コンバージョン率
優秀(すべて合格) ランキング向上 15-30% 35%未満 3.5%以上
改善必要(一部合格) ランキングに顕著な変化なし 40-60% 1.5-3%
低い(すべて不合格) ランキング低下 20-40% 70%以上 1%未満

ECサイトに関する研究では、LCP を100ms削減するごとにコンバージョン率が0.7%向上し、CLS を0.1低下させるごとにユーザーの滞在時間が15%増加することが示されています。パフォーマンスはもはや技術指標ではなく、ビジネス指標です。


LCP 最適化戦略

LCP(Largest Contentful Paint)は最大コンテンツ要素のレンダリング時間を測定します。2026年の合格基準は2.5秒以内、優秀基準は1.8秒以内です。

画像最適化

画像は通常 LCP 要素であり、最適化の効果が最も顕著です:

<picture>
  <source
    srcset="hero-image.avif?w=800 800w, hero-image.avif?w=1200 1200w, hero-image.avif?w=1600 1600w"
    type="image/avif"
  />
  <source
    srcset="hero-image.webp?w=800 800w, hero-image.webp?w=1200 1200w, hero-image.webp?w=1600 1600w"
    type="image/webp"
  />
  <img
    src="hero-image.jpg?w=1200"
    srcset="hero-image.jpg?w=800 800w, hero-image.jpg?w=1200 1200w, hero-image.jpg?w=1600 1600w"
    sizes="(max-width: 768px) 100vw, 1200px"
    alt="Hero banner"
    width="1200"
    height="600"
    fetchpriority="high"
    decoding="async"
    loading="eager"
  />
</picture>

重要ポイント:AVIF を WebP より優先、WebP を JPEG より優先;fetchpriority="high" で LCP 画像の優先度を向上;widthheight を常に宣言して CLS を防止;LCP 画像には lazy loading を使用しない。

フォント最適化

カスタムフォントの読み込みは LCP 遅延の一般的な原因です:

<link
  rel="preload"
  href="/fonts/inter-var-subset.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>
<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var-subset.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
  }
</style>

font-display: swap でテキストを常に表示;unicode-range サブセット化でフォントサイズを60-80%削減;preload でフォントリクエストを早期に開始。

CSS 最適化

クリティカル CSS をインライン化、非クリティカル CSS を非同期読み込み:

<style>
  /* Critical CSS - <head> 内にインライン化 */
  .hero{display:flex;align-items:center;justify-content:center;min-height:60vh;background:linear-gradient(135deg,#667eea,#764ba2)}
  .hero h1{font-size:clamp(2rem,5vw,4rem);color:#fff;margin:0}
  .hero p{font-size:clamp(1rem,2vw,1.5rem);color:rgba(255,255,255,0.9)}
</style>
<link rel="preload" href="/styles/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles/non-critical.css" /></noscript>

サーバー最適化

CDN + Edge Caching + Streaming の組み合わせ:

// Next.js Streaming SSR の例
export default async function Page() {
  const data = await fetch('https://api.example.com/hero', {
    next: { revalidate: 3600 }
  }).then(r => r.json());

  return (
    <main>
      <Suspense fallback={<HeroSkeleton />}>
        <HeroSection data={data} />
      </Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <DeferredContent />
      </Suspense>
    </main>
  );
}

Edge キャッシュにより LCP リクエストが50ms以内に返却;Streaming により初回描画が遅いデータでブロックされない。


CLS 修正戦略

CLS(Cumulative Layout Shift)は視覚的安定性を測定します。2026年の合格基準は0.1以内、優秀基準は0.05以内です。

レイアウト安定性テクニック

.card {
  contain: layout style paint;
  content-visibility: auto;
  contain-intrinsic-size: 0 320px;
}

.ad-slot {
  min-height: 250px;
  background: #f5f5f5;
}

.skeleton {
  min-height: 200px;
  animation: shimmer 1.5s infinite;
}

contain プロパティでレイアウトへの影響範囲を隔離;content-visibility: auto で画面外コンテンツのレンダリングをスキップ;min-height で動的コンテンツのスペースを確保。

画像サイズ属性

<!-- 誤り:サイズ未指定 -->
<img src="product.jpg" alt="Product" />

<!-- 正しい:サイズを宣言 -->
<img
  src="product.jpg"
  alt="Product"
  width="400"
  height="300"
  style="width: 100%; height: auto;"
/>

<!-- レスポンシブアスペクト比 -->
<img
  src="product.jpg"
  alt="Product"
  style="aspect-ratio: 4/3; width: 100%; height: auto;"
/>

動的コンテンツ処理

function loadComments(postId) {
  const container = document.getElementById('comments');
  // 最小高さを確保
  container.style.minHeight = '300px';

  fetch(`/api/comments/${postId}`)
    .then(r => r.json())
    .then(comments => {
      // 一括更新で複数回のリフローを回避
      const fragment = document.createDocumentFragment();
      comments.forEach(comment => {
        const el = document.createElement('div');
        el.className = 'comment-item';
        el.textContent = comment.text;
        fragment.appendChild(el);
      });
      container.innerHTML = '';
      container.appendChild(fragment);
      container.style.minHeight = '';
    });
}

フォント読み込み CLS 防止

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var-subset.woff2') format('woff2');
  font-display: optional;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

/* size-adjust でフォールバックフォントとカスタムフォントのサイズを統一 */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.06%;
  ascent-override: 90%;
  descent-override: 22%;
}

font-display: optional でフォント交換による CLS を完全に防止;size-adjust でフォールバックフォントとカスタムフォントのサイズを一致。


INP 最適化戦略

INP(Interaction to Next Paint)はインタラクションの応答速度を測定します。2026年の合格基準は200ms以内、優秀基準は100ms以内です。

イベントハンドラ最適化

// 誤り:同期の長いタスクがメインスレッドをブロック
searchInput.addEventListener('input', (e) => {
  const results = heavyFilter(e.target.value, allData); // 500msかかる可能性
  renderResults(results);
});

// 正しい:デバウンス + メインスレッドへの譲渡
searchInput.addEventListener('input', debounce(async (e) => {
  const value = e.target.value;

  // scheduler.yield でメインスレッドに譲渡
  await scheduler.yield();

  const results = heavyFilter(value, allData);
  renderResults(results);
}, 150));

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

メインスレッド管理

async function processLargeDataset(data) {
  const CHUNK_SIZE = 50;
  const results = [];

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);
    results.push(...processChunk(chunk));

    // 50件ごとにメインスレッドに譲渡
    if (i % (CHUNK_SIZE * 10) === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }

  return results;
}

Web Workers での重い計算処理

// main.js
const worker = new Worker('/workers/search-worker.js');

worker.postMessage({ type: 'SEARCH', query: userInput, data: largeDataset });

worker.onmessage = (event) => {
  if (event.data.type === 'SEARCH_RESULT') {
    renderResults(event.data.results);
  }
};

// workers/search-worker.js
self.onmessage = (event) => {
  if (event.data.type === 'SEARCH') {
    const results = performHeavySearch(event.data.query, event.data.data);
    self.postMessage({ type: 'SEARCH_RESULT', results });
  }
};

function performHeavySearch(query, data) {
  return data.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );
}

requestAnimationFrame と Scheduler API

// 2026年推奨:Scheduler API
async function handleScroll() {
  const pendingUpdates = collectScrollUpdates();

  // メインスレッドに譲渡、次のフレーム描画を確保
  await scheduler.yield();

  // 適切なタイミングでビジュアル更新を実行
  requestAnimationFrame(() => {
    applyScrollUpdates(pendingUpdates);
  });
}

// 互換性フォールバック
function yieldToMain() {
  return new Promise(resolve => {
    if ('scheduler' in window && 'yield' in scheduler) {
      scheduler.yield().then(resolve);
    } else {
      setTimeout(resolve, 0);
    }
  });
}

測定とモニタリング体制

Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - name: Lighthouse CI
        uses: treosh/lighthouse-ci-action@v12
        with:
          urls: |
            http://localhost:3000/
          uploadArtifacts: true
          budgetPath: ./lighthouse-budget.json
          configPath: ./lighthouserc.json
// lighthouse-budget.json
[
  {
    "path": "/*",
    "options": {
      "first-contentful-paint": ["warn", { "maxNumericValue": 1800 }],
      "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
      "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
      "interactive": ["warn", { "maxNumericValue": 200 }]
    }
  }
]

web-vitals ライブラリ

import { onLCP, onCLS, onINP } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    url: location.href,
    timestamp: Date.now()
  });

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);

Real User Monitoring (RUM)

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log({
      name: entry.name,
      duration: entry.duration,
      startTime: entry.startTime,
      entryType: entry.entryType
    });

    reportToAnalytics({
      metric: entry.name,
      value: entry.duration,
      path: location.pathname,
      connection: navigator.connection?.effectiveType,
      deviceMemory: navigator.deviceMemory
    });
  }
});

observer.observe({
  type: 'largest-contentful-paint',
  buffered: true
});
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'event', buffered: true });

5つのよくある落とし穴と解決策

落とし穴 1:lazy loading の過剰使用

<!-- 誤り:LCP画像にlazy loadingを使用 -->
<img src="hero.jpg" loading="lazy" alt="Hero" />

<!-- 正しい:LCP画像にeager + fetchpriorityを使用 -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero" />

LCP要素にlazy loadingを使用すると、200-500msの遅延が発生します。

落とし穴 2:フォント CLS の無視

フォント読み込み後のテキストサイズ変化によりレイアウトシフトが発生。常に size-adjust または font-display: optional を使用。

落とし穴 3:サードパーティスクリプトの未最適化

<!-- Partytown でサードパーティスクリプトをWeb Workerに移動 -->
<script type="text/partytown" src="https://analytics.example.com/script.js"></script>
<script>
  partytown = {
    forward: ['dataLayer.push', 'gtag']
  };
</script>

落とし穴 4:動的コンテンツのレイアウトシフト未処理

広告、レコメンドリストなどの非同期コンテンツには min-height を確保しないと、読み込みのたびに CLS が発生。

落とし穴 5:メインスレッドでの重い計算実行

検索、ソート、フィルタリングなどの処理が50msを超える場合は Web Worker に移すべき。そうしないと INP が確実に基準値を超えます。


10のエラートラブルシューティング項目

# 症状 考えられる原因 トラブルシューティング方法
1 LCP > 4s LCP画像が未最適化 DevTools Performance パネルで LCP 要素を確認、画像形式とサイズを検証
2 LCP画像の読み込みが遅い CDNやキャッシュ戦略の欠如 Response Headers の cache-controlcf-cache-status を確認
3 CLS > 0.25 画像に width/height が未指定 Lighthouse CLS 監査項目で具体的なシフト要素を確認
4 CLSシフトがフォント由来 font-display の設定不適切 @font-facefont-display 値を確認
5 INP > 500ms イベントハンドラに長いタスク DevTools Long Animation Frames パネルで特定
6 INPが時々基準値超過 サードパーティスクリプトのブロック Performance パネルでURL別にフィルタリングして特定
7 FCPは正常だがLCPが遅い クリティカルリソースの読み込み順序 Coverage パネルで未使用 CSS/JS を確認、Network パネルでウォーターフォールを確認
8 モバイルのINPがデスクトップより大幅に悪い タッチイベント処理の不備 touchstart/touchend イベントに passive: true があるか確認
9 ページスクロールのカクつき scroll イベント処理が重い requestAnimationFrame スロットリングを使用しているか確認
10 最適化後もLighthouseスコアが不安定 テスト環境の不一致 Lighthouse CI で一貫したテスト条件を確保

実際のケーススタディ:ECサイトトップページを50点から95+に

あるECプラットフォームのトップページ最適化前後の Core Web Vitals データ:

指標 最適化前 最適化後 改善率
Lighthouse スコア 50 96 +92%
LCP 5.2s 1.4s -73%
FID 280ms 45ms -84%
CLS 0.35 0.03 -91%
INP 450ms 85ms -81%
TTFB 1.8s 0.2s -89%
ファーストビュー JS サイズ 850KB 180KB -79%
ファーストビュー CSS サイズ 320KB 28KB -91%

最適化ステップ:

  1. 画像のAVIF移行 + CDN:LCP 5.2s → 3.1s
  2. クリティカルCSS インライン化 + 非クリティカルCSS 非同期化:LCP → 2.3s
  3. フォントサブセット化 + font-display: optional:CLS 0.35 → 0.12
  4. 画像サイズ属性宣言 + min-height 確保:CLS → 0.03
  5. 検索ロジックのWeb Worker移行:INP 450ms → 120ms
  6. scheduler.yield + デバウンス最適化:INP → 85ms
  7. Edge Caching + Streaming SSR:TTFB 1.8s → 0.2s、LCP 最終 1.4s

おすすめツール

最適化プロセスで役立つオンラインツール:

  • JSON フォーマッター — Lighthouse レポートの JSON データをフォーマット・検証し、パフォーマンス問題を迅速に特定
  • Base64 エンコーダー — 小さなアイコンを Base64 インラインに変換し、HTTP リクエスト数を削減
  • ハッシュ計算ツール — 静的アセットのコンテンツハッシュを生成し、正確なキャッシュ無効化戦略を実現

まとめ:Core Web Vitals の最適化は一度きりの作業ではなく、継続的なプロセスです。LCP の画像とフォントの最適化から、CLS のレイアウト安定性の確保、INP のメインスレッド管理まで、各指標には体系的な戦略が必要です。覚えておいてください:パフォーマンス最適化の目標は満点ではなく、ユーザーにスムーズな体験を提供することです。Lighthouse スコアが50から95+に向上すれば、より良い SEO ランキングだけでなく、実際のコンバージョン率の向上も得られます。

ブラウザローカルツールを無料で試す →

#Core Web Vitals#LCP优化#CLS修复#INP优化#性能指标#2026