New Relic Browser監視入門 第9.2章 - Core Web Vitals詳細分析と最適化実践

📖 ナビゲーション

メイン: 第9章 New Relic Browser詳細化
前セクション: 第9.1章 ブラウザ監視の重要性とRUM
次セクション: 第9.3章 SPA・PWA監視とエラー追跡


💡 この章で学べること

Core Web Vitalsは、Googleが定める ユーザーエクスペリエンスの3つの重要指標 であり、SEOランキングの決定要因としても重要視されています。単純にページが速いだけでなく、ユーザーが実際に感じる体験の質を測定する革新的な指標群です。本章では、これらの指標を New Relic Browser で詳細に監視し、データ駆動型の継続的パフォーマンス最適化を実現する実践手法を初心者から上級者まで学べるよう、段階的に詳しく解説します。

学習目標

  • [ ] Core Web Vitals の本質理解:LCP・FID・CLSが重要な理由とビジネスインパクト
  • [ ] New Relic Browser での詳細監視:3つの指標を継続的に追跡する実装方法
  • [ ] LCP最適化の実践:画像・フォント・Critical CSS最適化の具体的手法
  • [ ] FID改善テクニック:JavaScript最適化・Web Workers活用・タスク分割実装
  • [ ] CLS対策の包括実装:レイアウトシフト防止の設計パターンとコード例
  • [ ] 科学的最適化アプローチ:A/Bテストによる効果測定と継続改善システム

期待される成果

本章を完了すると、以下が実現できます:

  • Core Web Vitals 全項目での目標値達成:LCP < 2.5s、FID < 100ms、CLS < 0.1
  • Google PageSpeed Insights スコア90+:検索順位向上とSEO対策完了
  • ユーザー体験の定量的改善:直帰率20%削減、コンバージョン率15%向上
  • 継続的パフォーマンス運用:自動監視・アラート・改善サイクルの確立

9.2.1 Core Web Vitals の基本概念と重要性

Core Web Vitals とは何か

Core Web Vitals は、2021年よりGoogleが Page Experience Update の一部として、検索ランキング要因に組み込んだ3つのユーザーエクスペリエンス指標です。これまでの技術的なパフォーマンス指標とは異なり、実際のユーザーが体感する品質に焦点を当てた革新的な測定基準となっています。

3つの指標の詳細定義

yaml
# Core Web Vitals 指標の詳細
LCP (Largest Contentful Paint):
  定義: "ページの主要コンテンツが読み込まれるまでの時間"
  測定対象: "最大の画像、動画、テキストブロックの表示完了時点"
  目標値: "2.5秒以下(優良)、2.5-4.0秒(改善必要)、4.0秒以上(不良)"
  ビジネス影響: "ページの実用性とコンテンツアクセス性に直結"

FID (First Input Delay):
  定義: "ユーザーの最初の操作に対する応答遅延時間"
  測定対象: "クリック、タップ、キー押下への反応開始時間"
  目標値: "100ms以下(優良)、100-300ms(改善必要)、300ms以上(不良)"
  ビジネス影響: "インタラクティブ性とユーザーフラストレーション軽減"

CLS (Cumulative Layout Shift):
  定義: "ページ読み込み中の予期しないレイアウト変動の累積値"
  測定対象: "要素の位置変更による視覚的安定性の評価"
  目標値: "0.1以下(優良)、0.1-0.25(改善必要)、0.25以上(不良)"
  ビジネス影響: "誤クリック防止とコンテンツ読み込み体験の向上"

Core Web Vitals がビジネスに与える具体的影響

事例研究:ECサイトA社の改善事例

javascript
// 改善前の状況
const beforeOptimization = {
  lcp: "4.2秒",        // 商品画像の読み込み遅延
  fid: "245ms",        // 「カートに追加」ボタンの反応遅延
  cls: "0.28",         // レビューセクションの動的読み込みによるシフト
  conversionRate: "2.1%",
  bounceRate: "68%",
  averageSessionTime: "1.8分"
};

// 最適化実装後の結果
const afterOptimization = {
  lcp: "1.8秒",        // 75%改善(画像最適化・Critical CSS)
  fid: "65ms",         // 73%改善(JavaScript最適化・Web Workers)
  cls: "0.05",         // 82%改善(レイアウト予約・スケルトンスクリーン)
  conversionRate: "3.2%", // 52%向上
  bounceRate: "51%",      // 25%改善
  averageSessionTime: "2.6分" // 44%増加
};

// ビジネス価値の計算
const businessImpact = {
  monthlyRevenue: "従来: $85,000 → 改善後: $129,000 (52%増加)",
  customerExperience: "NPS向上: 32 → 48点 (50%改善)",
  seoRanking: "主要キーワード平均順位: 15位 → 8位",
  developmentROI: "投資: $45,000 → 年間追加売上: $528,000 (ROI: 1,173%)"
};

Google PageSpeed Insights との関係性

Core Web Vitals は Google PageSpeed Insights での パフォーマンススコア算出の主要要素 となっています。従来の技術的指標から、よりユーザー体験重視の評価基準に変化したことを理解する必要があります。

従来のパフォーマンス指標との違い

yaml
# 評価基準の変化
従来の技術指標:
  重視項目: "サーバー応答時間、ファイルサイズ、リクエスト数"
  問題点: "技術的には最適でもユーザー体験が悪い場合がある"
: "HTMLは高速だが、重要なコンテンツ表示が遅い"

Core Web Vitals:
  重視項目: "実際のユーザーが感じる読み込み・操作・安定性"
  利点: "ユーザー体験と技術的最適化の両立が可能"
: "ページ全体は重いが、重要コンテンツは即座に表示"

9.2.2 LCP (Largest Contentful Paint) 最適化の詳細実装

LCP の測定メカニズムと問題の特定方法

LCP は、ページの 最も重要なコンテンツ要素 が完全に表示されるまでの時間を測定します。この「最も重要な要素」の特定と最適化が、効果的なLCP改善の鍵となります。

LCP要素の自動特定と New Relic での監視実装

javascript
// LCP要素の動的検出とNew Relicへの送信
class LCPOptimizer {
  constructor() {
    this.lcpElements = [];
    this.observer = new PerformanceObserver(this.handleLCPEntries.bind(this));
    this.observer.observe({ type: 'largest-contentful-paint', buffered: true });
  }

  handleLCPEntries(entries) {
    // 最新のLCP要素を取得
    const lastEntry = entries.getEntries().pop();
    if (!lastEntry) return;

    const lcpData = {
      // 基本的なLCP情報
      startTime: lastEntry.startTime,
      renderTime: lastEntry.renderTime || lastEntry.loadTime,
      size: lastEntry.size,
      
      // LCP要素の詳細分析
      element: {
        tagName: lastEntry.element?.tagName || 'unknown',
        id: lastEntry.element?.id || '',
        className: lastEntry.element?.className || '',
        src: lastEntry.element?.src || '',
        dimensions: {
          width: lastEntry.element?.clientWidth || 0,
          height: lastEntry.element?.clientHeight || 0
        }
      },
      
      // パフォーマンス分析用の追加データ
      url: lastEntry.url || window.location.href,
      resourceType: this.determineResourceType(lastEntry.element),
      isAboveFold: this.isElementAboveFold(lastEntry.element),
      loadingStrategy: this.getLoadingStrategy(lastEntry.element)
    };

    // New Relicへの詳細データ送信
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('LCPMeasurement', {
        lcpTime: lcpData.startTime,
        lcpElement: lcpData.element.tagName,
        lcpSize: lcpData.size,
        elementId: lcpData.element.id,
        resourceType: lcpData.resourceType,
        isOptimized: lcpData.startTime < 2500, // 2.5秒目標値
        pageType: this.getPageType(),
        deviceType: this.getDeviceType(),
        networkType: this.getNetworkType()
      });
    }

