When Your Performance Plugin Causes the Regression It’s Supposed to Prevent

Interaction to Next Paint (INP) measures how long a page takes to visibly respond after a user does something — a click, a tap, a keypress. Google’s Search Console reports it in aggregate, per URL, from real field data, bucketed into "Good," "Needs Improvement," and "Poor." One site in a WordPress multisite network had spent months comfortably in "Good" territory across its content pages. Then, over a short window, more than ten thousand of those URLs dropped into "Needs Improvement" at once.

That kind of mass, simultaneous regression across thousands of otherwise-unrelated URLs is its own diagnostic clue, before you’ve looked at a single line of code: something that affects all of those pages uniformly just changed, which usually rules out a content-specific or template-specific bug and points instead at something shared — a plugin, a script loaded sitewide, a piece of shared infrastructure.

TL;DR: A plugin that defers non-critical JavaScript until user interaction concentrated 400KB of script execution into exactly the moment INP gets measured — the technique built to help initial-load metrics actively hurt interaction responsiveness for over ten thousand URLs. Fix: stop loading feature-specific assets on pages that don’t need them, and shrink the fallback timeout.

What Was the Suspect? A Plugin Whose Whole Job Is to Help

The site ran a fairly common category of performance-optimization plugin — the kind that defers loading non-critical JavaScript until the user actually interacts with the page, on the reasoning that a script nobody has triggered yet doesn’t need to compete with the initial render for the main thread. It’s a legitimate, widely-used technique, and for metrics like Largest Contentful Paint or Time to Interactive, it usually helps: less JS competing for the main thread during initial load means the visible content shows up sooner.

On this site, it was configured to defer roughly 400KB of combined JavaScript — a general-purpose front-end library plus a page-builder/blocks framework bundle — until the visitor’s first interaction, with a fallback timeout (around fifteen seconds) that would load everything anyway if no interaction happened first.

Why Does "Wait for Interaction" Backfire for INP?

Here’s the mechanism, and it’s worth sitting with because it’s a genuinely counterintuitive failure mode: the technique defers loading until interaction, which means the deferred code starts executing at the exact moment of that same interaction — the click, the tap, the keypress that INP is measuring the responsiveness of.

Four hundred kilobytes of bundled JavaScript doesn’t parse and execute instantly. Run that much code as one uninterrupted block on the main thread, and it becomes a single long task, blocking the browser from responding to anything else — including painting a visible response to the very interaction that triggered it. The technique that’s supposed to get expensive script execution out of the way of the user instead scheduled all of it to run in direct response to the user, at the one moment responsiveness is actually being scored. It’s close to the worst possible timing you could pick for that execution, and it’s a direct, mechanical consequence of "defer until interaction" as a strategy — not a misconfiguration exactly, just a technique whose tradeoffs point the wrong way for this particular metric.

How Do You Diagnose This Class of Bug?

The general approach for tracking down a long-task-driven INP regression, independent of the specific cause: open a representative affected page in a browser’s performance profiler, simulate the kind of interaction a real user would make — a click on a typical link or button — and look at the main-thread timeline around that click. A long, unbroken block of "Script Evaluation" time starting right at the interaction, rather than immediately after it, is the signature to look for. From there, the browser’s own attribution (which script, which function) points at the specific bundle responsible, and from the bundle you can usually trace back to whichever plugin or loading strategy is responsible for scheduling it.

In this case, that trail led to the interaction-deferral plugin’s configuration, and specifically to two things bundled into what it was deferring: a general-purpose library that arguably belonged in that deferred bundle, and a much more specific, content-type-driven interactive feature — a step-by-step guided-view component used on a subset of article pages — that didn’t need to be loaded at all on most of the affected URLs, since a large share of them were archive and listing pages where that specific interactive feature never appears.

What Was the Fix?

Two changes, working together:

  1. Stop loading feature-specific assets on pages that don’t use the feature. The interactive component responsible for a meaningful share of the deferred bundle’s weight was being loaded sitewide, including on archive and listing pages where it never renders. Scoping its assets to only the templates that actually use it removes that weight from every page where it was dead weight to begin with.

  2. Shrink the fallback timeout. The interaction-deferral plugin’s fifteen-second no-interaction fallback was reduced to roughly three seconds. This doesn’t fix the long-task-on-interaction problem directly, but it limits how long a script can sit fully deferred before loading unconditionally anyway — a shorter ceiling on how "extreme" the deferral can get for a visitor who does eventually interact, and a smaller version of the same execution cost if it fires on the fallback timer instead of on a click.

Where Is This Account Honestly Incomplete?

The other posts in this series include a specific before-and-after measurement, repeated on a warm cache, as the evidence a fix actually worked. I don’t have that for this one — the after-the-fix INP numbers for this specific regression aren’t something I can responsibly report, so I’m not going to invent a plausible-looking one just to round out the story. What’s solid here is the mechanism (a genuinely useful diagnostic pattern worth knowing regardless of this specific case) and the scale of the trigger (ten thousand-plus URLs, a real and dramatic regression, not a marginal one). The fix direction is sound and addresses the actual root cause identified. If you’re chasing something that looks like this, treat the diagnostic approach above as the reusable part, and measure your own before/after with the same repeated-warm-run discipline covered elsewhere in this series before declaring victory.

What’s the Generalizable Lesson Here?

  • A performance technique’s tradeoffs are metric-specific. "Defer until interaction" trades initial-load metrics against interaction-responsiveness metrics — it isn’t a strict win, it’s a shift, and if you only ever look at the metrics it helps, you’ll miss the ones it quietly makes worse.
  • A regression that hits thousands of otherwise-unrelated URLs simultaneously is a strong signal to look at shared infrastructure — a plugin, a sitewide script, a CDN/edge config — before you start investigating individual pages.
  • When profiling an interaction-responsiveness problem, look at the main-thread timeline starting at the interaction, not just after it. A long task that begins exactly when the user acts is a specific, recognizable pattern, and "what got scheduled to run on interaction" is a very different question from "what’s slow in general."
  • Loading a feature’s assets sitewide instead of only on the templates that use it is a common, easy-to-miss source of unnecessary JavaScript weight — worth an explicit audit independent of any specific regression.
  • Report what you actually measured. A case study with an honest gap is more useful than one with an invented ending.