Core Web Vitals Chrome Extension: Measure LCP, FID and CLS

20 min read

Core Web Vitals Chrome Extension: Measure LCP, FID and CLS

Core Web Vitals Chrome Extension: Measure LCP, FID and CLS

In the ever-evolving landscape of web development, performance metrics have become the cornerstone of user experience. Core Web Vitals—Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS)—are Google’s user-centered metrics that measure loading performance, interactivity, and visual stability. Understanding and monitoring these metrics is crucial for any developer or SEO professional looking to create exceptional web experiences.

This comprehensive guide walks you through building a Core Web Vitals Chrome extension that measures these critical metrics in real-time. Whether you are a seasoned extension developer or just starting your journey, this tutorial provides everything you need to create a powerful performance monitoring tool.


Understanding Core Web Vitals

Before diving into the implementation, let us explore what makes Core Web Vitals so important and why building a dedicated web vitals extension can significantly benefit your development workflow.

Largest Contentful Paint (LCP)

Largest Contentful Paint measures the time it takes for the largest content element in the viewport to become visible. This typically includes images, video posters, or large text blocks. LCP is a critical metric because it directly correlates with how quickly users perceive your page to be loading.

A good LCP score is 2.5 seconds or less. Anything above 4.0 seconds needs improvement. Common causes of poor LCP include slow server response times, render-blocking resources, and unoptimized images. A well-designed core web vitals chrome extension should accurately capture this metric and provide actionable insights.

First Input Delay (FID)

First Input Delay measures the time between a user’s first interaction with your page (like clicking a button or selecting a dropdown) and the browser’s ability to begin processing that event. Unlike other metrics that can be measured synthetically, FID requires real user interaction, making it essential for field testing.

An excellent FID score is under 100 milliseconds. Delays between 100-300ms need improvement, while anything above 300ms is considered poor. FID issues typically stem from heavy JavaScript execution that blocks the main thread, preventing the browser from responding to user input promptly.

Cumulative Layout Shift (CLS)

Cumulative Layout Shift measures visual stability by calculating how much page content shifts unexpectedly during the user session. Imagine clicking a button only to have an advertisement load above it, causing your click to register on a different element entirely. This frustrating experience is measured by CLS.

A good CLS score is 0.1 or less. Scores between 0.1-0.25 need improvement, while anything above 0.25 is considered poor. Common causes include images without dimensions, dynamically injected content, and fonts causing text reflow.


Building Your Core Web Vitals Chrome Extension

Now that you understand the metrics, let us build a comprehensive performance metrics extension that captures and displays Core Web Vitals data in real-time.

Project Setup and Manifest Configuration

Every Chrome extension begins with a manifest file. For our web vitals extension, we will use Manifest V3, which offers improved security and performance.