    // 問題のあるLCP要素の自動特定
    if (lcpData.startTime > 2500) {
      this.identifyLCPIssues(lcpData);
    }
  }

  // LCP問題の自動分析
  identifyLCPIssues(lcpData) {
    const issues = [];
    
    // 画像最適化の問題検出
    if (lcpData.resourceType === 'image') {
      if (!lcpData.element.loading === 'lazy' && lcpData.isAboveFold) {
        issues.push({
          type: 'unnecessary_lazy_loading',
          severity: 'high',
          recommendation: 'Remove lazy loading from above-the-fold LCP image'
        });
      }
      
      if (lcpData.element.width > 800 && !lcpData.element.srcset) {
        issues.push({
          type: 'missing_responsive_images',
          severity: 'high', 
          recommendation: 'Implement responsive images with srcset'
        });
      }
    }

    // CSS配信の問題検出
    if (lcpData.startTime > 1500 && !this.hasCriticalCSS()) {
      issues.push({
        type: 'missing_critical_css',
        severity: 'medium',
        recommendation: 'Implement critical CSS for above-the-fold content'
      });
    }

    // New Relicに問題レポートを送信
    if (typeof newrelic !== 'undefined' && issues.length > 0) {
      newrelic.addPageAction('LCPIssuesDetected', {
        issueCount: issues.length,
        issues: JSON.stringify(issues),
        lcpTime: lcpData.startTime,
        urgency: issues.some(i => i.severity === 'high') ? 'high' : 'medium'
      });
    }
  }

  // リソースタイプの判定
  determineResourceType(element) {
    if (!element) return 'unknown';
    
    const tagName = element.tagName.toLowerCase();
    if (tagName === 'img') return 'image';
    if (tagName === 'video') return 'video';
    if (tagName === 'svg') return 'svg';
    if (element.style && element.style.backgroundImage) return 'background-image';
    return 'text-block';
  }

  // Above-the-fold判定
  isElementAboveFold(element) {
    if (!element) return false;
    const rect = element.getBoundingClientRect();
    return rect.top < window.innerHeight;
  }
}

// LCP最適化システムの初期化
document.addEventListener('DOMContentLoaded', () => {
  new LCPOptimizer();
});

画像最適化による LCP 劇的改善手法

画像は最も一般的なLCP要素であり、適切な最適化により劇的な改善が期待できます。以下は包括的な画像最適化戦略です。

次世代画像形式とレスポンシブ画像の実装

html
<!-- 包括的な画像最適化実装例 -->
<picture class="lcp-optimized-image">
  <!-- WebP形式(Chrome、Edge、Firefox対応) -->
  <source
    media="(min-width: 1200px)"
    srcset="
      /images/hero-1920w.webp 1920w,
      /images/hero-1600w.webp 1600w,
      /images/hero-1200w.webp 1200w
    "
    sizes="100vw"
    type="image/webp"
  />
  
  <!-- AVIF形式(最新ブラウザ対応、さらに高圧縮) -->
  <source
    media="(min-width: 1200px)"
    srcset="
      /images/hero-1920w.avif 1920w,
      /images/hero-1600w.avif 1600w,
      /images/hero-1200w.avif 1200w
    "
    sizes="100vw"
    type="image/avif"
  />
  
  <!-- モバイル用の軽量画像 -->
  <source
    media="(max-width: 768px)"
    srcset="
      /images/hero-mobile-800w.webp 800w,
      /images/hero-mobile-600w.webp 600w,
      /images/hero-mobile-400w.webp 400w
    "
    sizes="100vw"
    type="image/webp"
  />
  
  <!-- フォールバック用JPEG -->
  <img
    src="/images/hero-1200w.jpg"
    srcset="
      /images/hero-1920w.jpg 1920w,
      /images/hero-1600w.jpg 1600w,
      /images/hero-1200w.jpg 1200w
    "
    sizes="100vw"
    alt="メイン商品の詳細画像"
    width="1200"
    height="600"
    loading="eager"
    decoding="async"
    fetchpriority="high"
    onload="this.style.opacity=1"
    style="opacity:0;transition:opacity 0.3s ease;"
  />
</picture>

Critical CSS とフォント最適化による LCP 向上

css
/* Critical CSS: Above-the-fold コンテンツの即座表示 */
/* この部分はHTMLの<head>内に直接インライン挿入 */

/* 1. レイアウトの基本構造(CLS対策も兼ねる) */
.hero-section {
  width: 100%;
  height: 100vh;
  min-height: 600px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f8f9fa; /* 画像読み込み前の背景色 */
}

/* 2. LCP要素の事前レイアウト確保 */
.lcp-optimized-image {
  width: 100%;
  max-width: 1200px;
  height: auto;
  aspect-ratio: 2/1; /* 画像の縦横比を事前に確保してCLSを防止 */
}

.lcp-optimized-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* 3. フォント最適化(LCPがテキスト要素の場合) */
@font-face {
  font-family: 'OptimizedMain';
  src: url('/fonts/main-font.woff2') format('woff2'),
       url('/fonts/main-font.woff') format('woff');
  font-display: swap; /* FOUT戦略で即座にフォールバック表示 */
  font-weight: 400 700; /* Variable fontで帯域幅を節約 */
}

/* 4. Above-the-fold コンテンツの基本スタイル */
.hero-title {
  font-family: 'OptimizedMain', sans-serif;
  font-size: clamp(2rem, 5vw, 4rem);
  font-weight: 700;
  line-height: 1.2;
  color: #2c3e50;
  text-align: center;
  margin: 0 0 1rem 0;
  opacity: 1;
  transform: translateY(0);
  transition: none; /* Critical CSS段階では不要なアニメーションを無効化 */
}

.hero-subtitle {
  font-family: 'OptimizedMain', sans-serif;
  font-size: clamp(1rem, 2.5vw, 1.5rem);
  font-weight: 400;
  line-height: 1.5;
  color: #7f8c8d;
  text-align: center;
  margin: 0;
  max-width: 600px;
}

高度な画像配信最適化システム

javascript
// 画像配信の自動最適化システム
class ImageOptimizationManager {
  constructor() {
    this.deviceCapabilities = this.detectDeviceCapabilities();
    this.networkCondition = this.detectNetworkCondition();
    this.setupImageObserver();
  }

  // デバイス性能の自動検出
  detectDeviceCapabilities() {
    const capabilities = {
      // CPU性能の推定(JavaScriptベンチマーク)
      cpuScore: this.measureCPUPerformance(),
      
      // メモリ情報(利用可能な場合)
      deviceMemory: navigator.deviceMemory || 4,
      
      // GPU加速対応
      hardwareAcceleration: this.detectGPUAcceleration(),
      
      // 画面解像度とデバイスピクセル比
      screenInfo: {
        width: window.screen.width,
        height: window.screen.height,
        devicePixelRatio: window.devicePixelRatio || 1
      }
    };
    
    return capabilities;
  }

  // ネットワーク状況の動的検出
  detectNetworkCondition() {
    const connection = navigator.connection || 
                     navigator.mozConnection || 
                     navigator.webkitConnection;
    
    if (connection) {
      return {
        effectiveType: connection.effectiveType, // '4g', '3g', '2g', 'slow-2g'
        downlink: connection.downlink,           // Mbps
        rtt: connection.rtt,                     // ms
        saveData: connection.saveData || false   // データセーバーモード
      };
    }
    
    // フォールバック:接続速度の動的測定
    return this.measureNetworkSpeed();
  }

  // 画像配信の動的最適化
  optimizeImageDelivery(imageElement) {
    const optimizationStrategy = this.calculateOptimizationStrategy();
    
    // 1. 画像形式の最適選択
    const optimalFormat = this.selectOptimalImageFormat();
    
    // 2. 画像サイズの動的調整
    const optimalDimensions = this.calculateOptimalDimensions(imageElement);
    
    // 3. 配信タイミングの最適化
    const loadingStrategy = this.determineLoadingStrategy(imageElement);
    
    // 最適化された画像URLの生成
    const optimizedImageUrl = this.buildOptimizedImageUrl({
      originalSrc: imageElement.dataset.originalSrc,
      format: optimalFormat,
      width: optimalDimensions.width,
      height: optimalDimensions.height,
      quality: optimizationStrategy.quality,
      progressive: optimizationStrategy.progressive
    });
    
    // New Relic に最適化情報を送信
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('ImageOptimizationApplied', {
        imageId: imageElement.id || 'unknown',
        originalFormat: this.getImageFormat(imageElement.dataset.originalSrc),
        optimizedFormat: optimalFormat,
        qualityReduction: optimizationStrategy.quality < 90,
        sizeReduction: this.calculateSizeReduction(imageElement, optimalDimensions),
        networkType: this.networkCondition.effectiveType,
        deviceScore: this.deviceCapabilities.cpuScore
      });
    }
    
