Mastering Core Web Vitals with JavaScript

Learn how to measure, optimize, and monitor Core Web Vitals using JavaScript. Discover the patterns that destroy performance and the techniques that fix them.
Mastering Core Web Vitals with JavaScript
While I was looking over some performance metrics for a client project the other day, I realized something that made me cringe. The site was beautifully designed, had clean code, and followed all the best practices I could think of—yet it was failing miserably on Core Web Vitals. Little did I know that some of my "optimization" attempts were actually making things worse.
I was once guilty of thinking that performance optimization was just about making code run faster. But Core Web Vitals taught me that it's really about making users feel like the site is fast. And that's a completely different challenge.
Understanding Core Web Vitals in 2026
Core Web Vitals have become the gold standard for measuring user experience on the web. When I first encountered them back in 2020, I'll admit I was overwhelmed. But after years of working with these metrics, I've come to appreciate how brilliantly they capture what actually matters to users.
The metrics focus on three critical aspects of the user experience. Loading performance, interactivity, and visual stability. In other words, how fast does content appear, how quickly can users interact with it, and does the page jump around unexpectedly?
What's fascinating about 2026 is that these metrics have evolved. The thresholds are stricter, the measurement is more accurate, and search engines are paying closer attention than ever before.
The Three Pillars: LCP, INP, and CLS Explained
Let me break down what we're actually measuring here because this is where many developers get confused.
Largest Contentful Paint (LCP) measures how long it takes for the largest visible element to render. This could be a hero image, a video thumbnail, or a large text block. The target? Under 2.5 seconds. When I finally decided to optimize LCP on one of my projects, I discovered it wasn't about the total page load time—it was about getting that main content visible as quickly as possible.
Interaction to Next Paint (INP) replaced First Input Delay and it's much more comprehensive. It measures the time from when a user interacts with your page until the next paint occurs. This covers clicks, taps, and keyboard interactions. The goal is under 200 milliseconds. I cannot stress this enough—this is where JavaScript performance really matters.
Cumulative Layout Shift (CLS) tracks unexpected layout movements. Ever clicked a button only to have an ad load and shift it down, making you click the wrong thing? That's what CLS measures. You want a score below 0.1.

Measuring Web Vitals with the web-vitals.js Library
Here's where things get practical. Google provides an excellent library called web-vitals that makes measuring these metrics straightforward. Let me show you how I set this up in my projects:
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
// This is where you'd send data to your analytics service
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
});
// I usually send this to a custom endpoint
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', body);
} else {
fetch('/api/analytics', {
body,
method: 'POST',
keepalive: true,
});
}
}
// Measure each Core Web Vital
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);This code snippet does something wonderful—it measures real user experiences, not just synthetic lab tests. When I first implemented this, I was shocked to discover that my lab scores were great, but real users were having a completely different experience.
The rating field is particularly useful. It tells you whether the metric is "good", "needs-improvement", or "poor" based on Google's thresholds. Luckily we can use this to prioritize which issues to tackle first.
JavaScript Patterns That Destroy Your Core Web Vitals
Let me share some patterns I was guilty of using that absolutely tanked my Core Web Vitals. These are common mistakes that look harmless but have devastating effects.
Pattern 1: Loading Third-Party Scripts Synchronously
I used to do this all the time:
// DON'T DO THIS
<script src="https://example.com/analytics.js"></script>
<script src="https://example.com/chat-widget.js"></script>
<script src="https://example.com/social-sharing.js"></script>Each script blocks the parser. Your LCP suffers because the browser can't render anything until these complete. The fix? Use async or defer, or better yet, lazy load them after the critical content renders.
Pattern 2: Massive JavaScript Bundles
I came across a project where the main bundle was 800KB. Wonderful code quality, but it destroyed INP because the browser spent so much time parsing and executing JavaScript that interactions felt sluggish.
Pattern 3: Layout Shifts from Lazy Loaded Images
Here's a mistake I made early on—loading images without dimensions:
// This causes CLS issues
<img src="hero.jpg" alt="Hero image" />
// Better approach
<img
src="hero.jpg"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
/>Always specify dimensions. Always. The browser needs to reserve space before the image loads.
Optimizing Largest Contentful Paint with Resource Hints
When I finally understood resource hints, my LCP scores improved dramatically. Here's what actually works:
Use preconnect for critical third-party origins. If your LCP element is hosted on a CDN, establish that connection early:
<link rel="preconnect" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />For the LCP resource itself, use preload:
<link
rel="preload"
as="image"
href="https://cdn.example.com/hero.jpg"
imagesrcset="hero-320w.jpg 320w, hero-640w.jpg 640w, hero-1200w.jpg 1200w"
imagesizes="100vw"
/>I implemented this on a client site and watched LCP drop from 4.2 seconds to 1.8 seconds. The difference was remarkable.

