メインコンテンツにスキップ

Core Web Vitals改善でSEOランキングを向上させた話 - 初心者向けWebパフォーマンス改善ガイド

タグ: 🏷 Core Web Vitals ,SEO

はじめに

2021年以降、GoogleはCore Web Vitalsを検索ランキングの要因として正式に導入しました。つまり、サイトの表示速度やユーザー体験が直接SEOに影響する時代になったのです。

Hugoテーマを作ったものの体感の表示速度が良くなかったので、Core Web Vitalsを改善しました。

パフォーマンススコアを95点以上に向上させた具体的な手順を記録します。

🔍 Core Web Vitalsとは?

3つの重要指標

Core Web Vitalsは、Googleが定義したWebサイトのユーザー体験を測る3つの指標です:

指標測定内容目標値ユーザーへの影響
LCP
(Largest Contentful Paint)
ページの主要なコンテンツ(画像やテキストブロックなど)が表示されるまでの時間2.5秒以下「ページが重い」と感じる
FID
(First Input Delay)
ユーザーが最初に操作(ボタンクリックやテキスト入力など)してから、ブラウザが反応するまでの時間100ms以下「反応が悪い」と感じる
CLS
(Cumulative Layout Shift)
ページ読み込み中に、予期せずレイアウトがずれる量0.1以下「画面がガクガクする」と感じる

なぜ重要なのか?

SEOへの直接影響

  • Google検索ランキングの要因として採用
  • モバイル検索では特に重要視される
  • 競合サイトとの差別化要因になる

ビジネスへの影響

  • ページ離脱率の改善(1秒の遅延で7%の離脱率増加)
  • コンバージョン率の向上(Amazon調べ:100ms改善で1%売上向上)
  • ユーザー満足度の向上

🚨 発見した問題点

問題1: LCP(最大要素の表示)が遅い

現象: ページを開いてからメインコンテンツが表示されるまで3.5秒かかっていました。

原因分析:

<!-- 問題のあるコード -->
<img src="large-image.jpg" alt="記事画像" loading="lazy">
<!-- ↑ 重要な画像なのにlazyloadingしている -->

<link rel="stylesheet" href="main.css">
<!-- ↑ CSSがレンダリングをブロックしている -->

何が悪いのか:

  • Above the fold(スクロールせずに最初に見える画面領域) の重要な画像が後回しで読み込まれる
  • CSSの読み込み完了まで画面が真っ白
  • WebP形式などの軽量フォーマットを使っていない(WebPはJPGやPNGよりもファイルサイズが小さく、画質を保ちながら高速表示が可能です)

ユーザーへの影響:

  • 「このサイト重いな…」と感じて離脱
  • 3秒以上の待機で53%のユーザーが離脱(Google調べ)

問題2: FID(入力反応)が遅い

現象: 検索ボタンをクリックしても150ms以上反応しませんでした。

原因分析:

// 問題のあるコード
input.addEventListener("input", function(e) {
  // 文字を入力するたびに重い検索処理を実行
  const results = searchAllData(e.target.value); // 重い処理
  renderResults(results); // さらに重い処理
});

何が悪いのか:

  • 入力のたびに重い処理が実行される
  • メインスレッド(ブラウザがページの表示やJavaScriptの実行を行う主要な処理の流れ) がブロックされて他の操作ができない
  • 大きなデータセットを一度に処理している

ユーザーへの影響:

  • 「このサイト反応悪いな…」とストレスを感じる
  • タイピングがカクつく
  • ユーザビリティの著しい低下

問題3: CLS(レイアウトシフト)が発生

現象: ページ読み込み中に画面がガクガク動いてスコア0.15でした。

原因分析:

<!-- 問題のあるコード -->
<img src="article-image.jpg" alt="記事画像">
<!-- ↑ width/heightが指定されていない -->

<div class="card-cover">
  <!-- 画像読み込み後にサイズが決まってレイアウトが崩れる -->
</div>

何が悪いのか:

  • 画像のサイズが読み込み後まで分からない
  • フォント読み込み時にテキストサイズが変わる
  • 動的コンテンツの挿入でレイアウトが崩れる

ユーザーへの影響:

  • 読もうとした瞬間に文章が動いて読み直し
  • クリックしようとしたボタンがズレて誤タップ
  • 非常にストレスフルな体験

🛠️ 実施した改善策

改善1: LCP最適化 - 画像とCSSの高速化

重要画像のプリロード実装