    return optimizedImageUrl;
  }

  // 最適化戦略の算出
  calculateOptimizationStrategy() {
    const strategy = {
      quality: 90,        // デフォルト品質
      progressive: true,   // プログレッシブJPEG
      lazyLoad: true      // 遅延読み込み
    };

    // ネットワーク状況に基づく調整
    if (this.networkCondition.effectiveType === '2g' || 
        this.networkCondition.effectiveType === 'slow-2g') {
      strategy.quality = 60;
      strategy.progressive = true;
    } else if (this.networkCondition.effectiveType === '3g') {
      strategy.quality = 75;
    }

    // データセーバーモード対応
    if (this.networkCondition.saveData) {
      strategy.quality = Math.min(strategy.quality, 50);
    }

    // デバイス性能に基づく調整
    if (this.deviceCapabilities.deviceMemory < 2) {
      strategy.quality = Math.min(strategy.quality, 70);
    }

    return strategy;
  }
}

// 画像最適化システムの初期化とLCP監視連携
document.addEventListener('DOMContentLoaded', () => {
  const imageOptimizer = new ImageOptimizationManager();
  
  // LCP画像の特別処理
  const lcpImages = document.querySelectorAll('img[data-lcp="true"]');
  lcpImages.forEach(img => {
    // LCP画像は最高優先度で処理
    img.loading = 'eager';
    img.fetchPriority = 'high';
    
    // 最適化された画像URLを適用
    const optimizedUrl = imageOptimizer.optimizeImageDelivery(img);
    img.src = optimizedUrl;
  });
});

9.2.3 FID (First Input Delay) 改善の実践的テクニック

FID の測定メカニズムと JavaScript 最適化戦略

FID は、ユーザーが初めてページと対話(クリック、タップ、キー入力)した時から、ブラウザがその操作に応答を開始するまでの時間を測定します。メインスレッドのブロッキングを解消することが、FID改善の最重要ポイントです。

JavaScript タスクの分割と Web Workers の活用

javascript
// 高度なJavaScriptタスク最適化システム
class FIDOptimizationManager {
  constructor() {
    this.taskQueue = [];
    this.isProcessing = false;
    this.workerPool = [];
    this.setupPerformanceMonitoring();
    this.initializeWorkerPool();
  }

  // Web Worker プールの初期化
  initializeWorkerPool() {
    const numWorkers = Math.min(navigator.hardwareConcurrency || 4, 4);
    
    for (let i = 0; i < numWorkers; i++) {
      const worker = new Worker('/js/performance-worker.js');
      worker.onmessage = (event) => this.handleWorkerResult(event);
      this.workerPool.push({
        worker: worker,
        busy: false,
        id: i
      });
    }
  }

  // 重い処理のタスク分割実装
  processHeavyTask(taskData, callback) {
    const CHUNK_SIZE = 1000; // 1ミリ秒ごとにタスクを分割
    const chunks = this.splitIntoChunks(taskData, CHUNK_SIZE);
    let processedChunks = [];
    let currentChunkIndex = 0;

    const processNextChunk = () => {
      if (currentChunkIndex >= chunks.length) {
        // すべてのチャンクの処理完了
        const finalResult = this.combineChunkResults(processedChunks);
        callback(finalResult);
        return;
      }

      const startTime = performance.now();
      
      // 現在のチャンクを処理
      const chunkResult = this.processChunk(chunks[currentChunkIndex]);
      processedChunks.push(chunkResult);
      currentChunkIndex++;

      const processingTime = performance.now() - startTime;
      
      // New Relic に処理時間を報告
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('TaskChunkProcessed', {
          chunkIndex: currentChunkIndex - 1,
          processingTime: processingTime,
          remainingChunks: chunks.length - currentChunkIndex,
          taskType: taskData.type || 'unknown'
        });
      }

      // メインスレッドブロッキングの回避
      if (processingTime > 5) { // 5ms以上かかった場合は次フレームに延期
        setTimeout(processNextChunk, 0);
      } else {
        requestIdleCallback(processNextChunk, { timeout: 50 });
      }
    };

    processNextChunk();
  }

  // 長時間実行タスクの Web Worker への委譲
  delegateToWorker(taskData) {
    return new Promise((resolve, reject) => {
      const availableWorker = this.findAvailableWorker();
      
      if (!availableWorker) {
        // Workerが利用できない場合は軽量タスクとして処理
        this.processLightweightTask(taskData).then(resolve).catch(reject);
        return;
      }

      availableWorker.busy = true;
      const taskId = this.generateTaskId();
      
      // タスクの実行時間監視
      const startTime = performance.now();
      
      availableWorker.worker.postMessage({
        id: taskId,
        type: 'process_heavy_task',
        data: taskData
      });

      // 結果待機の設定
      this.pendingTasks = this.pendingTasks || new Map();
      this.pendingTasks.set(taskId, {
        resolve,
        reject,
        startTime,
        workerId: availableWorker.id
      });

      // タイムアウト設定(10秒)
      setTimeout(() => {
        if (this.pendingTasks.has(taskId)) {
          this.pendingTasks.delete(taskId);
          availableWorker.busy = false;
          reject(new Error('Worker task timeout'));
        }
      }, 10000);
    });
  }

  // Worker からの結果処理
  handleWorkerResult(event) {
    const { id, result, error, processingTime } = event.data;
    const taskInfo = this.pendingTasks.get(id);
    
    if (!taskInfo) return;
    
    const worker = this.workerPool.find(w => w.id === taskInfo.workerId);
    if (worker) worker.busy = false;
    
    this.pendingTasks.delete(id);
    
    // New Relic に Worker 処理時間を報告
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('WorkerTaskCompleted', {
        taskId: id,
        processingTime: processingTime,
        totalTime: performance.now() - taskInfo.startTime,
        workerId: taskInfo.workerId,
        success: !error
      });
    }
    
    if (error) {
      taskInfo.reject(new Error(error));
    } else {
      taskInfo.resolve(result);
    }
  }

  // FID に影響を与えるイベントハンドラーの最適化
  optimizeEventHandlers() {
    const criticalElements = document.querySelectorAll('[data-critical-interaction="true"]');
    
    criticalElements.forEach(element => {
      // 既存のイベントリスナーの最適化
      this.optimizeElementEventHandlers(element);
    });
  }

  optimizeElementEventHandlers(element) {
    // クリックイベントの高速化
    element.addEventListener('click', (event) => {
      const startTime = performance.now();
      
      // 即座にUIフィードバックを提供
      this.provideImmediateFeedback(element);
      
      // 重い処理は非同期で実行
      requestIdleCallback(() => {
        this.handleHeavyClickOperation(element, event);
        
        // 処理時間をNew Relicに報告
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('OptimizedClickHandler', {
            elementId: element.id || 'unknown',
            processingTime: performance.now() - startTime,
            wasDeferred: true
          });
        }
      });
    }, { passive: false });

    // タッチイベントの最適化(モバイル対応)
    if ('ontouchstart' in window) {
      element.addEventListener('touchstart', this.optimizedTouchHandler.bind(this), 
                            { passive: true });
    }
  }

  // 即座のUIフィードバック提供
  provideImmediateFeedback(element) {
    // 視覚的フィードバック
    element.style.transform = 'scale(0.98)';
    element.style.transition = 'transform 0.1s ease';
    
    // ハプティックフィードバック(対応デバイス)
    if (navigator.vibrate) {
      navigator.vibrate(50);
    }
    
    setTimeout(() => {
      element.style.transform = '';
    }, 100);
  }
}

// Worker ファイルの内容 (/js/performance-worker.js)
/*
// Web Worker 内で実行される重い処理
self.addEventListener('message', (event) => {
  const { id, type, data } = event.data;
  const startTime = performance.now();
  
  try {
    let result;
    
    switch (type) {
      case 'process_heavy_task':
        result = processHeavyComputation(data);
        break;
      case 'data_transformation':
        result = transformLargeDataset(data);
        break;
      case 'image_processing':
        result = processImageData(data);
        break;
      default:
        throw new Error(`Unknown task type: ${type}`);
    }
    
    const processingTime = performance.now() - startTime;
    
    self.postMessage({
      id,
      result,
      processingTime
    });
    
  } catch (error) {
    self.postMessage({
      id,
      error: error.message,
      processingTime: performance.now() - startTime
    });
  }
});

// 重い計算処理の例
function processHeavyComputation(data) {
  // 複雑なアルゴリズム実行
  let result = 0;
  for (let i = 0; i < data.iterations; i++) {
    result += Math.sqrt(i) * Math.sin(i) * Math.cos(i);
  }
  return result;
}
*/

