The gap between lab and phone
Lighthouse does not measure Interaction to Next Paint, because it does not interact with the page. Its Total Blocking Time is a proxy, and a page can have modest TBT and still produce 400-millisecond taps for real people, because the slow interaction happens after load, or on an element Lighthouse never touched, or on a device far slower than the emulated one. Desktop RUM often passes too, because a laptop absorbs the work.
Mobile-only INP failures are common on business sites and they are entirely fixable. The trick is to reproduce them under conditions that match the phone, then read what the phone is doing.
Step 1: find out which interaction
You cannot fix "INP is poor". Field attribution tells you which element, which event type, and which phase (input delay, processing, presentation) is slow, and at what loadState. Add web-vitals/attribution reporting to a first-party endpoint if you have not, and wait a week. On most sites the answer is one or two elements: the mobile menu button, the cookie banner's Accept, a filter or search input, a "Book" button that opens a widget.
If you already have a guess and no time to wait, test the guesses below in order of how often they turn out to be right: menu toggle, consent banner, anything that opens a third-party widget, form inputs.
Step 2: reproduce on a phone, or an honest emulation
Lab INP needs an interaction, throttling that matches a mid-range phone, and the timing of a real visit.
On a real device: a mid-range Android connected via USB, Chrome remote debugging, the Performance panel recording on the phone. This is the truth.
Emulated: DevTools Performance panel with CPU throttling at 6x (4x is generous for a mid-range phone), network at Slow 4G, mobile viewport. Then:
- Start recording before navigating.
- Load the page and, as soon as the element is visible, tap it. Do not wait for load to finish; real visitors do not.
- Also tap it again after load completes.
- Stop. Look at the Interactions track: each tap shows its input delay, processing and presentation as a bar. The main-thread flame chart directly below shows what was running.
The first tap, during load, is usually the slow one. That is the phase Lighthouse never measures.
The causes that are phone-specific
Hydration or script execution overlapping the tap. On desktop, the framework hydrates and the scripts run in 150 milliseconds, before anyone taps. On the phone they take 900, and the visitor taps at 400. The tap queues behind them. The flame chart shows a long task (the bundle, the tag manager, a widget's init) spanning the tap.
Long tasks that are short on desktop. A 40-millisecond task on a laptop is 250 on a mid-range phone. Anything near 50 milliseconds in a desktop trace is a long task on mobile. The threshold for "this is fine" is far lower than desktop testing suggests.
Touch-specific handlers. touchstart and touchmove listeners that are not passive block scrolling and delay the tap. A slider or carousel library that listens on the whole document is a common source.
Layout on a narrow viewport. A menu that opens as a full-screen overlay forces layout of a large hidden subtree on mobile, where on desktop it was a small dropdown. Presentation delay dominates.
Memory pressure. Phones with 3 to 4 GB of RAM and several tabs open garbage-collect more often; a big allocation on tap (rendering a large list) triggers a GC pause inside the interaction.
Third parties that behave differently on mobile. Some widgets load a heavier mobile bundle, or defer initialisation to first interaction, so the first tap pays for the whole widget. The chat bubble is the classic case.
The fixes, matched to the phase
Input delay dominates (something else was running):
- Break up the long task the tap waited behind.
scheduler.yield()or chunkedsetTimeoutinside init code you control. - Defer third-party scripts until after the first interaction or after idle.
next/scriptwithlazyOnload; tag manager after load; widgets on click. - Reduce hydration: server components,
'use client'at the leaves, Suspense boundaries so the tapped island hydrates first. - Make the element work before hydration where possible: a menu that opens with a CSS
:checkedorpopoverattribute, enhanced by JavaScript later.
Processing dominates (the handler was slow):
- Do less synchronously. Set state for the visual change and paint; do the heavy part in
startTransition,requestAnimationFrameor after a yield. - Mark touch listeners
{ passive: true }where they do not callpreventDefault. - Debounce input handlers; virtualise lists; avoid synchronous layout reads.
Presentation dominates (the render or paint was slow):
- Reduce what changes. Toggle a class on a small container rather than re-rendering a tree.
content-visibility: autoon large off-screen sections so opening the menu does not lay them out.- Avoid animating layout properties; animate
transformandopacity. - Pre-render the hidden menu or panel so opening it is a display change, not a mount.
Step 3: give feedback first
Whatever the fix, make the tap paint something immediately: a pressed state, the menu's first frame, a spinner. INP measures the next paint after the interaction, not the completion of the work. A 20-millisecond visual acknowledgement followed by 300 milliseconds of work scores well and feels responsive. The same work with no paint until the end scores 320.
Step 4: verify on the same profile, then in the field
Re-record the same tap on the same throttled profile. The Interactions bar should be short and the phases should have shrunk in the one you targeted. Then watch the field p75 for that page group on mobile over two weeks; Search Console follows in 28 days. Keep the attribution running so the next regression names its element.
Where this sits
Mobile-only INP is the case where "test on a mid-range Android" stops being advice and becomes the method. It is the core of any Core Web Vitals engagement where INP is the failing metric, and the first tap during load, on a 6x-throttled profile, is the trace we record before anything else.