loke.dev
Header image for Fix Poor INP and LCP: A Data-Driven Performance Framework
Web Performance Frontend Engineering Core Web Vitals Debugging

Fix Poor INP and LCP: A Data-Driven Performance Framework

Stop chasing Lighthouse scores. Learn to debug real-user performance data to fix poor INP and LCP scores using Chrome DevTools and field-based diagnostics.

Published 5 min read

High Lighthouse scores are vanity metrics. I’ve seen sites with a 99 performance score fail every Core Web Vitals target in the wild.

Why? Because Lighthouse runs on your tricked-out M3 MacBook, over a clean fiber connection, with a cold cache. Your users are on a crusty budget Android in a tunnel with one bar of 4G. If you want to fix poor INP and LCP, stop obsessing over lab scores and look at the Chrome User Experience Report (CrUX). Lab data is for CI gates; field data is for building.

Lighthouse is a Hallucination

Lighthouse is a snapshot of an idealized state that doesn't exist. Real-world performance is a brutal mix of CPU throttling, bloated third-party scripts, and spotty network reception.

When you chase a green Lighthouse score, you usually optimize for the wrong things—like deferring scripts that shouldn’t be moved or over-compressing images that weren't the problem. The transition from FID to Interaction to Next Paint (INP) wasn't just a rename; it was an admission that measuring the first tap is useless. If a site feels snappy for five seconds and then chokes for two when the user tries to click a button, your site is broken.

Stop asking, "How do I get a 100 in Lighthouse?" Start asking, "What is the 75th percentile of my users actually seeing?"

Debugging INP: Identifying Interaction Bottlenecks

INP measures the latency of every interaction. If your INP is above 200ms, you have "Long Tasks" parking on the main thread and refusing to move.

Stop guessing. Open the Chrome DevTools Performance tab. Record an interaction—a button click, a menu toggle—and look for the red bars in the "Main" track. Those are your bottlenecks. Anything over 50ms is a Long Task you need to break up.

I recently audited a checkout page where the "Add to Cart" button clocked in at 650ms. I opened the Performance tab and saw that the onClick handler was triggering a massive state re-render, deep-cloning an object, and calculating tax on 200 items in a single loop.

I didn't reach for a new framework. I just chopped the task up.

// Before: The blocking long task
const addToCart = (items) => {
  const result = heavyCalculation(items); // Blocks thread for 400ms
  updateUI(result);
};

// After: Using setTimeout to yield to the main thread
const addToCart = (items) => {
  setTimeout(() => {
    const result = heavyCalculation(items); 
    updateUI(result);
  }, 0);
};

Forcing the browser to yield makes the interaction feel instant. The math takes the same amount of time, but the UI thread stays responsive, dropping the INP from 650ms to 80ms. It’s a simple trick, and it works every time.

LCP Optimization: Mapping Asset Delivery to Perception

LCP is usually an image or a block of text. If you're lazy-loading your LCP element, you've already lost the battle.

The most common mistake I see? Preloading everything. Preloading every hero image on your homepage wastes bandwidth and creates a priority war. Identify exactly which asset is your LCP. If it’s the hero image, prioritize it.

<link rel="preload" as="image" href="/hero-banner.webp" fetchpriority="high">

I moved an LCP from 4.0s to 1.2s for a client by doing two things:
1. Removing the image from the JS bundle: It was being injected by a React component that took 800ms to mount. I moved it to static HTML.
2. TTFB Optimization: I added a Server-Timing header to debug. The database query for the hero banner took 600ms. We cached it at the edge, dropping TTFB from 700ms to 50ms.

Fixing INP and LCP is 80% about deleting invisible work that happens before the first pixel lands.

Solving the Third-Party Script Problem

Third-party scripts are the black box that kills performance. Analytics, chat widgets, and A/B testing suites are usually the primary culprits. They run on the main thread, and they don't give a damn about your performance metrics.

If you can’t strip them out, isolate them. Don't let your chat widget execute on load. Use a click-to-load pattern. The user doesn't need a support chatbot the millisecond the page renders.

const chatButton = document.querySelector('#open-chat');

chatButton.addEventListener('click', () => {
  const script = document.createElement('script');
  script.src = 'https://third-party-chat.com/widget.js';
  document.body.appendChild(script);
}, { once: true });

This simple change can shave 300ms off your main thread blocking time. Stop paying for scripts your users haven't touched.

Field-Based Monitoring

You can’t fix what you don't measure in the wild. If you rely on Lighthouse, you're navigating by a map of a city you visited three years ago. Use the web-vitals library to feed real-world metrics into your analytics.

import { onLCP, onINP } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({ [metric.name]: metric.value });
  navigator.sendBeacon('/analytics', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);

Once you have this, the truth emerges. You’ll see that while your desktop builds look fine, 20% of your iOS users are seeing an LCP of 5s. That’s your actual work.

Stop the Layout Shift (CLS)

Most developers fix CLS with min-height hacks. That’s a band-aid. Use CSS aspect-ratio to force the browser to reserve the space before the content loads.

.hero-image {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

Hierarchy of Fixes

1. Prioritize LCP. It’s the visual anchor. If it looks loaded, users will wait.
2. Attack the Main Thread. INP is a measure of how much garbage your JS is throwing at the CPU. Find the Long Tasks and break them up.
3. Audit third-party bloat. If you have five+ external scripts in your <head>, you've already failed.

The industry average of 48% of sites hitting 'Good' scores isn't because the technology is hard. It's because developers build for their high-speed office internet, not for the reality of a mobile device. Build for the 75th percentile, stop loading dead weight, and measure the field.

Resources