// FID最適化システムの初期化
document.addEventListener('DOMContentLoaded', () => {
  const fidOptimizer = new FIDOptimizationManager();
  
  // 重要な対話要素の最適化
  fidOptimizer.optimizeEventHandlers();
});

Code Splitting と動的インポートによる初期読み込み最適化

javascript
// 高度なCode Splitting戦略
class DynamicLoadingManager {
  constructor() {
    this.loadedModules = new Map();
    this.moduleQueue = new Map();
    this.intersectionObserver = this.setupIntersectionObserver();
  }

  // 重要でない機能の遅延読み込み
  async loadModuleOnDemand(moduleName, triggerElement) {
    // すでに読み込み済みの場合は即座に返却
    if (this.loadedModules.has(moduleName)) {
      return this.loadedModules.get(moduleName);
    }

    // 読み込み中の場合は既存のPromiseを返却
    if (this.moduleQueue.has(moduleName)) {
      return this.moduleQueue.get(moduleName);
    }

    const loadPromise = this.performModuleLoad(moduleName, triggerElement);
    this.moduleQueue.set(moduleName, loadPromise);

    try {
      const module = await loadPromise;
      this.loadedModules.set(moduleName, module);
      this.moduleQueue.delete(moduleName);
      return module;
    } catch (error) {
      this.moduleQueue.delete(moduleName);
      throw error;
    }
  }

  async performModuleLoad(moduleName, triggerElement) {
    const startTime = performance.now();
    
    let module;
    
    try {
      // 動的インポートの実行
      switch (moduleName) {
        case 'chartModule':
          module = await import('./modules/charts.js');
          break;
        case 'advancedFilters':
          module = await import('./modules/advanced-filters.js');
          break;
        case 'dataVisualization':
          module = await import('./modules/data-visualization.js');
          break;
        case 'userPreferences':
          module = await import('./modules/user-preferences.js');
          break;
        default:
          throw new Error(`Unknown module: ${moduleName}`);
      }
      
      const loadTime = performance.now() - startTime;
      
      // New Relic に動的読み込み情報を送信
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('DynamicModuleLoaded', {
          moduleName: moduleName,
          loadTime: loadTime,
          triggerElement: triggerElement?.id || 'unknown',
          memoryUsage: this.getMemoryUsage(),
          wasUserTriggered: !!triggerElement
        });
      }
      
      return module;
      
    } catch (error) {
      // 読み込みエラーの報告
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('DynamicModuleLoadError', {
          moduleName: moduleName,
          errorMessage: error.message,
          loadTime: performance.now() - startTime
        });
      }
      throw error;
    }
  }

  // Intersection Observer を使用した自動的なモジュール読み込み
  setupIntersectionObserver() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const element = entry.target;
          const moduleName = element.dataset.requireModule;
          
          if (moduleName) {
            // 要素が表示されたら対応するモジュールを読み込み
            this.loadModuleOnDemand(moduleName, element);
            observer.unobserve(element); // 一度読み込んだら監視を停止
          }
        }
      });
    }, {
      rootMargin: '50px' // 表示される50px前に読み込み開始
    });

    return observer;
  }

  // 対話的要素の監視開始
  observeElements() {
    const elementsToObserve = document.querySelectorAll('[data-require-module]');
    elementsToObserve.forEach(element => {
      this.intersectionObserver.observe(element);
    });
  }

  // メモリ使用量の監視
  getMemoryUsage() {
    if (performance.memory) {
      return {
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize,
        limit: performance.memory.jsHeapSizeLimit
      };
    }
    return null;
  }
}

// Critical Resource Hints の動的実装
class ResourceHintsOptimizer {
  constructor() {
    this.setupResourceHints();
  }

  setupResourceHints() {
    // DNS prefetch for external domains
    this.addResourceHint('dns-prefetch', 'https://fonts.googleapis.com');
    this.addResourceHint('dns-prefetch', 'https://api.example.com');
    
    // Preconnect for critical third-party resources
    this.addResourceHint('preconnect', 'https://fonts.gstatic.com', true);
    
    // Module preload for critical JavaScript
    this.preloadCriticalModules();
  }

  addResourceHint(rel, href, crossorigin = false) {
    const link = document.createElement('link');
    link.rel = rel;
    link.href = href;
    if (crossorigin) link.crossOrigin = 'anonymous';
    document.head.appendChild(link);
  }

  async preloadCriticalModules() {
    // Critical modules that should be preloaded
    const criticalModules = [
      '/js/modules/user-interaction.js',
      '/js/modules/form-validation.js'
    ];

    for (const moduleUrl of criticalModules) {
      const link = document.createElement('link');
      link.rel = 'modulepreload';
      link.href = moduleUrl;
      document.head.appendChild(link);
    }
  }
}

// システムの初期化
document.addEventListener('DOMContentLoaded', () => {
  const dynamicLoader = new DynamicLoadingManager();
  const resourceHints = new ResourceHintsOptimizer();
  
  // 遅延読み込み要素の監視開始
  dynamicLoader.observeElements();
  
  // 重要でないJavaScriptの遅延実行
  setTimeout(() => {
    // 非重要な機能は3秒後に読み込み
    dynamicLoader.loadModuleOnDemand('userPreferences');
  }, 3000);
});

9.2.4 CLS (Cumulative Layout Shift) 対策の包括的実装

CLS の測定メカニズムとレイアウトシフト防止戦略

CLS は、ページ読み込み中に発生する 予期しないレイアウト変更 を定量化する指標です。広告の遅延読み込み、動的コンテンツの挿入、フォントの読み込みなどが主な原因となります。

レイアウト予約システムの実装

javascript
// 包括的なCLS対策システム
class CLSOptimizationManager {
  constructor() {
    this.layoutShiftEntries = [];
    this.cumulativeScore = 0;
    this.setupLayoutShiftObserver();
    this.implementLayoutReservation();
  }

  // Layout Shift の継続的監視
  setupLayoutShiftObserver() {
    if (!('LayoutShift' in window)) {
      console.warn('Layout Shift API not supported');
      return;
    }

    this.observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // セッションウィンドウ内でのCLS計算
        if (!entry.hadRecentInput) {
          this.cumulativeScore += entry.value;
          this.layoutShiftEntries.push({
            timestamp: entry.startTime,
            value: entry.value,
            sources: entry.sources ? Array.from(entry.sources) : [],
            hadRecentInput: entry.hadRecentInput
          });

          // 問題のあるレイアウトシフトの自動分析
          if (entry.value > 0.1) { // 単一のシフトが大きい場合
            this.analyzeLayoutShiftCause(entry);
          }
        }
      }

