Core Web Vitals最適化実践ガイド2026


GoogleのCore Web Vitalsは、SEOランキングとユーザー体験に直接影響する重要な指標です。2024年のINP導入を経て、2026年現在、これらの指標はさらに重要性を増しています。本記事では、LCP、INP、CLSを実践的に改善する手法を解説します。

Core Web Vitals 2026の概要

3つの主要指標

LCP (Largest Contentful Paint)

  • 最大コンテンツの描画時間
  • 目標: 2.5秒以内
  • ページ読み込みのパフォーマンスを測定

INP (Interaction to Next Paint)

  • インタラクションから次の描画までの時間
  • 目標: 200ms以内
  • 2024年にFIDを置き換え

CLS (Cumulative Layout Shift)

  • 累積レイアウトシフト
  • 目標: 0.1以下
  • 視覚的安定性を測定

LCP(Largest Contentful Paint)最適化

1. 画像最適化

次世代フォーマット使用

<picture>
  <source
    srcset="hero.avif"
    type="image/avif"
  />
  <source
    srcset="hero.webp"
    type="image/webp"
  />
  <img
    src="hero.jpg"
    alt="Hero image"
    width="1200"
    height="600"
    loading="eager"
    fetchpriority="high"
  />
</picture>

レスポンシブ画像

<img
  srcset="
    hero-400.avif 400w,
    hero-800.avif 800w,
    hero-1200.avif 1200w,
    hero-1600.avif 1600w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 80vw,
    1200px
  "
  src="hero-1200.avif"
  alt="Hero image"
  width="1200"
  height="600"
  fetchpriority="high"
/>

Next.js Image最適化

import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority  // LCP要素には必須
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  )
}

2. リソースの優先度制御

<!DOCTYPE html>
<html>
<head>
  <!-- 重要なフォントを事前読み込み -->
  <link
    rel="preload"
    href="/fonts/inter-var.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />

  <!-- 重要なCSSをインライン化 -->
  <style>
    /* Critical CSS */
    body { margin: 0; font-family: 'Inter', sans-serif; }
    .hero { min-height: 100vh; }
  </style>

  <!-- 非重要CSSは遅延読み込み -->
  <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>

  <!-- LCP画像を事前読み込み -->
  <link
    rel="preload"
    as="image"
    href="/hero.avif"
    type="image/avif"
    fetchpriority="high"
  />
</head>
</html>

3. サーバー応答時間の改善

Next.js App Router SSR最適化

// app/blog/[slug]/page.tsx
import { cache } from 'react'

// データフェッチをキャッシュ
const getPost = cache(async (slug: string) => {
  const post = await db.post.findUnique({
    where: { slug },
    select: {
      id: true,
      title: true,
      content: true,
      // 必要なフィールドのみ取得
    },
  })
  return post
})

// Static Site Generation(可能な場合)
export async function generateStaticParams() {
  const posts = await db.post.findMany({
    select: { slug: true },
  })

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

CDNとエッジキャッシング

// Vercel Edge Functions
export const config = {
  runtime: 'edge',
}

export default async function handler(req: Request) {
  const { searchParams } = new URL(req.url)
  const id = searchParams.get('id')

  // エッジでキャッシュ
  const response = await fetch(`https://api.example.com/posts/${id}`, {
    next: { revalidate: 60 }, // 60秒キャッシュ
  })

  const data = await response.json()

  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
    },
  })
}

4. フォント最適化

/* Variable Font使用 */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap; /* フォント読み込み中もテキスト表示 */
  font-style: normal;
}

/* サブセット化 */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
// Next.js Font最適化
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

INP(Interaction to Next Paint)最適化

1. JavaScript実行の最適化

コード分割

// React lazy loading
import { lazy, Suspense } from 'react'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  )
}
// Next.js dynamic import
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(
  () => import('./HeavyComponent'),
  {
    loading: () => <Loading />,
    ssr: false, // クライアントサイドのみ
  }
)

export default function Page() {
  return <DynamicComponent />
}

Web Worker活用

// worker.ts
self.addEventListener('message', (e: MessageEvent) => {
  const { data } = e

  // 重い計算をワーカーで実行
  const result = expensiveCalculation(data)

  self.postMessage(result)
})

function expensiveCalculation(data: number[]): number {
  return data.reduce((sum, num) => sum + Math.sqrt(num), 0)
}
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url))

function processData(data: number[]) {
  return new Promise((resolve) => {
    worker.postMessage(data)
    worker.onmessage = (e) => {
      resolve(e.data)
    }
  })
}

// 使用例
const result = await processData([1, 2, 3, 4, 5])

2. イベントハンドラの最適化

デバウンス・スロットル

// デバウンス(最後のイベントのみ処理)
function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout | null = null

  return (...args: Parameters<T>) => {
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), wait)
  }
}

