Optimizing JavaScript for Core Web Vitals Guide

Core Web Vitals measure real user experience. Learn how JavaScript affects LCP, INP, and CLS, and how to optimize your code for better scores.

7 min read

Core Web Vitals are a set of metrics that Google uses to measure real-world user experience. They consist of three scores: Largest Contentful Paint (LCP) for loading speed, Interaction to Next Paint (INP) for responsiveness, and Cumulative Layout Shift (CLS) for visual stability.

JavaScript is often the biggest contributor to poor Core Web Vitals scores. Large scripts block rendering. Long synchronous tasks delay interaction responses.

Dynamically injected content shifts the page layout. Optimizing JavaScript for these metrics means writing code that loads fast, responds quickly, and stays visually stable.

Largest Contentful Paint (LCP)

LCP measures how long it takes for the largest visible content element to appear on screen. This is usually a hero image, a heading, or a large text block. Google considers LCP under 2.5 seconds good.

JavaScript hurts LCP in two ways. First, render-blocking scripts in the document head pause HTML parsing until they download and execute. Second, JavaScript that dynamically creates or modifies the LCP element delays its appearance.

The fix is to minimize JavaScript that runs before the LCP element renders. Move script tags to the end of the body. Use the async or defer attribute so scripts do not block parsing.

Inline critical CSS and the minimal JavaScript needed for above-the-fold content. Defer everything else.

Code splitting reduces the initial JavaScript payload. Instead of shipping one large bundle, split your code into smaller chunks loaded on demand. Modern bundlers like Vite, webpack, and Rollup support this with dynamic import.

javascriptjavascript
const module = await import("./heavy-component.js");

The heavy component loads only when needed, not during the initial page load. This directly reduces the time to LCP.

Interaction to Next Paint (INP)

INP measures the responsiveness of the page to user interactions. It records the time from a user click, tap, or keypress to the next visual update. Google considers INP under 200 milliseconds good.

The main thread can only do one thing at a time. If a long synchronous task is running when the user clicks, the click handler must wait. INP includes both the wait time and the handler execution time.

The fix is to break long tasks into chunks. The browser considers any task over 50 milliseconds a long task. Use setTimeout with a delay of zero to yield control back to the event loop between chunks.

setTimeout(fn, 0) places the continuation at the back of the task queue, so other scripts can run first. Where supported, scheduler.yield() yields the same way but is prioritized to resume sooner, so it is the more current choice for browsers that support it.

javascriptjavascript
function processLargeArray(items) {
  let index = 0;
 
  function chunk() {
    const end = Math.min(index + 500, items.length);
    for (let i = index; i < end; i += 1) {
      items[i] = transform(items[i]);
    }
    index = end;
 
    if (index < items.length) {
      setTimeout(chunk, 0);
    }
  }
 
  chunk();
}

Each chunk runs quickly and then yields. The browser can handle pending clicks and render updates between chunks. The total processing time is the same, but the user never experiences a blocked page.

For complex interactions, consider using a Web Worker to move computation off the main thread entirely. The worker processes data in the background and posts results back.

Cumulative Layout Shift (CLS)

CLS measures how much the page layout shifts during loading. A score of 0.1 or less is good. Layout shifts happen when content is added or resized after the page is already visible.

JavaScript causes CLS when it dynamically inserts content without reserving space. An ad that loads asynchronously and pushes text down.

A banner that appears after a delay. A font that swaps sizes after loading.

The fix is to reserve space for dynamic content. Give containers explicit width and height. Use aspect-ratio boxes for images and embeds.

Avoid inserting content above existing content after the page has loaded.

javascriptjavascript
const container = document.getElementById("ad-slot");
container.style.minHeight = "250px";
loadAd().then((ad) => {
  container.appendChild(ad);
});

By setting a minimum height before the ad loads, the container holds its space. When the ad appears, it fills the reserved space without shifting anything around it.

Measuring Core Web Vitals

Lab tools like Lighthouse provide estimates. Real user monitoring (RUM) provides actual measurements. Both are important.

The web-vitals JavaScript library captures real user metrics and reports them to your analytics. Install it from npm and call it on page load.

For lab testing, Lighthouse in Chrome DevTools audits your page and gives actionable recommendations. The Performance panel shows individual LCP, INP, and CLS events on the timeline.

Measure before you optimize. If your LCP is already under 2.5 seconds, shaving off another 100ms has diminishing returns. Focus on the vitals where your scores are worst.

JavaScript Patterns That Hurt Core Web Vitals

Several common patterns degrade vitals. Large client-side rendering frameworks that ship megabytes of JavaScript to render a page that could be server-rendered block LCP. Synchronous third-party scripts for analytics, ads, or chat widgets that load before first paint delay everything.

Dynamic font loading with JavaScript that triggers layout shifts when fonts swap.

Each pattern has a fix. Server-side render the initial page and hydrate on the client. Load third-party scripts with async or defer.

Use the CSS font-display property instead of JavaScript font loading. Implement virtual scrolling for infinite lists to keep the DOM small.

For profiling techniques to identify these issues, see /javascript/javascript-profiling-advanced-performance-guide. For fixing memory problems, read /javascript/fixing-javascript-memory-leaks-complete-guide.

Rune AI

Rune AI

Key Insights

  • Largest Contentful Paint (LCP) measures loading speed. Minimize render-blocking JavaScript to improve it.
  • Interaction to Next Paint (INP) measures responsiveness. Break long tasks into chunks under 50ms.
  • Cumulative Layout Shift (CLS) measures visual stability. Reserve space for dynamic content loaded by JavaScript.
  • Code splitting, deferring scripts, and using the async attribute reduce main thread blocking.
  • Measure with real user data (RUM) using the web-vitals library, not just lab tools like Lighthouse.
RunePowered by Rune AI

Frequently Asked Questions

What is a good Core Web Vitals score?

Google recommends LCP under 2.5 seconds, INP under 200 milliseconds, and CLS under 0.1. These thresholds are based on the 75th percentile of real user measurements, not lab tests.

Does JavaScript affect all three Core Web Vitals?

Yes. Large JS bundles delay LCP by blocking rendering. Long synchronous tasks increase INP by delaying event handling. JS that injects content after load can cause CLS by shifting page layout.

Conclusion

Core Web Vitals are not abstract metrics. They measure how real users experience your page. JavaScript is often the biggest contributor to poor scores. Reducing bundle size, deferring non-critical code, breaking up long tasks, and reserving space for dynamic content are practical steps that improve all three vitals.