      // New Relic への継続的な CLS 報告
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('CLSMeasurement', {
          cumulativeScore: this.cumulativeScore,
          shiftCount: this.layoutShiftEntries.length,
          isGoodCLS: this.cumulativeScore <= 0.1,
          needsImprovement: this.cumulativeScore > 0.1 && this.cumulativeScore <= 0.25,
          isPoorCLS: this.cumulativeScore > 0.25
        });
      }
    });

    this.observer.observe({ type: 'layout-shift', buffered: true });
  }

  // レイアウトシフト原因の自動分析
  analyzeLayoutShiftCause(entry) {
    const causes = [];
    
    if (entry.sources) {
      entry.sources.forEach(source => {
        const element = source.node;
        if (!element) return;

        // 画像の遅延読み込みによるシフト
        if (element.tagName === 'IMG' && !element.width && !element.height) {
          causes.push({
            type: 'image_without_dimensions',
            element: element.src || element.dataset.src,
            recommendation: 'Set explicit width and height attributes'
          });
        }

        // 広告要素によるシフト
        if (element.classList.contains('ad-container') || 
            element.id.includes('ad')) {
          causes.push({
            type: 'advertising_insertion',
            element: element.id || element.className,
            recommendation: 'Pre-reserve space for advertisements'
          });
        }

        // フォント読み込みによるシフト
        if (this.isTextNode(element) && this.hasCustomFont(element)) {
          causes.push({
            type: 'font_loading_shift',
            element: `${element.tagName}.${element.className}`,
            recommendation: 'Use font-display: swap and size-adjust'
          });
        }

        // 動的コンテンツ挿入によるシフト
        if (element.dataset.dynamicContent === 'true') {
          causes.push({
            type: 'dynamic_content_insertion',
            element: element.id || 'dynamic-element',
            recommendation: 'Pre-reserve space for dynamic content'
          });
        }
      });
    }

    // New Relic に詳細な原因分析を送信
    if (typeof newrelic !== 'undefined' && causes.length > 0) {
      newrelic.addPageAction('CLSCauseAnalysis', {
        shiftValue: entry.value,
        causesDetected: causes.length,
        causes: JSON.stringify(causes),
        timestamp: entry.startTime,
        requiresUrgentFix: entry.value > 0.25
      });
    }
  }

  // 包括的なレイアウト予約システム
  implementLayoutReservation() {
    // 1. 画像のレイアウト予約
    this.reserveImageLayout();
    
    // 2. 広告スペースの予約
    this.reserveAdSpace();
    
    // 3. 動的コンテンツエリアの予約
    this.reserveDynamicContentSpace();
    
    // 4. フォント読み込みの最適化
    this.optimizeFontLoading();
  }

  // 画像のレイアウト予約実装
  reserveImageLayout() {
    const images = document.querySelectorAll('img:not([width]):not([height])');
    
    images.forEach(img => {
      // 画像のアスペクト比を設定
      const aspectRatioContainer = document.createElement('div');
      aspectRatioContainer.style.cssText = `
        position: relative;
        width: 100%;
        background-color: #f0f0f0;
        border-radius: 4px;
        overflow: hidden;
      `;

      // アスペクト比の計算(デフォルト16:9)
      const aspectRatio = img.dataset.aspectRatio || '16/9';
      aspectRatioContainer.style.aspectRatio = aspectRatio;

      // CSS で padding-bottom を使用したフォールバック
      if (!CSS.supports('aspect-ratio', aspectRatio)) {
        const [width, height] = aspectRatio.split('/').map(Number);
        const paddingBottom = (height / width) * 100;
        aspectRatioContainer.style.paddingBottom = `${paddingBottom}%`;
        aspectRatioContainer.style.height = '0';
      }

      // 画像の絶対配置
      img.style.cssText = `
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: center;
        opacity: 0;
        transition: opacity 0.3s ease;
      `;

      // スケルトンローダーの追加
      const skeleton = this.createSkeletonLoader();
      aspectRatioContainer.appendChild(skeleton);

      // DOM の更新
      img.parentNode.insertBefore(aspectRatioContainer, img);
      aspectRatioContainer.appendChild(img);

      // 画像読み込み完了時の処理
      img.addEventListener('load', () => {
        img.style.opacity = '1';
        skeleton.style.opacity = '0';
        setTimeout(() => skeleton.remove(), 300);
      });

      // 読み込みエラー時の処理
      img.addEventListener('error', () => {
        aspectRatioContainer.style.backgroundColor = '#e0e0e0';
        skeleton.innerHTML = '<div style="text-align:center;color:#666;">画像を読み込めませんでした</div>';
      });
    });
  }

  // スケルトンローダーの作成
  createSkeletonLoader() {
    const skeleton = document.createElement('div');
    skeleton.style.cssText = `
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: linear-gradient(90deg, 
        #f0f0f0 25%, 
        #e0e0e0 50%, 
        #f0f0f0 75%
      );
      background-size: 200% 100%;
      animation: skeleton-loading 1.5s infinite;
      transition: opacity 0.3s ease;
    `;

    // スケルトンアニメーションの CSS 追加
    if (!document.getElementById('skeleton-animation-css')) {
      const style = document.createElement('style');
      style.id = 'skeleton-animation-css';
      style.textContent = `
        @keyframes skeleton-loading {
          0% {
            background-position: 200% 0;
          }
          100% {
            background-position: -200% 0;
          }
        }
      `;
      document.head.appendChild(style);
    }

    return skeleton;
  }

  // 広告スペースの予約
  reserveAdSpace() {
    const adContainers = document.querySelectorAll('.ad-container, [class*="ad-"]');
    
    adContainers.forEach(container => {
      // 広告の標準サイズに基づいてスペースを予約
      const adSize = container.dataset.adSize || 'medium-rectangle';
      const dimensions = this.getAdDimensions(adSize);
      
      container.style.cssText = `
        min-height: ${dimensions.height}px;
        width: ${dimensions.width}px;
        background-color: #f5f5f5;
        border: 1px solid #ddd;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #999;
        font-size: 12px;
        margin: 10px auto;
      `;

      // 広告読み込み中の表示
      if (!container.hasChildNodes()) {
        container.innerHTML = `
          <div style="text-align: center;">
            <div style="margin-bottom: 8px;">📢</div>
            <div>広告</div>
          </div>
        `;
      }

      // 広告の動的読み込み監視
      const observer = new MutationObserver(() => {
        if (container.children.length > 1) {
          container.style.backgroundColor = 'transparent';
          container.style.border = 'none';
          observer.disconnect();
        }
      });

      observer.observe(container, { childList: true });
    });
  }

  // 広告サイズの取得
  getAdDimensions(adSize) {
    const adSizes = {
      'leaderboard': { width: 728, height: 90 },
      'medium-rectangle': { width: 300, height: 250 },
      'large-rectangle': { width: 336, height: 280 },
      'wide-skyscraper': { width: 160, height: 600 },
      'mobile-banner': { width: 320, height: 50 },
      'large-mobile-banner': { width: 320, height: 100 }
    };

    return adSizes[adSize] || adSizes['medium-rectangle'];
  }
}

// フォント読み込み最適化
class FontOptimizationManager {
  constructor() {
    this.setupFontDisplay();
    this.implementFontLoadingStrategy();
  }

  setupFontDisplay() {
    // Critical CSS にフォント最適化を追加
    const style = document.createElement('style');
    style.innerHTML = `
      /* フォント読み込みの最適化 */
      @font-face {
        font-family: 'CustomFont';
        src: url('/fonts/custom-font.woff2') format('woff2'),
             url('/fonts/custom-font.woff') format('woff');
        font-display: swap;
        size-adjust: 100.06%; /* フォールバックフォントとのサイズ調整 */
      }

      /* フォント読み込み中のレイアウトシフト防止 */
      .font-loading body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }

      .font-loaded body {
        font-family: 'CustomFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }
    `;
    document.head.appendChild(style);
  }

  async implementFontLoadingStrategy() {
    if ('fonts' in document) {
      try {
        // フォントの事前読み込み
        await document.fonts.ready;
        
        // カスタムフォントが利用可能かチェック
        const customFont = new FontFace(
          'CustomFont', 
          'url(/fonts/custom-font.woff2) format("woff2")'
        );
        
        await customFont.load();
        document.fonts.add(customFont);
        
        document.body.classList.add('font-loaded');
        document.body.classList.remove('font-loading');
        
        // New Relic にフォント読み込み状況を報告
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('CustomFontLoaded', {
            loadTime: performance.now(),
            fontFamily: 'CustomFont',
            preventedCLS: true
          });
        }
        
      } catch (error) {
        console.warn('Custom font loading failed:', error);
        document.body.classList.remove('font-loading');
      }
    }
  }
}

// CLS最適化システムの初期化
document.addEventListener('DOMContentLoaded', () => {
  new CLSOptimizationManager();
  new FontOptimizationManager();
  
  // ページ読み込み開始時にフォント読み込みクラスを設定
  document.body.classList.add('font-loading');
});

9.2.5 New Relic Browser での Core Web Vitals 継続監視システム

カスタムダッシュボードとアラート設定

New Relic Browser では、Core Web Vitals の継続的な監視と改善効果の測定が可能です。カスタムダッシュボードとアラートシステムを構築することで、パフォーマンスの劣化を即座に検出し、継続的な最適化を実現できます。

高度な監視ダッシュボードの構築

javascript
// New Relic Browser カスタムイベント送信システム
class NewRelicCoreWebVitalsMonitor {
  constructor() {
    this.vitalsData = {
      lcp: null,
      fid: null,
      cls: 0
    };
    this.setupVitalsObservers();
    this.setupCustomEventTracking();
  }