解決策: Above the foldの重要画像をプリロード(事前に読み込んでおくこと)します。これにより、ユーザーがページを開いた瞬間に最も重要なコンテンツが素早く表示されます。

<!-- 改善後のコード -->
<head>
  <!-- ホームページの最初の3つの記事画像をプリロード -->
  {{ if .IsHome }}
    {{ range first 3 (where .Site.RegularPages "Type" "posts") }}
      {{ with .Params.cover }}
        {{ $img640webp := $res.Fill "640x360 center webp q85" }}
        <link rel="preload" as="image" href="{{ $img640webp.RelPermalink }}" type="image/webp">
      {{ end }}
    {{ end }}
  {{ end }}
  
  <!-- 記事ページのヒーロー画像をプリロード -->
  {{ if .IsPage }}
    {{ with .Params.cover }}
      {{ $w1200 := $res.Resize "1200x webp q85" }}
      <link rel="preload" as="image" href="{{ $w1200.RelPermalink }}" type="image/webp">
    {{ end }}
  {{ end }}
</head>

改善効果:

  • 重要画像の読み込み時間が50%短縮
  • LCPが3.5秒 → 2.3秒に改善

Critical CSSのインライン化

解決策: Above the foldに必要なCSSをHTMLにインライン化(直接書き込むこと)します。これにより、外部CSSファイルの読み込みを待つことなく、すぐにページの描画を開始できます。レンダリングブロッキング(CSSの読み込みが終わるまでページの表示が止まってしまうこと)を防ぎ、First Paint(最初の描画)を高速化します。

<!-- 改善後のコード -->
<head>
  <!-- Critical CSS inlined for fastest First Paint -->
  <style>
    {{ readFile "themes/karuta/static/css/critical.css" | safeCSS }}
  </style>

  <!-- Non-critical CSS loaded asynchronously -->
  <link rel="preload" href="/css/main.css" as="style" 
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>

改善効果:

  • レンダリングブロッキングの削除
  • First Paint時間が40%短縮

改善2: FID最適化 - JavaScript処理の軽量化

重いタスクの分割処理

解決策: 大きな処理をチャンク(小さな塊)に分けて実行します。これにより、メインスレッドが長時間ブロックされるのを防ぎ、ユーザー操作への応答性を高めます。

// 改善後のコード
function processSearchResults(list, query, callback) {
  const CHUNK_SIZE = 50; // 50件ずつ処理
  let index = 0;
  let results = [];
  
  function processChunk() {
    const endIndex = Math.min(index + CHUNK_SIZE, list.length);
    
    // 現在のチャンクを処理
    for (let i = index; i < endIndex; i++) {
      const item = list[i];
      const score = scoreItem(item, query);
      if (score >= 0) {
        results.push({ item: item, score: score });
      }
    }
    
    index = endIndex;
    
    // 続きがあれば非同期で継続
    if (index < list.length) {
      setTimeout(processChunk, 0); // メインスレッドに制御を戻す
    } else {
      callback(results); // 処理完了
    }
  }
  
  processChunk();
}

改善効果:

  • メインスレッドのブロック時間が80%短縮
  • ユーザー操作の反応性が大幅改善

入力処理のデバウンス

解決策: 連続した入力をデバウンス(まとめて一度だけ処理する技術)します。これにより、ユーザーが文字を入力するたびに検索処理が走るのを防ぎ、無駄な処理を削減します。

// 改善後のコード
let searchTimeout = null;

input.addEventListener("input", function(e) {
  const query = e.target.value;
  
  // 前回のタイマーをクリア
  if (searchTimeout) {
    clearTimeout(searchTimeout);
  }
  
  // 150ms後に検索実行(デバウンス)
  searchTimeout = setTimeout(function() {
    runSearch(query);
  }, 150);
});

改善効果:

  • 無駄な処理が95%削減
  • FIDが150ms → 80msに改善

改善3: CLS最適化 - レイアウト安定化

画像のアスペクト比固定

解決策: 画像読み込み前にアスペクト比(画像の縦横比)を固定し、サイズを確保します。これにより、画像が読み込まれた際にレイアウトがずれるのを防ぎます。

/* 改善後のコード */
.card-cover {
  aspect-ratio: 16 / 9; /* アスペクト比を固定 */
  position: relative;
  overflow: hidden;
}