// スロットル(一定間隔で処理)
function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle: boolean = false

  return (...args: Parameters<T>) => {
    if (!inThrottle) {
      func(...args)
      inThrottle = true
      setTimeout(() => (inThrottle = false), limit)
    }
  }
}

// 使用例
const handleSearch = debounce((query: string) => {
  // API呼び出し
  fetch(`/api/search?q=${query}`)
}, 300)

const handleScroll = throttle(() => {
  // スクロール処理
  updateScrollPosition()
}, 100)

React 19のuseTransition

import { useState, useTransition } from 'react'

function SearchPage() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value
    setQuery(value) // 即座に更新

    // 重い処理を低優先度に
    startTransition(() => {
      const filtered = heavySearchOperation(value)
      setResults(filtered)
    })
  }

  return (
    <>
      <input
        type="text"
        value={query}
        onChange={handleSearch}
        placeholder="検索..."
      />
      {isPending && <Loading />}
      <SearchResults results={results} />
    </>
  )
}

3. Virtual Scrolling

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = React.useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // 推定高さ
    overscan: 5, // バッファ
  })

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <ItemComponent item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

CLS(Cumulative Layout Shift)最適化

1. 画像・動画のサイズ指定

<!-- 必ずwidth/height属性を指定 -->
<img
  src="image.jpg"
  alt="Description"
  width="800"
  height="600"
/>

<!-- アスペクト比を保持 -->
<style>
  img {
    width: 100%;
    height: auto;
  }
</style>
/* CSS aspect-ratio */
.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.video-container iframe {
  width: 100%;
  height: 100%;
}

2. フォント読み込みの最適化

/* フォールバックフォントのサイズ調整 */
@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;
  size-adjust: 107%; /* フォールバックに合わせて調整 */
}
// Next.js Font調整
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true, // 自動調整
})

3. 動的コンテンツのスペース確保

// スケルトンスクリーン
function ArticleCard() {
  const { data, isLoading } = useQuery(['article'], fetchArticle)

  if (isLoading) {
    return (
      <div className="article-card">
        <div className="skeleton skeleton-image" style={{ height: '200px' }} />
        <div className="skeleton skeleton-title" style={{ height: '24px', marginTop: '16px' }} />
        <div className="skeleton skeleton-text" style={{ height: '16px', marginTop: '8px' }} />
      </div>
    )
  }

  return (
    <div className="article-card">
      <img src={data.image} alt={data.title} />
      <h3>{data.title}</h3>
      <p>{data.excerpt}</p>
    </div>
  )
}
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

4. 広告・埋め込みコンテンツ

<!-- 広告スペースを事前に確保 -->
<div class="ad-container" style="min-height: 250px;">
  <div id="ad-slot"></div>
</div>
// Intersection Observer で遅延読み込み
function AdSlot() {
  const ref = useRef<HTMLDivElement>(null)
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true)
          observer.disconnect()
        }
      },
      { rootMargin: '200px' }
    )

    if (ref.current) {
      observer.observe(ref.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <div ref={ref} style={{ minHeight: '250px' }}>
      {isVisible && <AdComponent />}
    </div>
  )
}

計測とモニタリング

Web Vitals計測

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
        <Analytics />
      </body>
    </html>
  )
}

カスタム計測

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

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
  })

  // Beacon API(ページ離脱時も送信)
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', body)
  } else {
    fetch('/analytics', { body, method: 'POST', keepalive: true })
  }
}

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

パフォーマンス予算

// lighthouse-budget.json
{
  "resourceSizes": [
    {
      "resourceType": "script",
      "budget": 300
    },
    {
      "resourceType": "image",
      "budget": 500
    },
    {
      "resourceType": "total",
      "budget": 1000
    }
  ],
  "resourceCounts": [
    {
      "resourceType": "third-party",
      "budget": 10
    }
  ],
  "timings": [
    {
      "metric": "interactive",
      "budget": 3000
    },
    {
      "metric": "largest-contentful-paint",
      "budget": 2500
    }
  ]
}

まとめ

Core Web Vitalsの最適化は継続的な取り組みが必要です。

LCP改善のポイント

  • 画像最適化(次世代フォーマット、レスポンシブ)
  • リソースの優先度制御
  • サーバー応答時間短縮

INP改善のポイント

  • JavaScript実行の最適化
  • イベントハンドラの効率化
  • コード分割とWeb Worker活用

CLS改善のポイント

  • すべてのメディアにサイズ指定
  • フォント読み込み最適化
  • 動的コンテンツのスペース確保

2026年現在、これらの指標はSEOだけでなく、ユーザー体験の質を測る重要な基準となっています。継続的な計測と改善でビジネス成果につなげましょう。

参考リンク