{
  "manifest_version": 3,
  "name": "Core Web Vitals Monitor",
  "version": "1.0.0",
  "description": "Measure and monitor LCP, FID, and CLS metrics in real-time",
  "permissions": ["activeTab", "scripting", "storage"],
  "host_permissions": ["<all_urls>"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

This manifest configuration grants our extension the necessary permissions to inject content scripts and access page performance data. The activeTab permission ensures we only measure the currently active tab, respecting user privacy.

Implementing the Web Vitals Measurement Engine

The core of our web vitals extension lies in the JavaScript that captures and calculates the metrics. We will use the web-vitals library, developed by Google, which provides accurate implementations of Core Web Vitals measurement.

Create a file named vitals.js in your extension directory:

// vitals.js - Core Web Vitals measurement library
(function() {
  'use strict';

  // Largest Contentful Paint measurement
  function measureLCP(callback) {
    if (!('PerformanceObserver' in window)) {
      callback({ name: 'LCP', value: 0, rating: 'needs-improvement' });
      return;
    }

    try {
      const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        
        const lcp = lastEntry.renderTime || lastEntry.loadTime;
        const rating = lcp <= 2500 ? 'good' : lcp <= 4000 ? 'needs-improvement' : 'poor';
        
        callback({ name: 'LCP', value: lcp, rating });
      });

      observer.observe({ entryTypes: ['largest-contentful-paint'] });
    } catch (e) {
      // Fallback for browsers without LCP support
      const paintEntries = performance.getEntriesByType('paint');
      const lcpEntry = paintEntries.find(e => e.name === 'largest-contentful-paint');
      
      if (lcpEntry) {
        const rating = lcpEntry.renderTime <= 2500 ? 'good' : 
                       lcpEntry.renderTime <= 4000 ? 'needs-improvement' : 'poor';
        callback({ name: 'LCP', value: lcpEntry.renderTime, rating });
      }
    }
  }

  // First Input Delay measurement
  function measureFID(callback) {
    if (!('PerformanceObserver' in window)) {
      callback({ name: 'FID', value: 0, rating: 'needs-improvement' });
      return;
    }

    try {
      const observer = new PerformanceObserver((entryList) => {
        const firstInput = entryList.getEntries()[0];
        
        if (firstInput && firstInput.processingStart !== undefined) {
          const fid = firstInput.processingStart - firstInput.startTime;
          const rating = fid <= 100 ? 'good' : fid <= 300 ? 'needs-improvement' : 'poor';
          
          callback({ name: 'FID', value: fid, rating });
        }
      });

      observer.observe({ entryTypes: ['first-input'] });
    } catch (e) {
      console.log('FID measurement not supported');
    }
  }

  // Cumulative Layout Shift measurement
  function measureCLS(callback) {
    if (!('PerformanceObserver' in window)) {
      callback({ name: 'CLS', value: 0, rating: 'needs-improvement' });
      return;
    }

    let clsValue = 0;
    let clsEntries = [];

    try {
      const observer = new PerformanceObserver((entryList) => {
        for (const entry of entryList.getEntries()) {
          if (!entry.hadRecentInput) {
            clsEntries.push(entry);
            clsValue += entry.value;
          }
        }
      });

      observer.observe({ entryTypes: ['layout-shift'] });

      // Report final CLS when page is hidden
      document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
          const rating = clsValue <= 0.1 ? 'good' : 
                         clsValue <= 0.25 ? 'needs-improvement' : 'poor';
          callback({ name: 'CLS', value: clsValue, rating });
        }
      });
    } catch (e) {
      console.log('CLS measurement not supported');
    }
  }

  // Export measurement functions
  window.WebVitals = {
    measureLCP,
    measureFID,
    measureCLS
  };
})();

This measurement engine provides accurate, Google-compliant implementations of all three Core Web Vitals metrics. The code handles browser compatibility issues gracefully and provides ratings based on Google’s established thresholds.

Content Script for Data Collection

Now we need a content script that injects our measurement code and communicates with the extension:

// content.js
(function() {
  'use strict';

  // Inject the vitals library
  const script = document.createElement('script');
  script.src = chrome.runtime.getURL('vitals.js');
  script.onload = function() {
    this.remove();
    
    // Collect metrics after page load
    const metrics = {
      url: window.location.href,
      timestamp: new Date().toISOString(),
      lcp: null,
      fid: null,
      cls: null
    };

    // Measure LCP
    window.WebVitals.measureLCP((data) => {
      metrics.lcp = data;
      sendMetrics(metrics);
    });

    // Measure FID
    window.WebVitals.measureFID((data) => {
      metrics.fid = data;
      sendMetrics(metrics);
    });

    // Measure CLS
    window.WebVitals.measureCLS((data) => {
      metrics.cls = data;
      sendMetrics(metrics);
    });
  };

  (document.head || document.documentElement).appendChild(script);

  function sendMetrics(metrics) {
    // Only send when all metrics are collected
    if (metrics.lcp && metrics.fid && metrics.cls) {
      chrome.runtime.sendMessage({
        type: 'METRICS_COLLECTED',
        payload: metrics
      });
    }
  }
})();