  setupVitalsObservers() {
    // LCP の詳細監視
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      const lastEntry = entries[entries.length - 1];
      
      this.vitalsData.lcp = {
        value: lastEntry.startTime,
        element: this.getElementInfo(lastEntry.element),
        renderTime: lastEntry.renderTime || lastEntry.loadTime,
        url: lastEntry.url
      };
      
      this.sendCoreWebVitalData('LCP', this.vitalsData.lcp);
    }).observe({ type: 'largest-contentful-paint', buffered: true });

    // FID の詳細監視
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      entries.forEach(entry => {
        this.vitalsData.fid = {
          value: entry.processingStart - entry.startTime,
          inputDelay: entry.processingStart - entry.startTime,
          processingTime: entry.processingEnd - entry.processingStart,
          eventType: entry.name,
          target: this.getElementInfo(entry.target)
        };
        
        this.sendCoreWebVitalData('FID', this.vitalsData.fid);
      });
    }).observe({ type: 'first-input', buffered: true });

    // CLS の継続監視
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      entries.forEach(entry => {
        if (!entry.hadRecentInput) {
          this.vitalsData.cls += entry.value;
          
          this.sendCoreWebVitalData('CLS', {
            cumulativeValue: this.vitalsData.cls,
            shiftValue: entry.value,
            affectedElements: entry.sources ? 
              Array.from(entry.sources).map(s => this.getElementInfo(s.node)) : []
          });
        }
      });
    }).observe({ type: 'layout-shift', buffered: true });
  }

  // 詳細なカスタムイベント送信
  sendCoreWebVitalData(metric, data) {
    if (typeof newrelic === 'undefined') return;

    const commonAttributes = {
      pageUrl: window.location.href,
      pageType: this.getPageType(),
      deviceType: this.getDeviceType(),
      connectionType: this.getConnectionType(),
      viewportSize: `${window.innerWidth}x${window.innerHeight}`,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    };

    // メトリック固有のデータ送信
    switch (metric) {
      case 'LCP':
        newrelic.addPageAction('CoreWebVitals_LCP', {
          ...commonAttributes,
          lcpValue: data.value,
          lcpGrade: this.gradeLCP(data.value),
          lcpElement: data.element?.tagName || 'unknown',
          lcpElementId: data.element?.id || '',
          lcpElementSrc: data.element?.src || '',
          renderTime: data.renderTime,
          resourceUrl: data.url || '',
          isAboveFold: data.element ? this.isAboveFold(data.element) : false
        });
        break;

      case 'FID':
        newrelic.addPageAction('CoreWebVitals_FID', {
          ...commonAttributes,
          fidValue: data.value,
          fidGrade: this.gradeFID(data.value),
          inputDelay: data.inputDelay,
          processingTime: data.processingTime,
          eventType: data.eventType,
          targetElement: data.target?.tagName || 'unknown',
          targetElementId: data.target?.id || ''
        });
        break;

      case 'CLS':
        newrelic.addPageAction('CoreWebVitals_CLS', {
          ...commonAttributes,
          clsCumulative: data.cumulativeValue,
          clsGrade: this.gradeCLS(data.cumulativeValue),
          shiftValue: data.shiftValue,
          affectedElementsCount: data.affectedElements.length,
          affectedElements: JSON.stringify(data.affectedElements)
        });
        break;
    }

    // 包括的なCore Web Vitalsサマリー送信
    this.sendVitalsSummary();
  }

  // Core Web Vitals サマリーの定期送信
  sendVitalsSummary() {
    if (typeof newrelic === 'undefined') return;

    const summary = {
      pageUrl: window.location.href,
      lcpValue: this.vitalsData.lcp?.value || 0,
      fidValue: this.vitalsData.fid?.value || 0,
      clsValue: this.vitalsData.cls || 0,
      
      // 各指標の評価
      lcpGrade: this.vitalsData.lcp ? this.gradeLCP(this.vitalsData.lcp.value) : 'unknown',
      fidGrade: this.vitalsData.fid ? this.gradeFID(this.vitalsData.fid.value) : 'unknown',
      clsGrade: this.gradeCLS(this.vitalsData.cls),
      
      // 総合評価
      overallGrade: this.calculateOverallGrade(),
      
      // パフォーマンス予算の遵守状況
      meetsPerformanceBudget: this.checkPerformanceBudget(),
      
      // ユーザー体験スコア
      userExperienceScore: this.calculateUXScore(),
      
      // デバイス・ネットワーク情報
      deviceType: this.getDeviceType(),
      connectionType: this.getConnectionType(),
      
      // ページ固有情報
      pageType: this.getPageType(),
      isFirstVisit: this.isFirstVisit(),
      
      timestamp: Date.now()
    };

    newrelic.addPageAction('CoreWebVitals_Summary', summary);
  }

  // 評価グレードの計算
  gradeLCP(value) {
    if (value <= 2500) return 'good';
    if (value <= 4000) return 'needs-improvement';
    return 'poor';
  }

  gradeFID(value) {
    if (value <= 100) return 'good';
    if (value <= 300) return 'needs-improvement';
    return 'poor';
  }

  gradeCLS(value) {
    if (value <= 0.1) return 'good';
    if (value <= 0.25) return 'needs-improvement';
    return 'poor';
  }

  // 総合パフォーマンスグレードの算出
  calculateOverallGrade() {
    const grades = [];
    
    if (this.vitalsData.lcp) {
      grades.push(this.gradeLCP(this.vitalsData.lcp.value));
    }
    if (this.vitalsData.fid) {
      grades.push(this.gradeFID(this.vitalsData.fid.value));
    }
    grades.push(this.gradeCLS(this.vitalsData.cls));

    // 最も悪いグレードを全体グレードとする
    if (grades.includes('poor')) return 'poor';
    if (grades.includes('needs-improvement')) return 'needs-improvement';
    return 'good';
  }

  // ユーザー体験スコアの計算(0-100点)
  calculateUXScore() {
    let score = 100;
    
    // LCP のスコア減点
    if (this.vitalsData.lcp) {
      if (this.vitalsData.lcp.value > 4000) score -= 40;
      else if (this.vitalsData.lcp.value > 2500) score -= 20;
    }

    // FID のスコア減点
    if (this.vitalsData.fid) {
      if (this.vitalsData.fid.value > 300) score -= 30;
      else if (this.vitalsData.fid.value > 100) score -= 15;
    }

    // CLS のスコア減点
    if (this.vitalsData.cls > 0.25) score -= 30;
    else if (this.vitalsData.cls > 0.1) score -= 15;

    return Math.max(score, 0);
  }
}

// ビジネス指標との相関分析
class BusinessMetricsCorrelation {
  constructor() {
    this.setupBusinessEventTracking();
  }

  setupBusinessEventTracking() {
    // コンバージョン追跡
    document.addEventListener('conversion', (event) => {
      this.trackConversionWithVitals(event.detail);
    });

    // エンゲージメント追跡
    this.trackEngagementMetrics();
  }

  trackConversionWithVitals(conversionData) {
    if (typeof newrelic === 'undefined') return;

    // 現在のCore Web Vitals値を取得
    const currentVitals = window.webVitalsMonitor?.vitalsData || {};

    newrelic.addPageAction('Conversion_With_Vitals', {
      ...conversionData,
      lcpAtConversion: currentVitals.lcp?.value || null,
      fidAtConversion: currentVitals.fid?.value || null,
      clsAtConversion: currentVitals.cls || null,
      vitalsGrade: window.webVitalsMonitor?.calculateOverallGrade() || 'unknown',
      uxScore: window.webVitalsMonitor?.calculateUXScore() || null,
      pageLoadToConversion: Date.now() - performance.timing.navigationStart
    });
  }

  trackEngagementMetrics() {
    let engagementStartTime = Date.now();
    let scrollDepth = 0;
    let interactions = 0;

    // スクロール深度の追跡
    window.addEventListener('scroll', () => {
      const currentScroll = window.scrollY;
      const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
      scrollDepth = Math.max(scrollDepth, (currentScroll / maxScroll) * 100);
    });

    // ユーザー操作の追跡
    ['click', 'keydown', 'touchstart'].forEach(eventType => {
      document.addEventListener(eventType, () => {
        interactions++;
      });
    });

    // ページ離脱時にエンゲージメントデータを送信
    window.addEventListener('beforeunload', () => {
      if (typeof newrelic === 'undefined') return;

      const engagementTime = Date.now() - engagementStartTime;
      const currentVitals = window.webVitalsMonitor?.vitalsData || {};

      newrelic.addPageAction('Engagement_With_Vitals', {
        engagementTime,
        scrollDepth,
        interactions,
        lcpValue: currentVitals.lcp?.value || null,
        fidValue: currentVitals.fid?.value || null,
        clsValue: currentVitals.cls || null,
        vitalsGrade: window.webVitalsMonitor?.calculateOverallGrade() || 'unknown',
        uxScore: window.webVitalsMonitor?.calculateUXScore() || null
      });
    });
  }
}

