Performance Optimization Intermediate 10 min read

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:

Terminal window
npm install web-vitals
src/lib/web-vitals.ts
import { 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:

src/main.tsx
import { initWebVitals } from '@/lib/web-vitals';
// Start measuring after page load
if (typeof window !== 'undefined') {
initWebVitals();
}

Building a Performance Dashboard

Create a debug panel to view metrics during development:

src/components/debug/PerformancePanel.tsx
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>
);
}
PerformancePanel.module.css
.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:

src/lib/react-performance.ts
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 profiling
export 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 component
export 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 components
import { 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:

src/lib/long-tasks.ts
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:

src/lib/resource-timing.ts
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 load
window.addEventListener('load', () => {
setTimeout(reportSlowResources, 0);
});

Setting Up Real User Monitoring (RUM)

For production monitoring, consider these services:

src/lib/rum.ts
// Option 1: Google Analytics 4
export function initGA4() {
// Automatically tracks Core Web Vitals when enhanced measurement is enabled
}
// Option 2: Vercel Analytics
export function initVercelAnalytics() {
import('@vercel/analytics').then(({ inject }) => {
inject();
});
}
// Option 3: Custom RUM endpoint
export 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:

src/lib/performance-alerts.ts
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:

src/lib/shopify-metrics.ts
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

  1. Measure what matters: Focus on Core Web Vitals—LCP, INP, and CLS. These correlate with user experience and SEO.

  2. Use web-vitals library: It provides accurate, standardized measurements.

  3. Monitor in production: Lab tests don’t capture real-world conditions. Set up RUM.

  4. Set budgets and alerts: Know immediately when performance degrades.

  5. Track React-specific metrics: Use Profiler to identify slow components.

  6. Identify long tasks: JavaScript that blocks the main thread hurts interactivity.

  7. Analyze resource loading: Slow resources compound into slow pages.

  8. 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...