At Subito (Italy's leading classifieds platforms), we constantly monitor our Core Web Vitals. While we had a handle on LCP and CLS, we were constantly struggling with INP (Interaction to Next Paint) on our high-traffic public pages, specifically our Listing and Ad Details pages.
Unlike LCP or CLS, where we have automated alerts triggering when thresholds are breached, setting up alerts for INP seemed impossible.
What is INP?
For context, INP measures a page's responsiveness. It observes the latency of all user interactions (clicks, taps, and key presses) throughout the lifespan of a user's visit. The final value is the longest interaction observed (ignoring outliers).
According to Google's standards, a "Good" experience is defined by strictly defined thresholds:
- 🟢 Good: 200ms
- 🟡 Needs Improvement: Between 200ms and 500ms
- 🔴 Poor: 500ms
INP is about how "fast" the site feels when you try to use it. A poor INP means the user clicks a button and... waits.
The Problem: "It's Not Just Us"
We struggled with INP because fluctuations weren't always caused by our code.
On our highest-traffic pages, two major actors are almost entirely out of our direct control:
- GTM (Google Tag Manager): Managed by our Marketing team.
- ADV (Advertising): Managed by our Sales/Advertising team.
Both inject code and event listeners that heavily impact performance.
The Old "Empiric" Way (And Why It Failed)
Previously, our monitoring was purely observational via Grafana. When INP spiked, we would scramble to check recent releases. Most of the time, our code changes weren't the culprit, and rollbacks did nothing.
We resorted to an empiric, frustrating debugging process: open Chrome DevTools, throttle the CPU to 4x, simulate a 4G network, and click around hoping to reproduce the lag. It wasn't wrong, but it was inefficient and lacked engineering rigor.
The "Scientific" Approach: Isolating the Actors
This year, the Frontend Chapter decided to stop guessing. We needed to measure the specific weight of the three actors: Our Code, GTM, and ADV.
With approval from the respective teams, we ran an A/B test on 1% of our traffic:
- Segment A: Loaded without GTM.
- Segment B: Loaded without ADV.
- Segment C: Standard traffic.
This finally gave us clear baselines.
Case Study 1: The Ad Details Page
Here is what we found for the Ad Details page:
- Standard INP: 208ms (Needs Improvement)
- INP without ADV: 180ms
- INP without GTM: 112ms (Good!)
The data was undeniable: GTM was the primary bottleneck.
Case Study 2: The Listing Page
The Listing page was more complex. Even without external scripts, our baseline was high:
- Standard INP: 345ms (Very Poor)
- INP without ADV: 279ms
- INP without GTM: 320ms
Here, removing GTM didn't help much, and removing ADV helped but didn't solve it. We had to dig deeper.
Solving the GTM Issue (Ad Details)
Once we knew GTM was the culprit on the Ad Details page, we collaborated closely with the Marketing team.
GTM works via triggers (events) that inject JavaScript (tags). To find the needle in the haystack, we cloned the GTM workspace for our 1% traffic slice and used a "Bisect" approach (binary search):
- We disabled 50% of the triggers.
- Monitored the INP.
- If improved, the issue was in that 50%. If not, we checked the other half.
- Repeat until the specific script is found.
The Verdict: The heavy hitters were tracking scripts for TikTok and Facebook.
Instead of complex engineering workarounds to move these scripts out of GTM, the Marketing team simply agreed to disable the TikTok tracking.
Result: INP dropped from 208ms to ~170ms. We were finally under the 200ms threshold! 🎉
Solving the Complex Issue (Listing Page)
The Listing page was harder because there was no single "culprit."
Step 1: Better Telemetry with Grafana Faro
We integrated Google's web-vitals library to capture attribution data (longestScriptURL, interactionTarget) and sent it to Grafana Faro:
getFaro().api.pushMeasurement(
{
type: 'INP',
values,
},
{
context: beacon,
}
);
This allowed us to visualize exactly which scripts and which DOM elements were causing delays.
We discovered that clicks on a.index-module_link were problematic. These links had heavy event handlers attached by interstitial ads (full-page ads).
We are still negotiating a fix with the ADV team, but we couldn't just wait.
Step 2: Enter the React Compiler
To optimize the code we did control, we decided to upgrade to Next.js 16 and enable the React Compiler.
The upgrade process wasn't difficult (though switching from Webpack to Turbopack was an adventure for another article).
The Result:
Surprisingly, just enabling the React Compiler significantly dropped our INP:
From 345ms down to 271ms.
It's still not perfect, but it was a massive free performance win. If we calculate the "No ADV" scenario combined with React Compiler, we would actually be under the threshold:
Conclusion
We aren't done yet, but this journey taught us valuable lessons:
- Don't Guess, Measure: Isolate your third parties (GTM, ADV) using traffic splitting (even 1-2%). It gives you leverage and data to drive decisions.
- React Compiler is Legit: It's not hard to introduce, and it can provide a significant performance boost for free.
- Collaborate, Don't Hack: We avoided clever technical workarounds (like the DataLayer yield pattern). Why? Because talking to the Marketing team and solving the root cause is sustainable. Hacks are not.
Next Steps
- Enable automated alerting for INP now that we understand the baseline.
- Continue optimizing the Listing page to reach the green threshold (sub 200ms).
- The Big Goal: Treat Core Web Vitals as business metrics. We want to establish "Performance Budgets" (e.g., GTM gets max 50ms, ADV gets max 50ms) to ensure quality isn't sacrificed for tracking.








Top comments (2)
for tracking core web vitals which one is better as per your perspective Grafana faro using web vitals library or Amplitude using the same library?
In my opinion, the specific tool you choose is not the key point. What really matters is having the right information when INP exceeds the threshold: which element was clicked and what is blocking the main thread.