What INP actually measures
INP is not an average and not a page-load metric. It observes every interaction over the page's lifetime — clicks, taps, key presses — measures each from input to the next paint reflecting the result, and reports approximately the worst one. That design matters here: a script that stalls the thread once, for a third of a second, at an unlucky moment will move INP substantially even though the median interaction is unchanged.
Each interaction decomposes into three parts, and naming them is what turns "the page feels slow" into a diagnosis:
Input delay — from the user's action until the event handler starts. This is time spent waiting for the main thread to become free. A third-party script executing a long task lands here.
Processing time — the handler itself running.
Presentation delay — from the handler finishing until the browser paints the next frame, covering layout, style and paint work.
A third-party tag almost always inflates input delay, because the user clicked while the thread was busy executing someone else's code. If your measurement shows processing time growing instead, the tag is probably not the cause and you should be looking at your own handlers or at listeners the tag attached.
Getting evidence the argument cannot survive
Lab testing frequently shows nothing, and it is important to explain why before someone waves a green Lighthouse score. Lab runs load the page and often never interact with it, so INP is not produced at all. Tag managers commonly serve different payloads by geography, consent state, or campaign, so the script in a synthetic run may not be the script real users get. And a one-off run cannot see a p75 of the worst interaction across a population.
So the evidence has to come from the field, using the Event Timing API to attribute the delay:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId && entry.duration > 200) {
report({
duration: entry.duration,
inputDelay: entry.processingStart - entry.startTime,
processing: entry.processingEnd - entry.processingStart,
presentation: entry.startTime + entry.duration - entry.processingEnd,
target: entry.target?.tagName,
});
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });
Pair that with a Long Animation Frame observation, which reports the script attributed to a blocking frame — including its source URL. That is the piece that closes the argument, because it names the file rather than implying it.
The presentation that works with a non-engineering stakeholder is a before-and-after of field p75 INP with the input-delay component broken out, alongside the deploy date of the tag. Two lines and a vertical marker. Nobody has to accept a theory about the event loop.
Fixing it without removing the tag
Removing the script is often not available to you, so have the alternatives ready.
Load it when the thread is idle. Most tags do not need to run during the interactive window. Loading on requestIdleCallback, after the first meaningful interaction, or on a delay moves the execution out of the period where users are clicking. Tag managers usually support a trigger for this and it is the single highest-value change.
Yield inside long tasks you control. If the work is yours — an analytics wrapper, a consent handler — break it up so the browser can service input between chunks:
async function processQueue(items) {
for (const item of items) {
handle(item);
if (navigator.scheduling?.isInputPending?.()) {
await scheduler.yield?.() ?? new Promise( => (r, ));
}
}
}
Yielding does not reduce total work; it reduces the length of the longest uninterruptible block, which is what INP is sensitive to.
Move it off the main thread. Work that does not touch the DOM — hashing, serialisation, batching — belongs in a web worker. Some vendors offer worker-based or partytown-style sandboxing for exactly this.
Constrain what it may do. If the tag attaches listeners to document for click tracking, those listeners run on every interaction and add to input delay permanently. Scoping or replacing them with a passive delegated listener you own is often a bigger win than the load timing.
Stopping the next one
The systemic fix is a budget with a gate. Set a performance budget on total blocking time and third-party script weight, enforce it in CI for code you control, and require a field-data review after any tag manager change. The reason the regression got in was that a change to production behaviour bypassed the engineering pipeline entirely — the durable answer addresses that path, not just this script. Proposing it makes the difference between fixing an incident and removing a category of incident.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).