.card-cover img,
.card-cover picture {
  position: absolute; /* レイアウトシフトを防止 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

改善効果:

  • 画像読み込み時のレイアウトシフトが100%削除
  • 視覚的な安定性が大幅向上

フォント読み込み最適化

解決策: フォントの読み込み方法を最適化し、フォントが切り替わる際に発生するレイアウトシフトを防止します。

/* 改善後のコード */
@font-face {
  font-family: 'System UI Fallback';
  src: local('system-ui'), local('-apple-system');
  font-display: swap; /* レイアウトシフト防止 */
}

body {
  font-family: 
    'System UI Fallback',
    system-ui,
    -apple-system,
    sans-serif;
}

改善効果:

  • フォント読み込み時のレイアウトシフトが削除
  • CLSが0.15 → 0.08に改善

📊 改善結果

パフォーマンス指標の変化

指標改善前改善後改善率目標達成
Performance Score7896+23%OK
LCP3.5秒2.3秒-34%OK
FID150ms80ms-47%OK
CLS0.150.08-47%OK

SEOへの影響

検索結果の改善:

  • Google Search Consoleでの「ウェブに関する主な指標」が全て緑に
  • モバイルでの検索順位が平均2-3位向上
  • オーガニック流入が15%増加

ユーザー体験の改善:

  • ページ離脱率が12%改善
  • 平均セッション時間が20%向上
  • ユーザーからの「サイトが軽くなった」との好評価

🔧 継続的な監視・測定

自動化されたパフォーマンス監視

改善を継続するため、Lighthouseを使った自動監視システムを構築しました。

// scripts/lighthouse-audit.js(一部抜粋)
const CONFIG = {
  thresholds: {
    performance: 90,
    lcp: 2.5,     // 2.5秒以下
    fid: 100,     // 100ms以下  
    cls: 0.1      // 0.1以下
  }
};

async function runAudit(url) {
  const result = await lighthouse(url, options);
  const metrics = extractCoreWebVitals(result);
  
  // しきい値チェック
  const issues = checkThresholds(metrics);
  if (issues.length > 0) {
    console.error('⚠️ Performance issues found:', issues);
    process.exit(1); // CI/CDで失敗させる
  }
  
  return metrics;
}

使用方法

# 依存関係インストール
cd scripts && npm install lighthouse chrome-launcher

# ローカル測定
hugo server
npm run audit:local

# 本番サイト測定  
npm run audit https://yoursite.com

GitHub Actionsでの自動化

# .github/workflows/performance.yml
name: Performance Audit
on: [push, pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build and audit
        run: |
          hugo --minify
          hugo server &
          sleep 5
          cd scripts && npm run audit:local

💡 今すぐできる改善策

1. 画像の最適化(難易度:★☆☆)

<!-- Before: 重い画像 -->
<img src="large-photo.jpg" alt="写真">

<!-- After: 最適化された画像 -->
<picture>
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="写真" width="640" height="360" loading="lazy">
</picture>

効果: LCP改善、CLS防止

2. CSSの非同期読み込み(難易度:★★☆)

<!-- Before: レンダリングブロック -->
<link rel="stylesheet" href="styles.css">

<!-- After: 非同期読み込み -->
<link rel="preload" href="styles.css" as="style" 
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

効果: LCP改善、First Paint高速化

3. 重要リソースのプリロード(難易度:★★☆)

<!-- 重要な画像を事前読み込み -->
<link rel="preload" as="image" href="hero-image.webp" type="image/webp">

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

効果: LCP大幅改善

4. JavaScriptの最適化(難易度:★★★)

// Before: 重い処理
function heavyTask(data) {
  data.forEach(item => processItem(item)); // ブロッキング
}

// After: 軽量化
function optimizedTask(data) {
  const chunks = chunkArray(data, 100);
  
  function processNext(index) {
    if (index >= chunks.length) return;
    
    chunks[index].forEach(item => processItem(item));
    setTimeout(() => processNext(index + 1), 0); // 非ブロッキング
  }
  
  processNext(0);
}

効果: FID改善、ユーザー体験向上

🎯 まとめ

Core Web Vitals改善の重要ポイント

  1. 測定から始める: 現状を把握せずに改善はできません
  2. 優先度をつける: LCP → FID → CLSの順で影響が大きいです
  3. 継続的な監視: 一度の改善では不十分です
  4. ユーザー体験重視: 数値だけでなく実際の使いやすさも考慮

今回の改善で得られた成果

  • SEOランキング向上: Core Web Vitals全指標で目標達成
  • ユーザー体験改善: 離脱率12%改善、セッション時間20%向上
  • 競合優位性確保: パフォーマンススコア96点で業界上位レベル
  • 継続的品質保証: 自動監視システムで品質維持

参考資料