<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Francesca Milan</title>
    <description>The latest articles on Forem by Francesca Milan (@framilan).</description>
    <link>https://forem.com/framilan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3494751%2F8d09a673-800d-404c-ba20-569806f01334.jpeg</url>
      <title>Forem: Francesca Milan</title>
      <link>https://forem.com/framilan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/framilan"/>
    <language>en</language>
    <item>
      <title>How We Reduced INP by 100ms+: GTM Isolation, React Compiler, and Better Telemetry</title>
      <dc:creator>Francesca Milan</dc:creator>
      <pubDate>Tue, 17 Feb 2026 16:19:34 +0000</pubDate>
      <link>https://forem.com/subito/how-we-reduced-inp-by-100ms-gtm-isolation-react-compiler-and-better-telemetry-315g</link>
      <guid>https://forem.com/subito/how-we-reduced-inp-by-100ms-gtm-isolation-react-compiler-and-better-telemetry-315g</guid>
      <description>&lt;p&gt;At &lt;a href="https://www.subito.it/" rel="noopener noreferrer"&gt;Subito&lt;/a&gt; (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 &lt;strong&gt;&lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;INP (Interaction to Next Paint)&lt;/a&gt;&lt;/strong&gt; on our high-traffic public pages, specifically our &lt;strong&gt;Listing&lt;/strong&gt; and &lt;strong&gt;Ad Details&lt;/strong&gt; pages.&lt;/p&gt;

&lt;p&gt;Unlike LCP or CLS, where we have automated alerts triggering when thresholds are breached, setting up alerts for INP seemed impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is INP?
&lt;/h2&gt;

&lt;p&gt;For context, &lt;strong&gt;INP&lt;/strong&gt; 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).&lt;/p&gt;

&lt;p&gt;According to Google's standards, a "Good" experience is defined by strictly defined thresholds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;Good:&lt;/strong&gt; &lt;strong&gt;200ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;Needs Improvement:&lt;/strong&gt; Between &lt;strong&gt;200ms&lt;/strong&gt; and &lt;strong&gt;500ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Poor:&lt;/strong&gt; &lt;strong&gt;500ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: "It's Not Just Us"
&lt;/h2&gt;

&lt;p&gt;We struggled with INP because &lt;strong&gt;fluctuations weren't always caused by our code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On our highest-traffic pages, two major actors are almost entirely out of our direct control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://marketingplatform.google.com/about/tag-manager/" rel="noopener noreferrer"&gt;GTM (Google Tag Manager)&lt;/a&gt;:&lt;/strong&gt; Managed by our Marketing team.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;ADV (Advertising):&lt;/strong&gt; Managed by our Sales/Advertising team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both inject code and event listeners that heavily impact performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old "Empiric" Way (And Why It Failed)
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;wrong&lt;/em&gt;, but it was inefficient and lacked engineering rigor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Scientific" Approach: Isolating the Actors
&lt;/h2&gt;

&lt;p&gt;This year, the Frontend Chapter decided to stop guessing. We needed to measure the specific weight of the three actors: &lt;strong&gt;Our Code, GTM, and ADV.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With approval from the respective teams, we ran an A/B test on &lt;strong&gt;1% of our traffic&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Segment A:&lt;/strong&gt; Loaded without GTM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment B:&lt;/strong&gt; Loaded without ADV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment C:&lt;/strong&gt; Standard traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This finally gave us clear baselines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case Study 1: The Ad Details Page
&lt;/h3&gt;