// 監視システムの初期化
document.addEventListener('DOMContentLoaded', () => {
  window.webVitalsMonitor = new NewRelicCoreWebVitalsMonitor();
  new BusinessMetricsCorrelation();
});

NRQL クエリとダッシュボード設定例

sql
-- Core Web Vitals ダッシュボード用 NRQL クエリ集

-- 1. Core Web Vitals トレンド分析
SELECT average(lcpValue) AS 'LCP Average',
       average(fidValue) AS 'FID Average',
       average(clsValue) AS 'CLS Average'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
SINCE 7 days ago
TIMESERIES

-- 2. デバイス別パフォーマンス比較
SELECT average(lcpValue) AS 'LCP',
       average(fidValue) AS 'FID',
       average(clsValue) AS 'CLS'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
FACET deviceType
SINCE 24 hours ago

-- 3. パフォーマンス改善効果の測定
SELECT count(*) AS 'Page Views',
       percentage(count(*), WHERE lcpGrade = 'good') AS 'LCP Good %',
       percentage(count(*), WHERE fidGrade = 'good') AS 'FID Good %',
       percentage(count(*), WHERE clsGrade = 'good') AS 'CLS Good %'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
FACET pageType
SINCE 7 days ago

-- 4. ビジネス指標との相関分析
SELECT count(*) AS 'Conversions',
       average(uxScore) AS 'Average UX Score',
       average(lcpAtConversion) AS 'Average LCP at Conversion'
FROM PageAction
WHERE actionName = 'Conversion_With_Vitals'
FACET vitalsGrade
SINCE 30 days ago

-- 5. 問題のあるページの特定
SELECT pageUrl,
       average(lcpValue) AS 'LCP',
       average(fidValue) AS 'FID',
       average(clsValue) AS 'CLS',
       count(*) AS 'Views'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
AND (lcpValue > 4000 OR fidValue > 300 OR clsValue > 0.25)
FACET pageUrl
SINCE 24 hours ago
ORDER BY count(*) DESC

-- 6. パフォーマンス予算の遵守率
SELECT percentage(count(*), WHERE meetsPerformanceBudget = true) AS 'Budget Compliance %'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
TIMESERIES
SINCE 7 days ago

-- 7. ネットワーク状況別パフォーマンス
SELECT average(lcpValue) AS 'LCP',
       average(fidValue) AS 'FID'
FROM PageAction
WHERE actionName = 'CoreWebVitals_Summary'
FACET connectionType
SINCE 24 hours ago

-- 8. エンゲージメント と Core Web Vitals の相関
SELECT average(engagementTime/1000) AS 'Avg Engagement (seconds)',
       average(scrollDepth) AS 'Avg Scroll Depth %'
FROM PageAction
WHERE actionName = 'Engagement_With_Vitals'
FACET vitalsGrade
SINCE 7 days ago

9.2.6 継続的パフォーマンス最適化とA/Bテスト実装

データ駆動型最適化システムの構築

Core Web Vitals の真の価値は、継続的な測定と改善のサイクルにあります。A/Bテストを活用した科学的アプローチにより、最適化施策の効果を定量的に評価し、持続的なパフォーマンス向上を実現します。

A/Bテスト統合システムの実装

javascript
// Core Web Vitals A/Bテストシステム
class PerformanceABTestManager {
  constructor() {
    this.currentTest = null;
    this.userSegment = this.determineUserSegment();
    this.setupExperimentTracking();
  }

  // ユーザーセグメントの決定
  determineUserSegment() {
    const userId = this.getUserId() || this.generateAnonymousId();
    const hash = this.simpleHash(userId);
    
    // 50/50 分割での A/B テスト
    const segment = hash % 100 < 50 ? 'control' : 'treatment';
    
    // セグメント情報をセッションに保存
    sessionStorage.setItem('ab_test_segment', segment);
    
    return {
      userId,
      segment,
      assignedAt: Date.now(),
      testVersion: 'cwv_optimization_v1'
    };
  }

  // パフォーマンス最適化実験の実装
  async runPerformanceExperiment() {
    const experiments = {
      'image_optimization': this.testImageOptimization,
      'critical_css': this.testCriticalCSS,
      'javascript_splitting': this.testJavaScriptSplitting,
      'font_loading': this.testFontLoadingStrategy
    };

    // 現在実行中の実験を取得
    const activeExperiments = await this.getActiveExperiments();
    
    for (const experimentId of activeExperiments) {
      if (experiments[experimentId]) {
        await experiments[experimentId].call(this);
      }
    }
  }

  // 画像最適化のA/Bテスト
  async testImageOptimization() {
    const testConfig = {
      id: 'image_optimization_test',
      variants: {
        control: 'standard_images',
        treatment: 'optimized_images'
      }
    };

    if (this.userSegment.segment === 'treatment') {
      // 治療群:最適化された画像配信
      await this.implementOptimizedImages();
    }

    // テスト結果の追跡
    this.trackExperimentMetrics(testConfig.id, this.userSegment.segment);
  }

  async implementOptimizedImages() {
    const images = document.querySelectorAll('img[data-ab-test="true"]');
    
    images.forEach(async (img) => {
      // WebP フォーマットの適用
      if (this.supportsWebP()) {
        img.src = img.src.replace(/\.(jpg|jpeg|png)$/, '.webp');
      }

      // レスポンシブ画像の適用
      if (!img.srcset) {
        const baseSrc = img.src.replace(/\.[^/.]+$/, '');
        const extension = img.src.includes('.webp') ? '.webp' : '.jpg';
        
        img.srcset = `
          ${baseSrc}_400w${extension} 400w,
          ${baseSrc}_800w${extension} 800w,
          ${baseSrc}_1200w${extension} 1200w
        `;
        img.sizes = '(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px';
      }

      // 遅延読み込みの適用(above-the-fold以外)
      if (!this.isAboveFold(img)) {
        img.loading = 'lazy';
      }
    });

    // 最適化適用の追跡
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('AB_Test_Image_Optimization_Applied', {
        segment: this.userSegment.segment,
        testId: 'image_optimization_test',
        optimizationsApplied: images.length,
        timestamp: Date.now()
      });
    }
  }

  // Critical CSS のA/Bテスト
  async testCriticalCSS() {
    if (this.userSegment.segment === 'treatment') {
      // 治療群:Critical CSS の動的適用
      await this.applyCriticalCSS();
    }
  }

  async applyCriticalCSS() {
    // Above-the-fold コンテンツの Critical CSS を動的生成
    const criticalCSS = await this.generateCriticalCSS();
    
    if (criticalCSS) {
      const style = document.createElement('style');
      style.textContent = criticalCSS;
      style.setAttribute('data-critical-css', 'true');
      document.head.insertBefore(style, document.head.firstChild);

      // 非Critical CSS の遅延読み込み
      this.deferNonCriticalCSS();
    }
  }

  // 実験結果の詳細追跡
  trackExperimentMetrics(experimentId, segment) {
    // Core Web Vitals 監視との連携
    if (window.webVitalsMonitor) {
      const originalSendData = window.webVitalsMonitor.sendCoreWebVitalData;
      
      window.webVitalsMonitor.sendCoreWebVitalData = (metric, data) => {
        // 元の処理を実行
        originalSendData.call(window.webVitalsMonitor, metric, data);
        
        // A/B テスト情報を追加して送信
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction(`AB_Test_${metric}`, {
            experimentId,
            segment,
            metricValue: this.extractMetricValue(metric, data),
            ...data
          });
        }
      };
    }

    // ビジネス指標の追跡
    this.trackBusinessMetricsWithExperiment(experimentId, segment);
  }

  trackBusinessMetricsWithExperiment(experimentId, segment) {
    // コンバージョン追跡
    document.addEventListener('conversion', (event) => {
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('AB_Test_Conversion', {
          experimentId,
          segment,
          conversionType: event.detail.type,
          conversionValue: event.detail.value,
          timestamp: Date.now()
        });
      }
    });

    // エンゲージメント追跡の拡張
    let sessionStartTime = Date.now();
    
    window.addEventListener('beforeunload', () => {
      const sessionDuration = Date.now() - sessionStartTime;
      
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('AB_Test_Session', {
          experimentId,
          segment,
          sessionDuration,
          pageViews: this.getPageViewCount(),
          timestamp: Date.now()
        });
      }
    });
  }

  // 実験結果の統計分析
  async analyzeExperimentResults(experimentId) {
    const query = `
      SELECT 
        segment,
        count(*) AS sample_size,
        average(lcpValue) AS avg_lcp,
        average(fidValue) AS avg_fid,
        average(clsValue) AS avg_cls,
        percentage(count(*), WHERE lcpGrade = 'good') AS lcp_good_rate,
        percentage(count(*), WHERE fidGrade = 'good') AS fid_good_rate,
        percentage(count(*), WHERE clsGrade = 'good') AS cls_good_rate
      FROM PageAction 
      WHERE actionName LIKE '%AB_Test_%'
      AND experimentId = '${experimentId}'
      FACET segment
      SINCE 7 days ago
    `;

    // New Relic Insights API を使用して結果を取得
    const results = await this.queryNewRelicInsights(query);
    
    // 統計的有意性の検定
    const significance = this.calculateStatisticalSignificance(results);
    
    return {
      experimentId,
      results,
      significance,
      recommendation: this.generateRecommendation(results, significance)
    };
  }

  // 統計的有意性の計算
  calculateStatisticalSignificance(results) {
    if (results.length < 2) return null;

    const control = results.find(r => r.segment === 'control');
    const treatment = results.find(r => r.segment === 'treatment');

    if (!control || !treatment) return null;

    // t検定による有意性検定の簡易実装
    const significance = {
      lcp: this.tTest(control.avg_lcp, treatment.avg_lcp, control.sample_size, treatment.sample_size),
      fid: this.tTest(control.avg_fid, treatment.avg_fid, control.sample_size, treatment.sample_size),
      cls: this.tTest(control.avg_cls, treatment.avg_cls, control.sample_size, treatment.sample_size)
    };

    return significance;
  }

  // 最適化推奨事項の生成
  generateRecommendation(results, significance) {
    const recommendations = [];

    if (significance && significance.lcp && significance.lcp.pValue < 0.05) {
      const improvement = this.calculateImprovement(results, 'avg_lcp');
      if (improvement > 0) {
        recommendations.push({
          metric: 'LCP',
          improvement: `${improvement.toFixed(1)}%改善`,
          action: '治療群の最適化施策を本番適用することを推奨'
        });
      }
    }

    // 他の指標についても同様の分析...

    return recommendations;
  }
}