Reducing Interaction to Next Paint Through Event Optimization
INP is where JavaScript developers can make the biggest impact. Here's what I learned the hard way:
Long tasks are your enemy. Any JavaScript execution that takes longer than 50ms will hurt your INP. I realized I needed to break up my work into smaller chunks.
Here's a technique I use for heavy computations:
async function processLargeDataset(data: any[]) {
const chunkSize = 100;
const results = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
// Process this chunk
const chunkResults = chunk.map(item => heavyTransform(item));
results.push(...chunkResults);
// Yield to the browser between chunks
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
// For event handlers, debounce or throttle appropriately
function createOptimizedHandler() {
let timeoutId: number;
return function handleInput(event: Event) {
// Cancel pending work
if (timeoutId) {
clearTimeout(timeoutId);
}
// Schedule new work
timeoutId = setTimeout(() => {
// Keep this under 50ms
const result = processInput(event.target.value);
updateUI(result);
}, 150);
};
}The setTimeout trick yields control back to the browser. This keeps the main thread responsive and prevents INP spikes. When I implemented this pattern in a search-as-you-type feature, INP improved from 380ms to 145ms.
Monitoring and Debugging Web Vitals in Production
Lab testing only tells you half the story. I cannot stress this enough—you need real user monitoring. Here's my approach:
Set up alerts for when metrics cross thresholds. I use a simple system where if more than 25% of users experience "poor" ratings, I get notified immediately.
Use Chrome DevTools Performance panel to identify long tasks. Look for the red triangles—those indicate tasks over 50ms. I spent hours in this panel debugging a dropdown that was causing 600ms INP spikes. The culprit? Unnecessary re-renders on every keystroke.
Implement feature flags so you can quickly disable problematic features. I once had to roll back a "helpful" animation that looked great but destroyed CLS scores.
Building a Performance-First Development Workflow
Let me share the workflow that's saved me countless hours of retrofitting performance fixes.
First, I measure before building. I set performance budgets for each page. For example, "LCP must be under 2.0s, INP under 150ms, CLS under 0.05." If a feature would push us over budget, we either optimize it or cut it.
Second, I test on real devices. Your MacBook Pro doesn't represent your users. I keep a budget Android phone on my desk specifically for testing. The performance difference is eye-opening.
Third, I automate monitoring. Every deploy includes a Lighthouse CI check. If Core Web Vitals regress, the deploy fails. This has prevented so many performance regressions.
Fourth, I prioritize progressive enhancement. Load the minimum viable experience first, then enhance. This naturally leads to better Core Web Vitals because the critical path is lean.
Wrapping Up
Core Web Vitals transformed how I think about performance. It's not about perfection in every metric—it's about delivering a consistently good experience to real users. The techniques I've shared here are the ones that moved the needle most significantly in my projects.
Start with measurement. You can't improve what you don't measure. Implement the web-vitals library today and see where you stand. Then tackle the biggest problems first. Usually, that means optimizing your LCP element and breaking up long-running JavaScript.
Remember, these metrics correlate directly with user satisfaction and conversion rates. Every 100ms improvement in INP can increase engagement. Every reduction in CLS prevents user frustration. The ROI is real and measurable.
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!