&lt;p&gt;Here is what we found for the Ad Details page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard INP:&lt;/strong&gt; 208ms (Needs Improvement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP without ADV:&lt;/strong&gt; 180ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP without GTM:&lt;/strong&gt; 112ms (Good!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdkravmxc30iiogh7kua.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdkravmxc30iiogh7kua.png" alt="INP Data Ad Details" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhoi9g5y3kq6gce3fdgu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhoi9g5y3kq6gce3fdgu.png" alt="INP Graph Ad Details" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data was undeniable: &lt;strong&gt;GTM was the primary bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Case Study 2: The Listing Page
&lt;/h3&gt;

&lt;p&gt;The Listing page was more complex. Even without external scripts, our baseline was high:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard INP:&lt;/strong&gt; 345ms (Very Poor)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP without ADV:&lt;/strong&gt; 279ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP without GTM:&lt;/strong&gt; 320ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaoyuk6ozv3w180opn0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaoyuk6ozv3w180opn0.png" alt="INP Data Listing" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, removing GTM didn't help much, and removing ADV helped but didn't solve it. We had to dig deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the GTM Issue (Ad Details)
&lt;/h2&gt;

&lt;p&gt;Once we knew GTM was the culprit on the Ad Details page, we collaborated closely with the Marketing team.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;"Bisect" approach&lt;/strong&gt; (binary search):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We disabled 50% of the triggers.&lt;/li&gt;
&lt;li&gt; Monitored the INP.&lt;/li&gt;
&lt;li&gt; If improved, the issue was in that 50%. If not, we checked the other half.&lt;/li&gt;
&lt;li&gt; Repeat until the specific script is found.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt; The heavy hitters were tracking scripts for &lt;strong&gt;TikTok and Facebook&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of complex engineering workarounds to move these scripts out of GTM, the Marketing team simply agreed to disable the TikTok tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; INP dropped from &lt;strong&gt;208ms to ~170ms&lt;/strong&gt;. We were finally under the 200ms threshold! 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqps98r7fawjfi8xf97u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqps98r7fawjfi8xf97u.png" alt="INP Drop Ad Details" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the Complex Issue (Listing Page)
&lt;/h2&gt;

&lt;p&gt;The Listing page was harder because there was no single "culprit."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Better Telemetry with Grafana Faro
&lt;/h3&gt;

&lt;p&gt;We integrated Google's &lt;a href="https://github.com/GoogleChrome/web-vitals" rel="noopener noreferrer"&gt;web-vitals&lt;/a&gt; library to capture attribution data (&lt;a href="https://github.com/GoogleChrome/web-vitals/blob/main/src/types/inp.ts#L141" rel="noopener noreferrer"&gt;longestScriptURL&lt;/a&gt;, &lt;a href="https://github.com/GoogleChrome/web-vitals/blob/main/src/types/inp.ts#L71" rel="noopener noreferrer"&gt;interactionTarget&lt;/a&gt;) and sent it to &lt;a href="https://grafana.com/oss/faro/" rel="noopener noreferrer"&gt;Grafana Faro&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getFaro&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushMeasurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;beacon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed us to visualize exactly &lt;em&gt;which&lt;/em&gt; scripts and &lt;em&gt;which&lt;/em&gt; DOM elements were causing delays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74ku11cbispnolbxkoyp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74ku11cbispnolbxkoyp.png" alt="Grafana Faro Logs" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We discovered that clicks on &lt;code&gt;a.index-module_link&lt;/code&gt; were problematic. These links had heavy event handlers attached by &lt;strong&gt;interstitial ads&lt;/strong&gt; (full-page ads).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwqlxwadlwenmeofyjzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwqlxwadlwenmeofyjzy.png" alt="DOM Element Analysis" width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are still negotiating a fix with the ADV team, but we couldn't just wait.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Enter the React Compiler
&lt;/h3&gt;

&lt;p&gt;To optimize the code we &lt;em&gt;did&lt;/em&gt; control, we decided to upgrade to &lt;strong&gt;Next.js 16&lt;/strong&gt; and enable the &lt;strong&gt;React Compiler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The upgrade process wasn't difficult (though switching from Webpack to Turbopack was an adventure for another article).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt;&lt;br&gt;
Surprisingly, just enabling the React Compiler significantly dropped our INP:&lt;br&gt;
&lt;strong&gt;From 345ms down to 271ms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faty1qys3msz3ybj05p6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faty1qys3msz3ybj05p6s.png" alt="React Compiler Result" width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpj2a3s86ph8bhwpspw2h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpj2a3s86ph8bhwpspw2h.png" alt="React Compiler No ADV" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We aren't done yet, but this journey taught us valuable lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Don't Guess, Measure:&lt;/strong&gt; Isolate your third parties (GTM, ADV) using traffic splitting (even 1-2%). It gives you leverage and data to drive decisions.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;React Compiler is Legit:&lt;/strong&gt; It's not hard to introduce, and it can provide a significant performance boost for free.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Collaborate, Don't Hack:&lt;/strong&gt; We avoided clever technical workarounds (like the &lt;a href="https://www.corewebvitals.io/pagespeed/datalayer-inp-yield-pattern" rel="noopener noreferrer"&gt;DataLayer yield pattern&lt;/a&gt;). Why? Because talking to the Marketing team and solving the root cause is sustainable. Hacks are not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Enable automated alerting for INP now that we understand the baseline.&lt;/li&gt;
&lt;li&gt;Continue optimizing the Listing page to reach the green threshold (sub 200ms).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Big Goal:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>react</category>
      <category>nextjs</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