// パフォーマンス予算管理システム
class PerformanceBudgetManager {
  constructor() {
    this.budgets = {
      lcp: { target: 2500, warning: 2000, critical: 3000 },
      fid: { target: 100, warning: 80, critical: 200 },
      cls: { target: 0.1, warning: 0.05, critical: 0.15 },
      
      // 技術的な予算項目
      totalJSSize: { target: 300000, warning: 250000, critical: 400000 }, // 300KB
      totalCSSSize: { target: 100000, warning: 80000, critical: 150000 },   // 100KB
      totalImageSize: { target: 2000000, warning: 1500000, critical: 3000000 }, // 2MB
      httpRequests: { target: 50, warning: 40, critical: 70 }
    };
    
    this.setupBudgetMonitoring();
  }

  setupBudgetMonitoring() {
    // リアルタイム予算監視
    if (window.webVitalsMonitor) {
      this.monitorCoreWebVitalsBudget();
    }
    
    // リソース予算の監視
    this.monitorResourceBudget();
    
    // 継続的なレポート生成
    setInterval(() => {
      this.generateBudgetReport();
    }, 300000); // 5分ごと
  }

  monitorCoreWebVitalsBudget() {
    const originalSendData = window.webVitalsMonitor.sendCoreWebVitalData;
    
    window.webVitalsMonitor.sendCoreWebVitalData = (metric, data) => {
      // 元の処理を実行
      originalSendData.call(window.webVitalsMonitor, metric, data);
      
      // 予算チェック
      this.checkBudgetViolation(metric, data);
    };
  }

  checkBudgetViolation(metric, data) {
    const metricKey = metric.toLowerCase();
    const budget = this.budgets[metricKey];
    
    if (!budget) return;
    
    const value = this.extractMetricValue(metric, data);
    let violationType = null;
    
    if (value > budget.critical) {
      violationType = 'critical';
    } else if (value > budget.target) {
      violationType = 'warning';
    }
    
    if (violationType) {
      this.reportBudgetViolation({
        metric: metricKey,
        value,
        budget: budget.target,
        violationType,
        timestamp: Date.now()
      });
    }
  }

  reportBudgetViolation(violation) {
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('Performance_Budget_Violation', violation);
    }
    
    // 重要な違反の場合はアラート送信
    if (violation.violationType === 'critical') {
      this.sendCriticalAlert(violation);
    }
  }

  async sendCriticalAlert(violation) {
    // Slack, Email, PagerDuty などへのアラート送信
    const alertData = {
      title: `🚨 パフォーマンス予算違反: ${violation.metric.toUpperCase()}`,
      message: `${violation.metric} が予算値 ${violation.budget} を超過しました(現在値: ${violation.value})`,
      severity: 'critical',
      url: window.location.href,
      timestamp: new Date().toISOString()
    };
    
    try {
      await fetch('/api/alerts/performance', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(alertData)
      });
    } catch (error) {
      console.error('Failed to send critical alert:', error);
    }
  }
}

// システムの統合初期化
document.addEventListener('DOMContentLoaded', async () => {
  // A/B テスト管理の初期化
  const abTestManager = new PerformanceABTestManager();
  await abTestManager.runPerformanceExperiment();
  
  // パフォーマンス予算管理の初期化
  new PerformanceBudgetManager();
  
  // グローバルオブジェクトとして公開
  window.performanceOptimization = {
    abTestManager,
    budgetManager: window.budgetManager
  };
});

まとめ

本章では、Core Web Vitals の包括的な監視と最適化について、New Relic Browser を活用した実践的手法を詳しく解説しました。

🎯 主要な学習成果

1. Core Web Vitals の本質理解

  • LCP(Largest Contentful Paint):主要コンテンツの表示時間最適化
  • FID(First Input Delay):ユーザー操作への応答性向上
  • CLS(Cumulative Layout Shift):レイアウト安定性の確保

2. 技術的最適化手法の習得

  • 画像最適化:次世代フォーマット・レスポンシブ画像・Critical loading
  • JavaScript最適化:タスク分割・Web Workers・Code Splitting
  • CSS最適化:Critical CSS・フォント読み込み戦略・レイアウト予約

3. 継続的監視システムの構築

  • New Relic Browser:詳細なカスタムイベント送信と分析
  • NRQL クエリ:パフォーマンストレンド・問題特定・ROI測定
  • A/B テスト:科学的アプローチによる最適化効果の検証

4. ビジネス価値の実現

  • SEO向上:Google PageSpeed Insights スコア改善
  • ユーザー体験向上:直帰率削減・エンゲージメント増加
  • コンバージョン改善:ページ速度とビジネス指標の相関活用

📈 期待される実装効果

技術的成果

  • Core Web Vitals 全項目での目標値達成(LCP < 2.5s、FID < 100ms、CLS < 0.1)
  • Google PageSpeed Insights スコア 90+ の実現
  • JavaScript エラー率 90% 削減

ビジネス成果

  • コンバージョン率 15-30% 向上
  • 直帰率 20-35% 削減
  • SEO順位向上による自然流入増加

🚀 次のステップ

Core Web Vitals の基盤を確立した後は、**第9.3章 SPA・PWA監視とエラー追跡**に進み、モダンなWebアプリケーションでの高度な監視実装を学習することを推奨します。

React・Vue・Angular などのフレームワークを使用したアプリケーションでの 包括的なフロントエンド監視体制 を構築し、真のユーザー体験最適化を実現しましょう。


📖 ナビゲーション

メイン: 第9章 New Relic Browser詳細化
前セクション: 第9.1章 ブラウザ監視の重要性とRUM
次セクション: 第9.3章 SPA・PWA監視とエラー追跡


関連記事: New Relic APM 高度活用
関連記事: New Relic Infrastructure監視
関連記事: New Relic入門 完全ガイド