Understanding Modern Web Performance Metrics: Core Web Vitals Explained
- Published on
- Gyanaranjan Sahoo--5 min read
Overview
- Introduction
- Core Web Vitals
- Tools We Use for Monitoring
- Real-World Optimizations
- Monitoring and Reporting
- Conclusion
- Additional Resources
Introduction
Hey there! 👋 After spending countless hours optimizing web applications and seeing firsthand how performance impacts user experience and business metrics, I wanted to share my practical insights on modern web performance metrics.
Core Web Vitals
Before we dive into the code, let's understand why these metrics matter. Google uses Core Web Vitals as ranking signals, and from my experience, they directly correlate with user engagement.
Largest Contentful Paint (LCP)
During the optimization of our product landing pages, LCP was our biggest challenge. Trust me, I've been there - staring at waterfall charts at 2 AM trying to figure out why that hero image is taking forever to load. Here's how we tracked and improved it:
// Measuring LCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP:', entry.startTime);
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
Here's what actually worked for us in production (spoiler: it's all about priorities):
// Our optimized hero section implementation
function OptimizedHero() {
useEffect(() => {
// Preload critical assets
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.as = 'image';
preloadLink.href = '/hero-image.webp';
document.head.appendChild(preloadLink);
}, []);
return (
<div className="hero-section">
{/* Quick tip: Always set width/height to prevent layout shifts */}
<Image
src="/hero-image.webp"
alt="Hero"
width={1200}
height={630}
priority
/>
</div>
);
}
💡 Pro Tip: Don't just blindly copy-paste this code. The key is understanding that LCP improvement is about:
- Identifying your LCP element (use Chrome DevTools!)
- Prioritizing its loading
- Optimizing its delivery
Let me tell you about the time we shaved off 2 seconds from our LCP just by moving our hero image to a CDN edge location. Sometimes the simplest solutions have the biggest impact!
Ideal LCP scores that we aim for:
- Good: Under 2.5s (We consistently achieve this on desktop)
- Needs Improvement: 2.5s - 4s (Our mobile scores sometimes fall here)
- Poor: Above 4s (Red alert if we ever see this!)
First Input Delay (FID)
FID became crucial when we noticed users abandoning our checkout process. Here's how we monitor it:
// Measuring FID
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
}
}).observe({ type: 'first-input', buffered: true });
You know what's funny? We spent weeks optimizing our JavaScript bundles, only to find that a single third-party analytics script was causing most of our FID issues. Here's how we fixed it:
// Before: Blocking main thread 🚫
<script src="heavy-analytics.js">
// After: Defer non-critical scripts ✅
<script src="heavy-analytics.js" defer>
// Even better: Load conditionally
const loadAnalytics = () => {
if (isUserIdle && !isPrioritaryPage) {
import('./heavy-analytics.js');
}
};
Here's a real game-changer we implemented - using web workers for heavy computations:
// Moving price calculations off the main thread
const priceWorker = new Worker('/workers/price-calculator.js');
priceWorker.postMessage({
items: cart.items,
discounts: activePromotions
});
priceWorker.onmessage = (event) => {
updateTotalPrice(event.data.total);
};
🎯 Our FID Targets:
- Good: < 100ms (Where we are now after optimizations)
- Needs Improvement: 100ms - 300ms
- Poor: > 300ms (What triggered our optimization journey)
💡 Quick Tips That Actually Worked:
- Split JavaScript bundles by route
- Use requestIdleCallback for non-critical operations
- Keep event handlers light - defer complex logic
- Profile your app during high-traffic periods Would you believe we once had a 500ms FID because of a complex regex validation running on every keystroke? Moving it to a debounced worker dropped it to under 50ms!
Cumulative Layout Shift (CLS)
This was particularly problematic on our product pages where dynamic content loads frequently. Here's our solution:
// Measuring CLS
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('CLS:', entry.value);
}
}).observe({ entryTypes: ['layout-shift'] });
// Our production solution for product images
function ProductImage({ src, alt }) {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="aspect-ratio-box" style={{ aspectRatio: '4/3' }}>
<Image
src={src}
alt={alt}
layout="fill"
objectFit="contain"
onLoadingComplete={() => setIsLoading(false)}
/>
{isLoading && <Skeleton />}
</div>
)
}
Real-World Gotchas
One thing I've learned the hard way is that theoretical optimizations don't always translate to real-world improvements. Here are some surprising findings:
Tools We Use for Monitoring
In our production environment, we rely on:
- Lighthouse CI
- Integrated into our GitHub Actions
- Blocks PRs that degrade performance
- Custom thresholds based on our needs
// Our Lighthouse CI config
module.exports = {
ci: {
collect: {
numberOfRuns: 3,
},
assert: {
assertions: {
'first-contentful-paint': ['warn', {maxNumericValue: 2000}],
'interactive': ['error', {maxNumericValue: 3500}],
},
},
},
};
Real-World Optimizations
Here are some practical optimizations that made the biggest impact in our projects:
Image Optimization
// Our production image component
export function OptimizedImage({ src, alt, ...props }) {
return (
<div className="image-wrapper">
<Image
src={src}
alt={alt}
loading="lazy"
placeholder="blur"
{...props}
/>
</div>
)
}
Code Splitting
// How we handle route-based code splitting
const ProductPage = dynamic(() => import('./pages/Product'), {
loading: () => <ProductSkeleton />,
ssr: true
})
Monitoring and Reporting
We set up a custom monitoring system that alerts us when metrics degrade:
export function monitorWebVitals(metric) {
const { id, name, value } = metric;
if (value > THRESHOLDS[name]) {
notifyTeam(`Performance degradation detected in ${name}`)
}
// Send to our analytics
analytics.track('Web Vital', {
metric: name,
value: Math.round(value),
page: window.location.pathname
});
}
Conclusion
After implementing these optimizations across multiple projects, I've seen consistent improvements in both user experience and business metrics. The key is to start measuring, set realistic goals, and iterate based on real user data.
Remember, performance optimization is an ongoing process, not a one-time task. Keep monitoring, keep optimizing, and most importantly, keep focusing on the metrics that matter most to your users.