<?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>DEV Community: Francesca Milan</title>
    <description>The latest articles on DEV Community by Francesca Milan (@framilan).</description>
    <link>https://dev.to/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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3494751%2F8d09a673-800d-404c-ba20-569806f01334.jpeg</url>
      <title>DEV Community: Francesca Milan</title>
      <link>https://dev.to/framilan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/framilan"/>
    <language>en</language>
    <item>
      <title>How We Cut Slow Responses by 80% Migrating to Next.js App Router</title>
      <dc:creator>Francesca Milan</dc:creator>
      <pubDate>Tue, 16 Jun 2026 07:27:43 +0000</pubDate>
      <link>https://dev.to/subito/how-we-cut-slow-responses-by-80-migrating-to-nextjs-app-router-37da</link>
      <guid>https://dev.to/subito/how-we-cut-slow-responses-by-80-migrating-to-nextjs-app-router-37da</guid>
      <description>&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%2F1yer5u72bshgyybfy6aw.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%2F1yer5u72bshgyybfy6aw.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.subito.it" rel="noopener noreferrer"&gt;Subito&lt;/a&gt; is Italy's leading classifieds marketplace, serving millions of users every month. Our ad detail page is one of the most heavily trafficked pages on the platform, handling thousands of requests per minute and playing a critical role in both SEO and revenue.&lt;/p&gt;

&lt;p&gt;In this article, we'll walk through how we migrated it from the Next.js Pages Router to the App Router, the challenges we encountered along the way, and how the migration helped &lt;strong&gt;reduce slow responses by roughly 80%.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Goal&lt;/li&gt;
&lt;li&gt;Migration Approach: Ship Without Blocking Anyone&lt;/li&gt;
&lt;li&gt;Problem 1: HTTP 410 Gone in App Router&lt;/li&gt;
&lt;li&gt;Problem 2: HTML Streaming Behind Nginx and Akamai&lt;/li&gt;
&lt;li&gt;Rollout and SEO Monitoring&lt;/li&gt;
&lt;li&gt;How Much Effort Did This Migration Require?&lt;/li&gt;
&lt;li&gt;Results&lt;/li&gt;
&lt;li&gt;Conclusions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;As we always do at Subito, we never migrate for migration's sake. We go in with specific, measurable targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster page loads&lt;/strong&gt;: leverage React Server Components and HTML streaming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better SEO&lt;/strong&gt;: improve page speed scores, working alongside our SEO team, faster pages rank better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration touched every layer of the stack: Next.js, a custom Express server, Nginx, and Akamai as our CDN. Here's what each step looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Approach: Ship Without Blocking Anyone
&lt;/h2&gt;

&lt;p&gt;The ad detail page is one of the highest-traffic pages on the platform. We couldn't freeze product work for weeks while we rewrote it. So we didn't.&lt;/p&gt;

&lt;p&gt;Instead of doing a big-bang rewrite, we incrementally added the App Router tree, server components, data fetching, layouts, &lt;strong&gt;alongside&lt;/strong&gt; the existing Pages Router page.&lt;/p&gt;

&lt;p&gt;The key insight was that each new server component was simply a wrapper around the &lt;strong&gt;same client component&lt;/strong&gt; already used by the Pages Router. &lt;br&gt;
There was no duplication: one client component, two consumers. &lt;br&gt;
This meant that any bug fix or product feature added to a client component automatically applied to both the old and new page. Developers working on the Pages Router side were already contributing to the App Router version without knowing it.&lt;/p&gt;

&lt;p&gt;This approach gave us two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Clean component boundaries.&lt;/strong&gt; We used server components to own data fetching and pass only what client components actually need. The classic prop-drilling waterfall disappeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A defined Data Access Layer.&lt;/strong&gt; Following the &lt;a href="https://nextjs.org/docs/app/guides/data-security#data-access-layer" rel="noopener noreferrer"&gt;Next.js recommendation&lt;/a&gt;, we centralized all data fetching behind a DAL and used React's &lt;a href="https://react.dev/reference/react/cache" rel="noopener noreferrer"&gt;&lt;code&gt;cache()&lt;/code&gt;&lt;/a&gt; to deduplicate calls. When three server components all need the same ad data, the network call fires once.&lt;/p&gt;

&lt;p&gt;For sections with heavy data dependencies or secondary content, we combined &lt;code&gt;Suspense&lt;/code&gt; with the &lt;code&gt;use()&lt;/code&gt; hook to unlock streaming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GeneralInfo.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GeneralInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;withResolvers&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AsyncData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;fetchAdItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;advertiser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nf"&gt;fetchShopData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;advertiser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;advertiser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="c1"&gt;// ...all the other fetches&lt;/span&gt;
    &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;shop&lt;/span&gt; &lt;span class="cm"&gt;/* ...other results */&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shop&lt;/span&gt; &lt;span class="cm"&gt;/* ...other props */&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GeneralInfoSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LazyGeneralInfo&lt;/span&gt; &lt;span class="na"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LazyGeneralInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ClientGeneralInfo is the same component used by the Pages Router version. No duplication!&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ClientGeneralInfo&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skeleton renders immediately while data resolves in the background. Once it arrives, React swaps in the real component, no layout shift, no full-page loading state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive Rollout via Custom Express Server
