The usual suspect
When a site fails Interaction to Next Paint in Search Console and passes every lab test, the cause is nearly always a third-party script. Not the framework, not the images, not the server. The tag manager, the chat widget, the session recorder, the consent banner, the A/B testing tool, the heatmap, the retargeting pixel. Each was added by a different person for a good reason, and together they occupy the main thread at the exact moment a real user taps something.
The lab does not see it because the lab does not tap. This is the audit method we use to see it, and the loading patterns that fix it.
What INP actually measures
INP is the delay between a user interaction, a tap, click or key press, and the next frame the browser paints. It is measured for every interaction across the visit and the page reports roughly the worst one. Field data takes the 75th percentile across real users. The threshold for "good" is 200 milliseconds.
An interaction has three phases: input delay, while the main thread finishes whatever it was doing before it can even run your handler; processing, your handler itself; and presentation delay, the time to render the result. Third-party scripts hurt the first phase most. They are the "whatever it was doing" that the tap has to wait for.
Measuring what each tag costs
Start with the field. Search Console tells you INP is failing and on which URL group. Real-user monitoring that attributes INP to the element interacted with, the web-vitals library's attribution build does this, tells you which interaction is slow. Often it is the first one: the menu button, the booking link, the first scroll-triggered click.
Reproduce with the tags on. In Chrome DevTools, record a performance trace on a mid-range device profile with CPU throttling, and perform the slow interaction. Look at the long tasks around it. The trace attributes each to a script URL. That URL is a third party more often than not.
Then measure per tag. Block one third-party domain at a time using the DevTools network request blocking panel, repeat the interaction, and compare. A spreadsheet with one row per tag and the INP cost of each is the most persuasive document a performance audit produces, because it turns "third parties are slow" into "the chat widget costs 140 milliseconds on the first tap".
The ones that hurt most
From the audits we have run, roughly in descending order of damage:
Session replay and heatmap tools. They observe every interaction and DOM mutation, which means they run code on every interaction and DOM mutation. On a React page with frequent re-renders they can dominate the main thread.
Tag managers loading many tags. The container itself is modest. The twenty tags it loads on page view, each initialising, each adding listeners, are not. Tag managers also make it easy for marketing to add a script nobody in engineering reviewed.
Chat widgets. Large bundles, loaded early, often with their own framework, initialising a UI nobody has opened.
Consent management platforms. Ironically among the heaviest, because they must run before other tags, and they often block rendering to do it.
A/B testing tools with anti-flicker snippets. The snippet hides the page until the tool decides which variant to show. That is a deliberate delay on every visit.
Social embeds and video players. Heavy, but usually below the fold and easier to defer.
The loading patterns that keep the tag and lose the cost
Defer past first interaction. Load the script when the user first interacts, or after the page is idle and a few seconds have passed, rather than at page load. Analytics can afford to miss the first two seconds; a chat widget nobody has opened does not need to exist yet.
<script>
const loadTags = () => {
['scroll','click','keydown','touchstart'].forEach(e => removeEventListener(e, loadTags))
const s = document.createElement('script'); s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX'; s.async = true
document.head.appendChild(s)
}
['scroll','click','keydown','touchstart'].forEach(e => addEventListener(e, loadTags, { once: true, passive: true }))
setTimeout(loadTags, 5000)
</script>Load on intent. Render a static chat button. Load the real widget only when it is clicked. The user who wanted chat waits a second; the ninety-five percent who did not pay nothing.
Facade the embeds. A thumbnail with a play button in place of the video player; the player loads on click. Same for maps.
Move analytics off the main thread. Partytown and similar approaches run tags in a web worker. Not every tag survives the move, but the ones that do stop competing with interactions.
Prune the container. Audit every tag in the tag manager. Half are usually dead: campaigns that ended, tools nobody logs into, duplicates. Removing a tag is the only optimisation with a guaranteed result.
Yield in your own handlers. If your click handler does heavy work, break it up with scheduler.yield() or setTimeout so the browser can paint between chunks. This helps the processing phase, and it lets a deferred third-party task interleave rather than block.
Consent banners specifically
A consent platform has to run before anything it gates, so it cannot be deferred in the same way. It can be made cheap: a small first-party banner that stores the choice and loads the heavy platform only if analytics is accepted; the banner's space reserved so it does not shift layout; and no render-blocking behaviour. The platforms that hide the whole page until consent is decided are trading your Core Web Vitals for their default settings.
Proving it worked
Ship the change, and watch the field INP for the affected URL group over the following weeks; CrUX is a 28-day window, so the full effect takes that long to appear. In the meantime, your own RUM data at the 75th percentile shows the change within days. Report the before and after with matched date ranges and the same device class, and attach the per-tag spreadsheet so the next person who wants to add a tag can see what it will cost.
If Search Console says INP is failing and your team has already optimised the code, the tags are where to look. A Core Web Vitals audit that starts from field data and blocks tags one at a time will usually find the culprit in an afternoon.