
Why Is Your Site Still Failing Interaction to Next Paint (INP) Despite a 100 Lighthouse Score?
Discover why synthetic lab scores often mask real-world unresponsiveness and how to hunt down the long tasks sabotaging your event loop.
Why Is Your Site Still Failing Interaction to Next Paint (INP) Despite a 100 Lighthouse Score?
You’ve spent weeks trimming your bundle size, optimizing images, and chasing that sweet dopamine hit of a perfect 100 Lighthouse score, only to have Google Search Console slap you with a "Needs Improvement" for Interaction to Next Paint (INP). It feels like being told your car is a masterpiece of engineering while the steering wheel comes off in your hands the moment you try to take a turn.
The truth is that Lighthouse is a lab environment—a "clean room" where a simulated user loads your page once and then leaves. Real users, however, are chaotic. They click buttons before the JS has finished hydrating, they scroll while heavy third-party scripts are fighting for dominance, and they stay on your page long after Lighthouse has stopped watching.
The Lighthouse Mirage vs. The Real World
Lighthouse measures Total Blocking Time (TBT), which is a decent proxy for responsiveness during load. But INP is a field metric. It tracks the latency of *every* click, tap, and keyboard interaction throughout the entire lifespan of a user's visit.
If your site is a single-page app (SPA) where users stay for ten minutes, Lighthouse only sees the first ten seconds. It misses the massive, unoptimized onChange handler in your search bar or the heavy re-render that happens when someone toggles a filter.
Lighthouse is a sprint; INP is the entire marathon.
The Main Thread is a Diva
To understand why INP fails, you have to respect the Main Thread. It’s a single-tasking processor that handles everything: parsing HTML, executing JavaScript, calculating styles, and painting pixels.
When a user clicks a button, the browser queues an event. If the Main Thread is busy crunching a massive JSON object or running a complex React reconciliation, that click event just sits there. The "Interaction to Next Paint" is the time from that click until the browser actually shows the user a visual confirmation (like a spinner or a menu opening).
If you have a "Long Task"—anything taking longer than 50ms—you are officially encroaching on the user’s experience.
Finding the Culprits
Stop looking at the Lighthouse report and start looking at the Performance Tab in Chrome DevTools.
1. Open your site.
2. Open DevTools > Performance.
3. Hit "Record."
4. Interact with the UI like a frustrated user (click things, toggle menus).
5. Stop recording and look for the red bars.
Red bars at the top of the "Main" section are Long Tasks. If you click on one, the "Bottom-Up" tab will usually tell you exactly which function call is the bottleneck. More often than not, it's a third-party script like a chatbot or an analytics suite that decided to initialize right when the user tried to do something important.
How to Stop Blocking the UI
Once you've identified a heavy function, you need to break it up. You need to "yield" back to the main thread so the browser can breathe and process user input.
The Old Way: setTimeout
We used to do this, which works but feels a bit hacky:
function processData(items) {
items.forEach((item, index) => {
doExpensiveWork(item);
// Every 50 items, give the browser a chance to paint
if (index % 50 === 0) {
setTimeout(() => {}, 0);
}
});
}The Modern Way: scheduler.yield()
If you're targeting modern browsers, scheduler.yield() is the superior tool. It allows you to pause your execution, let the browser handle any pending interactions, and then resume right where you left off.
async function handleHeavyClick() {
const data = await fetchData();
// Do some work
renderPartOne(data);
// Check if we need to yield to the browser
if (typeof scheduler !== 'undefined' && scheduler.yield) {
await scheduler.yield();
} else {
// Fallback for older browsers
await new Promise(resolve => setTimeout(resolve, 0));
}
// Resume work
renderPartTwo(data);
}The "Gotcha" of Third-Party Scripts
I’ve seen dozens of sites with perfect CSS and hand-optimized JS fail INP because of a Tag Manager container.
Third-party scripts are notorious for running heavy tasks at unpredictable times. Since you can't usually edit their code to add scheduler.yield(), your best bet is to defer them aggressively. Don't just async your scripts; consider using a library like Partytown to run them in a Web Worker, or use requestIdleCallback to load them only when the main thread is literally doing nothing else.
Why "100" Isn't Enough
The 100 score in Lighthouse is a baseline. It means your "technical SEO" is solid and your initial load is fast. But INP is a measure of user frustration.
If your site feels "janky" or "stuck" when a user clicks a button, they don't care that your LCP was 1.2 seconds. They care that the button didn't work. To fix INP, you have to stop thinking about the moment the page loads and start thinking about the entire time the user is there.
Break up your long tasks, yield to the main thread, and remember: the user's input is always more important than your JavaScript's internal housekeeping.