Background Service Worker

The background service worker coordinates communication between content scripts and the popup:

// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'METRICS_COLLECTED') {
    // Store metrics in extension storage
    chrome.storage.local.get(['metricsHistory'], (result) => {
      const history = result.metricsHistory || [];
      
      // Add new metrics with tab information
      message.payload.tabId = sender.tab?.id;
      message.payload.tabUrl = sender.tab?.url;
      
      // Keep only last 100 measurements
      history.push(message.payload);
      if (history.length > 100) {
        history.shift();
      }
      
      chrome.storage.local.set({ metricsHistory: history });
    });
  }
  
  return true;
});

// Listen for tab updates to measure new pages
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    chrome.scripting.executeScript({
      target: { tabId: tabId },
      files: ['content.js']
    });
  }
});

Building the Popup Interface

The popup provides users with a quick view of their current page’s Core Web Vitals:

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      width: 350px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 16px;
      background: #ffffff;
    }
    
    h1 {
      font-size: 16px;
      margin: 0 0 16px 0;
      color: #202124;
    }
    
    .metric-card {
      background: #f8f9fa;
      border-radius: 8px;
      padding: 12px;
      margin-bottom: 12px;
    }
    
    .metric-name {
      font-size: 12px;
      color: #5f6368;
      margin-bottom: 4px;
    }
    
    .metric-value {
      font-size: 24px;
      font-weight: 600;
      color: #202124;
    }
    
    .metric-rating {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 500;
      margin-left: 8px;
    }
    
    .rating-good {
      background: #e6f4ea;
      color: #1e8e3e;
    }
    
    .rating-needs-improvement {
      background: #fef7e0;
      color: #b06000;
    }
    
    .rating-poor {
      background: #fce8e6;
      color: #d93025;
    }
    
    .overall-score {
      margin-top: 16px;
      padding: 16px;
      background: #e8f0fe;
      border-radius: 8px;
      text-align: center;
    }
    
    .overall-score h2 {
      font-size: 14px;
      margin: 0 0 8px 0;
      color: #1a73e8;
    }
  </style>
</head>
<body>
  <h1>Core Web Vitals</h1>
  
  <div class="metric-card">
    <div class="metric-name">Largest Contentful Paint (LCP)</div>
    <div>
      <span class="metric-value" id="lcp-value">--</span>
      <span class="metric-rating" id="lcp-rating">--</span>
    </div>
  </div>
  
  <div class="metric-card">
    <div class="metric-name">First Input Delay (FID)</div>
    <div>
      <span class="metric-value" id="fid-value">--</span>
      <span class="metric-rating" id="fid-rating">--</span>
    </div>
  </div>
  
  <div class="metric-card">
    <div class="metric-name">Cumulative Layout Shift (CLS)</div>
    <div>
      <span class="metric-value" id="cls-value">--</span>
      <span class="metric-rating" id="cls-rating">--</span>
    </div>
  </div>
  
  <div class="overall-score" id="overall-score">
    <h2>Overall Score</h2>
    <span id="overall-value">Waiting for data...</span>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>

Finally, the popup script that displays the collected metrics:

// popup.js
document.addEventListener('DOMContentLoaded', () => {
  chrome.storage.local.get(['metricsHistory'], (result) => {
    const history = result.metricsHistory || [];
    
    if (history.length === 0) {
      document.getElementById('overall-value').textContent = 'No data available';
      return;
    }
    
    // Get the most recent metrics
    const latest = history[history.length - 1];
    
    if (latest.lcp) {
      displayMetric('lcp', latest.lcp);
    }
    
    if (latest.fid) {
      displayMetric('fid', latest.fid);
    }
    
    if (latest.cls) {
      displayMetric('cls', latest.cls);
    }
    
    // Calculate overall score
    const scores = [latest.lcp, latest.fid, latest.cls]
      .filter(m => m !== null)
      .map(m => m.rating === 'good' ? 3 : m.rating === 'needs-improvement' ? 2 : 1);
    
    const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
    const overallEl = document.getElementById('overall-value');
    
    if (avgScore >= 2.5) {
      overallEl.textContent = 'Good';
      overallEl.style.color = '#1e8e3e';
      overallEl.style.fontWeight = '600';
    } else if (avgScore >= 1.5) {
      overallEl.textContent = 'Needs Improvement';
      overallEl.style.color = '#b06000';
      overallEl.style.fontWeight = '600';
    } else {
      overallEl.textContent = 'Poor';
      overallEl.style.color = '#d93025';
      overallEl.style.fontWeight = '600';
    }
  });
  
  function displayMetric(name, data) {
    const valueEl = document.getElementById(`${name}-value`);
    const ratingEl = document.getElementById(`${name}-rating`);
    
    // Format value based on metric type
    if (name === 'cls') {
      valueEl.textContent = data.value.toFixed(3);
    } else {
      valueEl.textContent = `${Math.round(data.value)}ms`;
    }
    
    // Set rating
    ratingEl.textContent = data.rating.replace('-', ' ');
    ratingEl.className = `metric-rating rating-${data.rating}`;
  }
});

Advanced Features for Your Web Vitals Extension

Now that you have a functional core web vitals chrome extension, consider adding these advanced features to make it even more powerful.

Historical Data Tracking

Implement a chart showing performance trends over time. Use Chrome’s storage API to persist metrics across sessions, then visualize the data using a library like Chart.js. This feature helps users understand how their optimizations affect performance over time.

Performance Budget Alerts

Add configurable alerts that trigger when metrics exceed defined thresholds. For example, notify users when LCP exceeds 2.5 seconds or when CLS goes above 0.1. These alerts transform your extension from a passive monitoring tool into an active performance guardian.

Export and Reporting

Enable users to export their performance data as JSON or CSV for further analysis. This feature is particularly valuable for developers reporting to stakeholders or tracking performance across multiple projects.

Integration with Google Analytics

Advanced users might benefit from sending Core Web Vitals data to their Google Analytics property. This allows for aggregated performance data analysis alongside other business metrics.


Testing and Debugging Your Extension

Proper testing ensures your performance metrics extension works correctly across different browsers and scenarios.

Manual Testing

Install your extension in development mode by navigating to chrome://extensions/, enabling Developer mode, and clicking “Load unpacked.” Test on various websites, including:

  • Simple static HTML pages
  • Complex single-page applications
  • Pages with heavy JavaScript
  • Pages with many images and media
  • Pages with dynamic content injection

Automated Testing

Create automated tests using Puppeteer or Playwright to verify your extension captures metrics correctly. Compare your extension’s results against known good values from Google’s web-vitals library.


Publishing Your Extension

Once your extension is tested and polished, you can publish it to the Chrome Web Store. Prepare your store listing with:

  • Clear, descriptive title mentioning “web vitals” and “core web vitals chrome”
  • Screenshots showing the extension in action
  • Detailed description covering all features
  • Appropriate category and tags

Remember to comply with Chrome Web Store policies and ensure your extension handles user data responsibly.


Conclusion

Building a Core Web Vitals Chrome extension is an excellent project that combines web performance knowledge with extension development skills. By measuring LCP, FID, and CLS in real-time, you create a valuable tool for developers, SEO professionals, and performance enthusiasts alike.

The web vitals extension you build following this guide provides immediate, actionable insights into page performance. Users can identify performance issues as they browse, understand the impact of different websites, and track improvements over time.

As web performance continues to play an increasingly important role in user experience and search rankings, tools like this become essential. Start building your extension today, and contribute to a faster, more stable web for everyone.

Remember, the best performance metrics extension is one that not only measures accurately but also helps users understand and improve their web experiences. Keep iterating, gather feedback, and continue enhancing your extension to make Core Web Vitals accessible to everyone.


Turn Your Extension Into a Business

Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.

Built by theluckystrike at zovo.one

No previous article
No next article