Performance Monitoring and Core Web Vitals
Monitor real-world performance of your React Shopify theme. Learn to track Core Web Vitals, set up real user monitoring, and use performance data to prioritize optimizations.
Performance optimization without measurement is just guessing. Core Web Vitals provide standardized metrics that correlate with user experience, and Google uses them as ranking signals. In this lesson, we’ll set up comprehensive performance monitoring for your React Shopify theme.
Understanding Core Web Vitals
Google’s Core Web Vitals measure three aspects of user experience:
┌─────────────────────────────────────────────────────────────────┐│ CORE WEB VITALS │├─────────────────────────────────────────────────────────────────┤│ ││ LCP (Largest Contentful Paint) ││ ├─ Measures: Loading performance ││ ├─ Target: < 2.5 seconds ││ └─ What: Time until largest visible element renders ││ ││ INP (Interaction to Next Paint) ││ ├─ Measures: Interactivity ││ ├─ Target: < 200 milliseconds ││ └─ What: Time from user input to visual feedback ││ ││ CLS (Cumulative Layout Shift) ││ ├─ Measures: Visual stability ││ ├─ Target: < 0.1 ││ └─ What: How much content moves unexpectedly ││ ││ ┌────────────────────────────────────────────────────────────┐ ││ │ GOOD │ NEEDS IMPROVEMENT │ POOR │ ││ │ (green) │ (yellow) │ (red) │ ││ ├──────────────┼───────────────────────┼─────────────────────┤ ││ │ LCP < 2.5s │ 2.5s - 4.0s │ > 4.0s │ ││ │ INP < 200ms │ 200ms - 500ms │ > 500ms │ ││ │ CLS < 0.1 │ 0.1 - 0.25 │ > 0.25 │ ││ └──────────────┴───────────────────────┴─────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────┘Measuring Core Web Vitals
Use the web-vitals library for accurate measurement:
npm install web-vitalsimport { onCLS, onINP, onLCP, onFCP, onTTFB, Metric } from 'web-vitals';
interface VitalsReport { name: string; value: number; rating: 'good' | 'needs-improvement' | 'poor'; delta: number; id: string; navigationType: string;}
function sendToAnalytics(metric: Metric) { const report: VitalsReport = { name: metric.name, value: metric.value, rating: metric.rating, delta: metric.delta, id: metric.id, navigationType: metric.navigationType, };
// Log in development if (process.env.NODE_ENV === 'development') { console.log(`[${metric.name}]`, { value: metric.value.toFixed(metric.name === 'CLS' ? 3 : 0), rating: metric.rating, }); }
// Send to your analytics service if (typeof gtag !== 'undefined') { gtag('event', metric.name, { event_category: 'Web Vitals', event_label: metric.id, value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value), non_interaction: true, }); }
// Or send to a custom endpoint navigator.sendBeacon?.('/api/vitals', JSON.stringify(report));}
export function initWebVitals() { onCLS(sendToAnalytics); onINP(sendToAnalytics); onLCP(sendToAnalytics); onFCP(sendToAnalytics); onTTFB(sendToAnalytics);}Initialize in your main entry:
import { initWebVitals } from '@/lib/web-vitals';
// Start measuring after page loadif (typeof window !== 'undefined') { initWebVitals();}Building a Performance Dashboard
Create a debug panel to view metrics during development:
import { useState, useEffect } from 'react';import { onCLS, onINP, onLCP, onFCP, onTTFB, Metric } from 'web-vitals';import styles from './PerformancePanel.module.css';
interface MetricData { name: string; value: number; rating: 'good' | 'needs-improvement' | 'poor';}
export function PerformancePanel() { const [metrics, setMetrics] = useState<Record<string, MetricData>>({}); const [isOpen, setIsOpen] = useState(false);
useEffect(() => { const handleMetric = (metric: Metric) => { setMetrics((prev) => ({ ...prev, [metric.name]: { name: metric.name, value: metric.value, rating: metric.rating, }, })); };
onCLS(handleMetric); onINP(handleMetric); onLCP(handleMetric); onFCP(handleMetric); onTTFB(handleMetric); }, []);
if (process.env.NODE_ENV !== 'development') { return null; }
return ( <div className={styles.container}> <button className={styles.toggle} onClick={() => setIsOpen(!isOpen)} aria-label="Toggle performance panel" > ⚡ </button>
{isOpen && ( <div className={styles.panel}> <h3>Core Web Vitals</h3> <div className={styles.metrics}> {['LCP', 'INP', 'CLS', 'FCP', 'TTFB'].map((name) => { const metric = metrics[name]; return ( <div key={name} className={`${styles.metric} ${styles[metric?.rating || 'pending']}`} > <span className={styles.name}>{name}</span> <span className={styles.value}> {metric ? name === 'CLS' ? metric.value.toFixed(3) : `${Math.round(metric.value)}ms` : '—'} </span> </div> ); })} </div> </div> )} </div> );}.container { position: fixed; bottom: 1rem; left: 1rem; z-index: 9999;}
.toggle { width: 40px; height: 40px; border-radius: 50%; border: none; background: #1f2937; color: white; font-size: 20px; cursor: pointer;}
.panel { position: absolute; bottom: 50px; left: 0; width: 200px; padding: 1rem; background: #1f2937; color: white; border-radius: 8px; font-family: monospace; font-size: 12px;}
.panel h3 { margin: 0 0 0.75rem; font-size: 14px;}
.metrics { display: flex; flex-direction: column; gap: 0.5rem;}
.metric { display: flex; justify-content: space-between; padding: 0.25rem 0.5rem; border-radius: 4px;}
.metric.good { background: #065f46; }.metric.needs-improvement { background: #92400e; }.metric.poor { background: #991b1b; }.metric.pending { background: #374151; }
.name { font-weight: bold; }Tracking React-Specific Performance
Monitor React component render times:
import { Profiler, ProfilerOnRenderCallback } from 'react';
const renderTimes: Map<string, number[]> = new Map();
const onRender: ProfilerOnRenderCallback = ( id, phase, actualDuration, baseDuration, startTime, commitTime) => { // Track render times const times = renderTimes.get(id) || []; times.push(actualDuration); renderTimes.set(id, times);
// Warn on slow renders if (actualDuration > 16) { console.warn( `Slow render: ${id} took ${actualDuration.toFixed(1)}ms (target: <16ms for 60fps)` ); }
// Log in development if (process.env.NODE_ENV === 'development') { console.log(`[Render] ${id}: ${actualDuration.toFixed(1)}ms (${phase})`); }};
// HOC to wrap components with profilingexport function withProfiler<P extends object>( Component: React.ComponentType<P>, id: string) { return function ProfiledComponent(props: P) { return ( <Profiler id={id} onRender={onRender}> <Component {...props} /> </Profiler> ); };}
// Get average render time for a componentexport function getAverageRenderTime(id: string): number | null { const times = renderTimes.get(id); if (!times || times.length === 0) return null; return times.reduce((a, b) => a + b, 0) / times.length;}Use in components:
// Wrap performance-critical componentsimport { withProfiler } from '@/lib/react-performance';
const ProductGallery = withProfiler(ProductGalleryBase, 'ProductGallery');const ProductForm = withProfiler(ProductFormBase, 'ProductForm');Long Task Detection
Identify JavaScript that blocks the main thread:
export function monitorLongTasks() { if (!('PerformanceObserver' in window)) return;
const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { // Tasks longer than 50ms are "long tasks" if (entry.duration > 50) { console.warn( `Long task detected: ${entry.duration.toFixed(0)}ms`, entry.name );
// In production, send to analytics if (process.env.NODE_ENV === 'production') { navigator.sendBeacon?.('/api/long-tasks', JSON.stringify({ duration: entry.duration, startTime: entry.startTime, url: window.location.pathname, })); } } } });
observer.observe({ type: 'longtask', buffered: true });}Resource Loading Performance
Track how long resources take to load:
interface ResourceReport { name: string; type: string; duration: number; size: number;}
export function analyzeResourceTiming(): ResourceReport[] { const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
return resources .map((resource) => ({ name: resource.name.split('/').pop() || resource.name, type: resource.initiatorType, duration: resource.duration, size: resource.transferSize, })) .sort((a, b) => b.duration - a.duration);}
export function reportSlowResources(threshold = 1000) { const resources = analyzeResourceTiming(); const slow = resources.filter((r) => r.duration > threshold);
if (slow.length > 0) { console.group('Slow Resources (>1s)'); console.table(slow); console.groupEnd(); }
return slow;}
// Call after page loadwindow.addEventListener('load', () => { setTimeout(reportSlowResources, 0);});Setting Up Real User Monitoring (RUM)
For production monitoring, consider these services:
// Option 1: Google Analytics 4export function initGA4() { // Automatically tracks Core Web Vitals when enhanced measurement is enabled}
// Option 2: Vercel Analyticsexport function initVercelAnalytics() { import('@vercel/analytics').then(({ inject }) => { inject(); });}
// Option 3: Custom RUM endpointexport function initCustomRUM() { const reportPerformance = () => { const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; const paint = performance.getEntriesByType('paint');
const report = { url: window.location.pathname, userAgent: navigator.userAgent, connection: (navigator as any).connection?.effectiveType, timestamp: Date.now(), metrics: { dns: navigation.domainLookupEnd - navigation.domainLookupStart, tcp: navigation.connectEnd - navigation.connectStart, ttfb: navigation.responseStart - navigation.requestStart, download: navigation.responseEnd - navigation.responseStart, domParse: navigation.domInteractive - navigation.responseEnd, domReady: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, load: navigation.loadEventEnd - navigation.loadEventStart, fcp: paint.find((p) => p.name === 'first-contentful-paint')?.startTime, }, };
navigator.sendBeacon?.('/api/rum', JSON.stringify(report)); };
window.addEventListener('load', () => { // Wait for all metrics to be available setTimeout(reportPerformance, 1000); });}Performance Budget Alerts
Set up alerts when metrics exceed thresholds:
interface Threshold { metric: string; warning: number; critical: number;}
const thresholds: Threshold[] = [ { metric: 'LCP', warning: 2500, critical: 4000 }, { metric: 'INP', warning: 200, critical: 500 }, { metric: 'CLS', warning: 0.1, critical: 0.25 }, { metric: 'FCP', warning: 1800, critical: 3000 }, { metric: 'TTFB', warning: 800, critical: 1800 },];
export function checkPerformanceBudget(metric: string, value: number): void { const threshold = thresholds.find((t) => t.metric === metric); if (!threshold) return;
if (value > threshold.critical) { console.error( `🔴 CRITICAL: ${metric} = ${value} exceeds threshold of ${threshold.critical}` ); // Send alert to monitoring service } else if (value > threshold.warning) { console.warn( `🟡 WARNING: ${metric} = ${value} exceeds threshold of ${threshold.warning}` ); }}Shopify-Specific Metrics
Track Shopify-relevant performance:
export function trackShopifyMetrics() { // Time to interactive for cart operations const cartInteractionStart = performance.now();
window.addEventListener('cart:updated', () => { const duration = performance.now() - cartInteractionStart; console.log(`Cart interaction: ${duration.toFixed(0)}ms`); });
// Product page specific metrics if (document.body.classList.contains('template-product')) { // Track time until add-to-cart is interactive const observer = new MutationObserver(() => { const addToCartButton = document.querySelector('[data-add-to-cart]'); if (addToCartButton && !addToCartButton.hasAttribute('disabled')) { console.log(`Add to Cart ready: ${performance.now().toFixed(0)}ms`); observer.disconnect(); } });
observer.observe(document.body, { childList: true, subtree: true }); }}Key Takeaways
-
Measure what matters: Focus on Core Web Vitals—LCP, INP, and CLS. These correlate with user experience and SEO.
-
Use web-vitals library: It provides accurate, standardized measurements.
-
Monitor in production: Lab tests don’t capture real-world conditions. Set up RUM.
-
Set budgets and alerts: Know immediately when performance degrades.
-
Track React-specific metrics: Use Profiler to identify slow components.
-
Identify long tasks: JavaScript that blocks the main thread hurts interactivity.
-
Analyze resource loading: Slow resources compound into slow pages.
-
Continuous monitoring: Performance is not a one-time task. Monitor ongoing.
This concludes the Performance Optimization module. You now have the tools to measure, analyze, and optimize your React Shopify theme for real-world performance. In the next module, we’ll bring everything together with production builds and deployment.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...