&lt;/h3&gt;

&lt;p&gt;We run a custom Express server in front of Next.js. This gave us a clean toggle to route traffic between the old and new page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.ts&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;adDetailListingPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;validateAdDetailListingParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;clientHints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;securityMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;enableAppRouterForAdDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ad/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/addetail/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&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="c1"&gt;// utils/rollout.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FORCE_MIGRATED_AD_QP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use-ar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIGRATED_CATEGORIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enableAppRouterForAdDetail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ParsedQs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pathFriendlyCategory&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;FORCE_MIGRATED_AD_QP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathFriendlyCategory&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;MIGRATED_CATEGORIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathFriendlyCategory&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;&lt;code&gt;?use-ar=1&lt;/code&gt; forced the new page for internal testing. The category list let us roll out one macro-category at a time in production, starting with &lt;em&gt;Job&lt;/em&gt; category and expanding progressively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: HTTP 410 Gone in App Router
&lt;/h2&gt;

&lt;p&gt;The ad detail page is indexed by search engines, so when an ad expires we must return a proper &lt;code&gt;410 Gone&lt;/code&gt; response. A redirect or even a standard &lt;code&gt;404 Not Found&lt;/code&gt; is not sufficient: a genuine &lt;code&gt;410&lt;/code&gt; clearly signals to crawlers that the page has been permanently removed and should be dropped from the index as quickly as possible.&lt;/p&gt;

&lt;p&gt;The App Router, at the time of our migration, had no built-in API for 410. &lt;code&gt;notFound()&lt;/code&gt; returns 404. There's no &lt;code&gt;gone()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We wrote a full discovery article on the available workarounds here: &lt;a href="https://dev.to/alessandro-grosselle/how-to-return-http-410-gone-status-in-nextjs-app-router-two-workarounds-2f0g"&gt;How to Return HTTP 410 Gone in Next.js App Router — Two Workarounds&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since we already owned the Express layer, we went with a &lt;strong&gt;status code interceptor&lt;/strong&gt;: Express intercepts the outgoing response before it hits the client and rewrites the status code when the page signals a gone state. No framework hacks, no monkey-patching Next.js internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: HTML Streaming Behind Nginx and Akamai
&lt;/h2&gt;

&lt;p&gt;This was the trickiest part of the whole migration: during staging tests we noticed our &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; skeletons were invisible. We even forced an artificial 5-second delay on data fetching; the skeleton still never appeared. The page just loaded slowly as a blank screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx Buffering
&lt;/h3&gt;

&lt;p&gt;The first fix was disabling response buffering in Nginx, &lt;a href="https://nextjs.org/docs/app/guides/streaming" rel="noopener noreferrer"&gt;as documented by Next.js&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With buffering off, the skeleton appeared... but only when hitting the origin server directly. Through the CDN, still nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Akamai Buffering
&lt;/h3&gt;

&lt;p&gt;We shifted our investigation to Akamai. Our initial hypothesis was a caching layer conflict or a compression issue. We disabled caching... no effect.&lt;/p&gt;

&lt;p&gt;Before diving deep into Akamai's Property Manager, we opened a support ticket. The answer was clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;By default, Akamai buffers HTML responses before delivering them to the client. Disabling this behavior requires advanced metadata and cannot be configured through standard Property Manager rules.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Chunked transfer encoding and early flush are not available as standard toggles, but Akamai provided us with a custom behavior that we could apply via Property Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;network:http.buffer-response-v2&amp;gt;&lt;/span&gt;off&lt;span class="nt"&gt;&amp;lt;/network:http.buffer-response-v2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;edgeservices:wco.input-buffer-minimum&amp;gt;&lt;/span&gt;0B&lt;span class="nt"&gt;&amp;lt;/edgeservices:wco.input-buffer-minimum&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;edgeservices:wco.request-flush&amp;gt;&lt;/span&gt;on&lt;span class="nt"&gt;&amp;lt;/edgeservices:wco.request-flush&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;edgeservices:modify-outgoing-response.chunk-to-client&amp;gt;&lt;/span&gt;on&lt;span class="nt"&gt;&amp;lt;/edgeservices:modify-outgoing-response.chunk-to-client&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once applied, streaming worked end-to-end. Skeletons appeared immediately; content streamed in as data resolved :tada! &lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming + Caching: What Actually Happens
&lt;/h3&gt;

