A failing WordPress INP score is a JavaScript problem, not a hosting problem. Something on the page is holding the main thread when a visitor taps, and fixing it means finding which interaction and which script — not installing another caching plugin, because page caching has finished its work before the first click ever happens.
Interaction to Next Paint replaced First Input Delay as a Core Web Vital in March 2024, and it is a much harder metric to pass. FID measured only the delay before the first event handler started. INP measures every click, tap and key press on the page, from the moment of input to the moment the browser paints the frame that responds to it. The mega-menu, the cookie banner, the variable-product dropdown and the quantity stepper all count now, and the worst one is the one you are graded on.
The short version
What WordPress INP actually measures
Every measured interaction is split into three phases, and knowing which one dominates decides what you fix. Guessing at this is how people spend a weekend on image compression and move the number by nothing.
| Rating | INP at the 75th percentile | What it means in practice |
|---|---|---|
| Good | 200 ms or less | A quarter of your visits can still be slower than this and you pass |
| Needs improvement | Above 200 ms, up to 500 ms | Usually one identifiable script. Fixable in an afternoon |
| Poor | Above 500 ms | A visible lag on tap. Normally a page builder bundle or a consent script |
Two details in how the score is calculated matter when you read a report. The page’s INP is the worst interaction, except that one high interaction is discarded for every 50 the visitor makes — so a single unlucky click on a busy page does not define you, but on a page with three clicks it does. And a page with no interactions reports no INP at all. That is why a text-heavy blog can pass effortlessly while the product pages on the same domain fail: readers scroll, and scrolling is not measured.
The detail that surprises most developers: asynchronous work after the handler returns is not counted. If your add-to-cart handler kicks off a request and paints a spinner in the same frame, INP ends at the spinner. A 900 ms response from admin-ajax.php after that is a real problem for your conversion rate and shows up nowhere in your Core Web Vitals. Conversely, a handler that synchronously rebuilds a cart table before yielding will fail INP even on a fast server.
Why caching plugins do not fix INP
Page caching, object caching and a CDN all shorten the time before the HTML arrives. INP happens after that, on the visitor’s CPU, when they touch the page. A full-page cache can take time to first byte from 800 ms to 80 ms and leave INP sitting at 540 ms, because the same 300 KB of plugin JavaScript still parses and the same click handler still runs.
There is one narrow exception worth knowing. Removing render-blocking scripts from the head genuinely reduces input delay for clicks that happen while the page is still loading, which is a real slice of mobile traffic. That is a load-order win, not a caching win, and it does nothing for a handler that takes 400 ms once the page is idle. If you are choosing a cache for other reasons, which WooCommerce caching plugins break what is the more useful question, and the older roundup of WordPress caching plugins covers the general-purpose options.
“Delay JavaScript until user interaction” can make INP worse
Find the slow interaction before you change anything
Four steps, in this order. Skipping to step four is how you end up optimizing the wrong page.
1. Confirm the problem exists in the field
Run the URL through PageSpeed Insights and read the top section only — the Chrome User Experience Report data from real visitors over a rolling 28-day window. The Lighthouse audit underneath it does not report INP at all, because a lab run has no interactions to measure; that is why it shows Total Blocking Time instead. Treat Total Blocking Time as a hint and nothing more.
CrUX gives you a page-level number and no cause. It also only includes Chrome users who opted into telemetry, so a low-traffic page may show nothing at all. That is not a pass.
2. Name the script with Long Animation Frames
This is the single most useful thing in the modern performance toolkit and it needs no plugin. Open the page in Chrome, paste this into the console, then click around the way a visitor would — the menu, the filters, the add-to-cart button.
// Reports every rendering update delayed past 50 ms,
// and attributes it to the scripts that caused it.
new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
if (frame.duration < 100) continue; // raise this to cut the noise
console.groupCollapsed(
`${Math.round(frame.duration)}ms frame, ` +
`blocking ${Math.round(frame.blockingDuration)}ms`
);
for (const script of frame.scripts) {
console.log(
Math.round(script.duration) + 'ms',
script.invokerType,
script.invoker,
script.sourceURL,
script.sourceFunctionName
);
}
console.groupEnd();
}
}).observe({ type: 'long-animation-frame', buffered: true });
A long animation frame is a rendering update delayed beyond 50 ms — the same threshold the older Long Tasks API used, except that LoAF adds up all the tasks in one frame, so it catches the case where six 30 ms tasks combine into a janky click. For any script running more than 5 ms it gives you sourceURL, sourceFunctionName and invoker, and that sourceURL is normally a path under /wp-content/plugins/ that ends the investigation.
Two honest limits. Attribution reports the entry point, so if your theme’s click handler calls a slow library, you will see the handler rather than the library. And the API shipped in Chrome 123 and Edge 123, with no support in Firefox or Safari — your iPhone traffic is invisible to it, which is a reason to reproduce on an Android device or with CPU throttling rather than to assume Safari is fine.
3. Collect attribution from real visitors
Your own clicks are not a sample. The attribution build of Google’s web-vitals library records which element was interacted with and how the latency split across the three phases, for actual traffic.
import { onINP } from 'web-vitals/attribution';
onINP(({ value, rating, attribution }) => {
navigator.sendBeacon('/wp-json/wpfairs/v1/inp', JSON.stringify({
value,
rating,
url: location.pathname,
target: attribution.interactionTarget, // a CSS selector
type: attribution.interactionType, // pointer or keyboard
inputDelay: attribution.inputDelay,
processing: attribution.processingDuration,
presentation: attribution.presentationDelay,
}));
});
Ship it from a small must-use plugin rather than your theme, keep it to logged-out visitors so your own admin sessions do not pollute the data, and sample a fraction of traffic rather than all of it. A register_rest_route endpoint that appends to a log table is enough; you need a few hundred data points, not an analytics product. After a week, sort by target and the argument about which button is slow is over.
4. Reproduce it in DevTools
Now that you know the page and the element, open the Performance panel, set CPU throttling to 4× — a mid-range Android phone, not your laptop — record, perform the interaction, and stop. The INP breakdown insight splits the longest interaction into the three phases and tells you which one to attack. Record several different interactions, because the worst one is rarely the one you assumed.
Test logged out, in a clean profile, with no extensions. The admin bar, the block editor’s assets and your ad blocker all change the result, and a logged-in test is a test of a page no visitor sees.
The scripts that usually cause it on WordPress
After the diagnosis, the culprit is nearly always on this list. The confirmation column is what to look for in the LoAF output from step two.
| Suspect | Why it hurts INP | How to confirm | What to do |
|---|---|---|---|
| Page builder front-end bundle | Widget initialisation and jQuery handlers load on every page whether the page uses them or not | sourceURL under /plugins/elementor/assets/js/ or the equivalent builder path | Drop the addon packs first — they are usually the bulk. Rebuild the two or three highest-traffic templates in blocks |
| Cookie consent or CMP | Accepting rewrites the DOM and then loads every deferred tag in one frame | The INP target is the accept button itself | Pick a CMP that completes in one frame, and never chain a tag manager’s full payload to the accept click |
| Tag manager and analytics stack | Third-party tasks compete for the main thread, inflating input delay on unrelated clicks | invokerType of user-callback with a cross-origin sourceURL and no function name | Audit the container and delete tags nobody reads. Move what you can server-side |
| Live chat or support widget | A large bundle, usually enqueued site-wide for a feature used on two pages | sourceURL on the vendor’s domain | Replace with a static button that loads the widget on first click |
| Mega menus and sliders | Handlers read layout after writing styles, forcing synchronous layout on tap | Presentation delay dominates the breakdown | CSS-only dropdowns. One slide, or none |
| WooCommerce variation and add-to-cart handlers | The handler rewrites price and availability markup, then triggers a cart fragment refresh | Target is .single_add_to_cart_button or the variation select | Paint the visual response in the first frame and let the network work finish afterwards |
| Client-rendered “related posts” and infinite scroll | Building a large HTML chunk in JavaScript blocks the browser until parsing and rendering finish | Long presentation delay with a theme or plugin script in the frame | Render it server-side with a Query Loop, or chunk it |
Builders deserve a fair hearing here: none of them automatically fails INP, and a well-built Elementor page with no addons can pass comfortably. What fails is the accumulation — a builder, plus six addon packs, plus a slider, plus a popup plugin, each initialising on every page. Comparing Elementor, Divi and WPBakery covers the trade-offs if you are choosing one; if you are moving off one, the destination is a block theme.
Disclosure: I build and maintain Bloqra, one of the block-theme options in that category, so treat that as an interested recommendation. GeneratePress and Kadence are the established alternatives and either is a perfectly sensible choice — and if your pages are simple enough, a core theme with the Site Editor needs no third-party theme at all. Judge whichever you pick the same way: profile the rebuilt page with the snippet above and compare it to the old one, rather than trusting anybody’s marketing claim about lightweight code, mine included.
Fixes that move the number
Ship less JavaScript on the page that is failing
This is the only fix that works every time, and most WordPress sites have an easy 100 KB to remove: scripts enqueued site-wide for a feature used on one page. Start by seeing what is actually there.
// Temporary. Prints every front-end handle for administrators.
add_action( 'wp_print_footer_scripts', function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
echo '<!-- handles: ' . implode( ', ', wp_scripts()->done ) . ' -->';
}, 999 );
View source, read the list, and for each handle ask which page needs it. Then dequeue the rest conditionally. Priority matters: scripts are enqueued at priority 10, so your removal has to run later.
add_action( 'wp_enqueue_scripts', function () {
// The contact form script is only needed on the contact page.
if ( ! is_page( 'contact' ) ) {
wp_dequeue_script( 'contact-form-7' );
wp_dequeue_style( 'contact-form-7' );
}
// The slider runs on the front page and nowhere else.
if ( ! is_front_page() ) {
wp_dequeue_script( 'theme-slider' );
}
}, 20 );
Do this one handle at a time and click through the site after each. Removing a script a block silently depends on breaks that block with no error message, which is the same debugging exercise as isolating a plugin conflict — bisect, do not guess.
Defer what has to stay
Since WordPress 6.3 the Scripts API takes a loading strategy, so the old script_loader_tag string surgery is no longer necessary.
wp_enqueue_script(
'my-widget',
get_theme_file_uri( '/js/widget.js' ),
array(),
'1.2.0',
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
// Or, for a handle registered elsewhere:
wp_script_add_data( 'some-plugin-script', 'strategy', 'defer' );
Core will quietly downgrade defer back to blocking if a dependent script is registered as blocking, or if the handle has an inline script attached in the “after” position — which plugins do constantly. So verify the attribute actually appears in the HTML rather than assuming the call worked. And be clear about what this buys you: deferring reduces input delay for clicks that land during load. It does not shorten a 400 ms event handler by a single millisecond.
Put a facade in front of third-party widgets
A chat widget nobody opens still costs every visitor its parse time. Render a real button, load the vendor script when someone presses it, and the cost moves to the people who wanted the feature.
document.querySelector('#chat-launch')
.addEventListener('click', () => {
const s = document.createElement('script');
s.src = 'https://vendor.example/widget.js';
s.async = true;
document.head.appendChild(s);
}, { once: true });
That first click will be slow — it is loading a bundle. It is one click, by someone who asked for it, instead of a tax on every interaction on the page. Apply the same pattern to embedded maps, video players and comment systems.
Yield inside your own handlers
When a handler genuinely has work to do, paint the visual response first and let everything else run in a later task.
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
return new Promise((resolve) => setTimeout(resolve, 0));
}
button.addEventListener('click', async () => {
updateButtonLabel(); // the user sees this in the next frame
await yieldToMain();
recalculateTotals(); // everything else waits its turn
await yieldToMain();
sendAnalyticsEvent();
});
scheduler.yield() shipped in Chrome and Edge 129 and in Firefox 142, and Safari has not implemented it, hence the feature detection and the setTimeout fallback. The fallback puts your continuation at the back of the queue rather than the front, which is slightly worse and still far better than not yielding at all.
Cut presentation delay
If the breakdown points at presentation delay, the problem is rendering cost, not script cost. Count your nodes with document.querySelectorAll('*').length — a page builder template with 4,000 elements makes every style recalculation expensive. Trimming the markup is the real fix; content-visibility: auto with a sensible contain-intrinsic-size on long off-screen sections is the cheap one, and it costs nothing to try.
What will not help
Being direct about this saves a weekend. Each of these is worth doing for its own reasons, and none of them will change INP.
WordPress 7.1 details worth knowing
Three things in the current release touch this work, and one of them is routinely misread.
Speculative loading configuration. 7.1 lets site owners and hosts set speculative loading defaults through constants or environment variables instead of a plugin. Prefetching and prerendering the next page makes navigation feel instant, which is a real win — and it is a navigation win. It does not reduce the work a click triggers on a page that is already open, so it will not move INP.
jQuery UI 1.14.2. 7.1 updates jQuery UI from 1.13.3, with jQuery.uiBackCompat left at true so older code keeps working. Several internal APIs were removed — $.fn._form, $.ui.ie, $.ui.safeActiveElement and $.ui.safeBlur — which matters if a theme or an old plugin reaches into them. Worth checking your console after updating, though it is a compatibility question rather than a performance one.
Client-side media processing. Image resizing and thumbnail generation now run in the browser through a WebAssembly build of libvips. That removes load from your server and helps large uploads avoid memory limits, and it all happens in wp-admin — not on the pages where INP is measured.
If your fixes involve editing theme JavaScript or enqueue logic, put them somewhere that survives an update. theme.json or a child theme walks through where each kind of change belongs.
A realistic afternoon
Frequently asked questions
What is a good INP score for a WordPress site?
200 ms or less at the 75th percentile of real page views, scored separately for mobile and desktop. Above 500 ms is poor. Because mobile and desktop are graded independently, the common outcome for a WordPress site is a comfortable desktop pass and a mobile failure from the same code running on slower hardware.
Will a caching plugin improve my INP?
Barely, and only indirectly. Caching shortens the time before the page arrives; INP measures what happens on the visitor’s CPU once it has. The one real overlap is that removing render-blocking scripts from the head cuts input delay for taps that happen during load. If a caching plugin’s “delay JavaScript” or “remove unused JS” feature is enabled, it may be actively costing you INP.
Why does PageSpeed Insights show no INP for my page?
Either the page has too little Chrome traffic for CrUX to report on it, or visitors are not interacting with it. A page with no clicks, taps or key presses reports no INP at all, and scrolling does not count. That is an absence of data, not a pass — deploy field measurement and find out.
Do Elementor and Divi automatically fail INP?
No. A lean page built with either can pass. What fails is the stack that tends to grow around them — the builder, several addon packs, a slider, a popup plugin and a chat widget, all initialising on every page. Profile the actual page before blaming the builder, and if the attribution does point at the builder’s bundle, start by removing addons rather than migrating the whole site.
Does deactivating plugins fix INP?
Only the ones that enqueue front-end JavaScript on the page you are measuring. A backup plugin, an SMTP plugin or a redirect manager adds nothing to the main thread a visitor sees. Deactivating plugins is still the fastest way to confirm a suspect: switch one off, re-record, switch it back on. Bisect rather than clearing the whole list.
How long until Search Console shows the improvement?
CrUX aggregates a rolling 28-day window, so expect roughly a month before the field number fully reflects a fix, and partial movement before that. This is the main argument for running your own beacon: your data updates the day you deploy, while Search Console is still averaging in the old code.
Does INP affect my Google rankings?
Core Web Vitals are part of Google’s page experience signals, so it counts, and it counts less than relevance. Fixing a 700 ms INP will not lift a page that does not answer the query. Treat it as removing a handicap and as a straightforward usability improvement — a 600 ms lag on tap is something visitors feel, whatever it does for your rankings.
Sources
- Interaction to Next Paint (INP) — web.dev, for the thresholds, the three phases, the worst-interaction rule and which interaction types are measured
- Optimize Interaction to Next Paint — web.dev, for yielding, layout thrashing and presentation-delay guidance
- Find slow interactions in the field — web.dev, for the
web-vitalsattribution build and its fields - Long Animation Frames API — Chrome for Developers, for the 50 ms threshold, script attribution and browser support
- Use
scheduler.yield()to break up long tasks — Chrome for Developers, for availability and the fallback approach - Registering scripts with async and defer attributes in WordPress 6.3 — Make WordPress Core, for the loading-strategy API and the downgrade rules
- WordPress 7.1 “Mary Lou” and jQuery UI updated to 1.14.2 in WordPress 7.1 — for the 7.1 details above

Leave a Reply to This Post