LCP measures when the largest visible content element paints, which can happen early even on a JS-heavy page if that content (an image, a block of server-rendered or quickly-rendered text) doesn’t depend on the full JavaScript bundle finishing execution. INP measures something entirely different: how responsive the page is when the user actually interacts with it, later in the session. A large JavaScript payload can sit mostly unexecuted or still hydrating in the background while LCP fires early and cleanly, then still degrade INP significantly once the user clicks or types, because by that point the browser has to parse, compile, and execute all that deferred code, often on a single main thread, exactly when it needs to also respond to input.
Why JS payload and hydration cost hit INP, not LCP
Total JS payload size affects three distinct costs that compound: download time, parse/compile time, and execution time. The first two happen largely up front and can be finished (or close to finished) by the time LCP fires, especially if the LCP element itself doesn’t depend on JS execution completing. The third cost, execution, includes not just initial script run but also everything the framework does afterward: attaching event listeners, running framework reactivity/state management overhead, and specifically hydration in frameworks that render HTML on the server and then “hydrate” it client-side by re-executing component logic to attach interactivity.
Hydration is the critical mechanism here. A server-rendered or statically-rendered page can show real, complete content almost immediately, which is exactly what LCP rewards. But that visible content isn’t actually interactive until the client-side JavaScript finishes hydrating it, attaching the event handlers and framework state that make it respond to clicks. If the hydration workload is large (large component trees, heavy framework overhead, many nested components each doing setup work), it creates substantial main-thread activity that can still be running, or can be triggered right as the user tries to interact, well after the page visually looks “done” and LCP has already been recorded as good.
Concretely: a user sees a fully painted page (good LCP), tries to click a button almost immediately, and the browser is still busy parsing/compiling/executing megabytes of JavaScript or running hydration logic on the main thread. That work is a long task (or several), and input processing has to wait for the main thread to become free, which shows up directly as elevated input delay and processing time, the core components of INP. The bytes that caused this cost were part of “total JS payload,” but their cost never touched LCP because LCP had already fired based on visible paint, not on JS completion.
This is the core reason “fast load” and “fast interactivity” are not the same claim, and why a good LCP score tells you nothing about whether the page will feel responsive once the user actually tries to use it.
There’s also a compounding effect from bundle structure that’s easy to miss. If the JavaScript ships as one large bundle rather than split by route or component, the browser has to parse and compile the entire bundle even for code that supports parts of the page the user hasn’t scrolled to or interacted with yet. Parse and compile cost scales with total bytes shipped, not with bytes actually needed for the current interaction, so a large payload penalizes INP even when most of that code is irrelevant to the specific button the user is trying to click. This is distinct from hydration cost itself; it’s pure parse/compile overhead that happens before any component-level hydration logic even runs, and it happens on the main thread regardless of whether the framework uses eager or lazy hydration strategies.
It’s also worth distinguishing this from a third-party script problem, which produces a superficially similar symptom (good LCP, poor INP) through a different mechanism. Third-party tags, chat widgets, and ad scripts add to “total JavaScript payload” in the aggregate sense, but they aren’t part of your first-party hydration path; they compete for the same main thread independently, often on their own timers or event listeners unrelated to your framework’s hydration cycle. Diagnosing which of these two mechanisms, your own hydration workload versus third-party script contention, is responsible matters because the fixes are completely different: one is an architecture and code-splitting problem, the other is a vendor loading-strategy and governance problem.
Reducing hydration and payload-driven INP cost
Reduce total JS payload where genuinely possible (code splitting, removing unused dependencies, deferring non-critical feature code), since less code means less parse/compile/execution cost overall, even if it doesn’t directly move LCP.
For hydration-heavy frameworks specifically, look at partial/progressive hydration or islands-style architecture patterns (hydrating only the interactive components that need it, rather than the entire page tree at once), since this directly reduces the main-thread work concentrated around the moment the page becomes visually ready and users start trying to interact.
Break remaining necessary JS execution into smaller, yieldable chunks (using techniques like scheduler.yield, setTimeout, or checking isInputPending) rather than one long synchronous block, so that even unavoidable execution work doesn’t monopolize the main thread long enough to delay input processing when a real interaction arrives mid-hydration.
Measure with the web-vitals library’s attribution build or Chrome DevTools’ Performance panel specifically around early-session interactions, since this is where hydration-cost-driven INP problems concentrate, and a generic Lighthouse run (which measures overall performance, not a real interaction at a specific hydration state) won’t surface this pattern reliably.
To distinguish first-party hydration cost from third-party script contention, record a Performance panel trace, interact with the page early, and inspect the long tasks on the main thread during that window. Long tasks attributed to your own framework’s chunk files point to hydration or bundle-parse cost; long tasks attributed to separately-loaded third-party domains point to vendor scripts. Chrome DevTools’ Performance panel groups this by script source and shows self time per task, so the attribution is usually visible directly in the flame chart rather than requiring guesswork.
Check whether your framework supports selective or lazy hydration and whether it’s actually configured, rather than assuming a modern framework automatically avoids the eager, whole-tree hydration pattern described above. Many frameworks default to hydrating the entire component tree on load unless islands-style or resumability-based hydration is explicitly opted into, so the presence of a “modern” framework in the stack doesn’t by itself rule out this failure mode.
Finally, verify with field data rather than lab data alone once a fix ships. Because this pattern concentrates specifically in the first few interactions of a session on real devices with real CPU variance, CrUX’s INP figures (via Search Console or the CrUX API) and RUM attribution data are more trustworthy confirmation than a Lighthouse re-run, which again only simulates one throttling profile and may not reproduce the low-end-device contention where hydration-driven INP problems are most severe.