&lt;p&gt;After enabling the custom behavior, we re-enabled the Akamai cache layer (non-negotiable for a high-traffic site like ours) and observed the following behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache MISS&lt;/strong&gt;: The HTML is streamed. No &lt;code&gt;Content-Length&lt;/code&gt; header in the response, the browser receives chunks progressively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache HIT&lt;/strong&gt;: The full HTML is returned at once with a &lt;code&gt;Content-Length&lt;/code&gt; header (e.g., &lt;code&gt;28315&lt;/code&gt;). Streaming doesn't apply, but the page is already fully built so it doesn't need to be.&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%2Fdauu7oxju69bm56rp3e1.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%2Fdauu7oxju69bm56rp3e1.png" alt=" " width="800" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the correct behavior. Cached responses are fast regardless; streaming only matters for the cold path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout and SEO Monitoring
&lt;/h2&gt;

&lt;p&gt;We rolled out in two distinct phase, deliberately separated, not just for technical safety, but for SEO safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1&lt;/strong&gt; was controlled at the Express layer: migrate to App Router, streaming off. We started with a single category (&lt;em&gt;Job&lt;/em&gt;), monitored carefully, then expanded one macro-category per cycle until all traffic was on the App Router.&lt;/p&gt;

&lt;p&gt;Only once the migration was stable did we consider enabling HTML streaming. We were genuinely concerned about the SEO implications: would Google's crawler handle a chunked, incrementally-delivered HTML response correctly? Would it see the full page content, or just whatever arrived in the first chunk? We didn't want to find out by watching rankings drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2&lt;/strong&gt; enabled HTML streaming: we started with the &lt;em&gt;Videogiochi&lt;/em&gt; (Video Games) category, using a targeted rule in Akamai Property Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If Path matches one of: /vi/*, *.htm
And Path matches one of: /videogiochi/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fapkbdk99kaex8bcr14jq.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%2Fapkbdk99kaex8bcr14jq.png" alt=" " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our SEO team played a key role throughout the rollout: before enabling streaming in a new category, we monitored its SEO metrics for roughly two weeks, ensuring there were no unexpected impacts on crawling, indexing, or search performance. Only after the category showed stable results did we move on to the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Effort Did This Migration Require?
&lt;/h2&gt;

&lt;p&gt;Surprisingly little!&lt;br&gt;
The migration was carried out primarily by a single developer, working on it incrementally between regular feature deliveries rather than as a dedicated full-time initiative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A significant enabler was Claude Code&lt;/strong&gt;, which we used extensively throughout the project. It helped with planning, architectural exploration, implementation details, and routine development tasks, allowing us to move faster while keeping the migration manageable for a very small team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Response Times
&lt;/h3&gt;

&lt;p&gt;The Grafana graph below shows the percentage of responses taking more than 250ms. Before the migration (up to ~03/25), the figure sat consistently between 25% and 40%, with spikes reaching 55%. &lt;br&gt;
After the rollout, it collapsed to near zero and stayed there.&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%2Fx2we28wc7r9mulwav8tn.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%2Fx2we28wc7r9mulwav8tn.png" alt=" " width="799" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SEO Performance
&lt;/h3&gt;

&lt;p&gt;The data below shows Fast URLs (&amp;lt; 500ms) across categories:&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%2Fblibpclziivxoyc9uv78.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%2Fblibpclziivxoyc9uv78.png" alt=" " width="799" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The improvements are significant across the board, with Video Games showing the strongest gain at nearly 30%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;The migration itself wasn't the hardest part: Next.js made it possible to run Pages Router and App Router side-by-side, allowing us to migrate incrementally without stopping feature development.&lt;/p&gt;

&lt;p&gt;The real challenge was making the entire delivery chain work together: React Server Components, streaming, Express, Nginx, Akamai, observability, and SEO requirements.&lt;/p&gt;

&lt;p&gt;Only when all those pieces aligned did we unlock the performance improvements we were aiming for.&lt;/p&gt;

&lt;p&gt;A few takeaways for teams considering a similar migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel routing lets you migrate without a freeze.&lt;/strong&gt; Pages Router and App Router coexist in the same Next.js project. Use that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom server layer is an asset.&lt;/strong&gt; Express gave us per-request routing control, progressive rollout toggles, and the ability to solve framework gaps (like 410 status codes) without waiting for upstream fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML streaming requires work at every layer.&lt;/strong&gt; Nginx buffering, CDN buffering, and chunked transfer encoding all need explicit configuration. Don't assume streaming works end-to-end just because your Next.js code is correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end and visual regression tests caught regressions early.&lt;/strong&gt; Before touching production we had confidence from automated test coverage.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <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://dev.to/subito/how-we-reduced-inp-by-100ms-gtm-isolation-react-compiler-and-better-telemetry-315g</link>
      <guid>https://dev.to/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>
