<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged inp</title>
<link>https://coderlegion.com/tag/inp</link>
<description>Powered by Question2Answer</description>
<item>
<title>INP, the Core Web Vital That Replaced FID: How to Fix It</title>
<link>https://coderlegion.com/31420/inp-the-core-web-vital-that-replaced-fid-how-to-fix-it</link>
<description>&lt;p&gt;Interaction to Next Paint tracks how fast a page shows a visible reaction to a click, tap or key press. It took over from First Input Delay as a Core Web Vital on March 12, 2024. Good is 200 milliseconds or less and poor is over 500, measured at the 75th percentile of real visits. Many sites that passed FID easily fail INP because it covers every interaction in a visit and includes running your code and painting the result.&lt;/p&gt;
&lt;p&gt;In the guide I explain what counts as an interaction (clicks, taps and key presses, not scrolling or hovering) and the three phases of each one: input delay, processing duration and presentation delay. Knowing which phase is slow tells you which fix applies.&lt;/p&gt;
&lt;p&gt;Measurement starts with field data from CrUX, PageSpeed Insights and Search Console, then moves to DevTools with CPU throttling to reproduce problems. The web-vitals library's attribution build tells you which element was slow for real visitors and how long each phase took.&lt;/p&gt;
&lt;p&gt;The fixes come in three groups: break up long tasks and yield to the main thread (with scheduler.yield() where supported); cut rendering work by avoiding layout thrashing, keeping the DOM small and using content-visibility; and tame input handlers with debouncing while deferring or removing third-party scripts. I'm clear about limits too: INP isn't a ranking shortcut, field data moves slowly, and a fast spinner doesn't fix slow work.&lt;/p&gt;
&lt;h4 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Good INP is 200 ms or less at the 75th percentile&lt;/li&gt;
&lt;li&gt;Each interaction has input, processing and presentation phases&lt;/li&gt;
&lt;li&gt;Use field data first; Lighthouse doesn't measure INP&lt;/li&gt;
&lt;li&gt;Break up long tasks and yield to the main thread&lt;/li&gt;
&lt;li&gt;Debounce heavy handlers and audit third-party scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related on BuiltToWinWeb: &lt;a rel=&quot;nofollow&quot; href=&quot;https://built2winweb.com/core-web-vitals-explained/&quot;&gt;Core Web Vitals explained&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full guide on BuiltToWinWeb: &lt;a rel=&quot;nofollow&quot; href=&quot;https://built2winweb.com/blog/inp-core-web-vitals/&quot;&gt;The full INP explainer and fixes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A summary by Jacob Campbell of an article first published on built2winweb.com on October 6, 2026.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31420/inp-the-core-web-vital-that-replaced-fid-how-to-fix-it</guid>
<pubDate>Sun, 18 Oct 2026 07:09:33 +0000</pubDate>
</item>
<item>
<title>Interaction to Next Paint Breakdown: Solving first move lag in BareBoard</title>
<link>https://coderlegion.com/26890/interaction-to-next-paint-breakdown-solving-first-move-lag-in-bareboard</link>
<description>&lt;h2 id=&quot;interaction-to-next-paint-breakdown-solving-first-move-lag-in-bareboard&quot;&gt;Interaction to Next Paint Breakdown: Solving first move lag in BareBoard&lt;/h2&gt;
&lt;h4 id=&quot;background&quot;&gt;Background&lt;/h4&gt;
&lt;p&gt;So I was developing this dumb chess board which only contains presentation logic, emits event when a piece is touched and moved and doesn't have any move calculation/validation logic whatsoever. The idea was to ship it as a web component for the community and use it as the chess board for the project I was developing (known as 8bye8). I noticed that my piece stayed mid air for around half-a-second after I dropped the piece on target square which appeared to me as rendering delay. The solution of the problem led me to the Performance Tab in Chrome Dev Tools to investigate INP Breakdown.&lt;/p&gt;
&lt;h4 id=&quot;what-is-inp-&quot;&gt;What is INP ?&lt;/h4&gt;
&lt;p&gt;From &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;web.dev&lt;/a&gt; -&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;When a user interacts with the page, those interactions should be as fast as possible. The amount of time it takes for an interaction to complete—ending when the browser presents the next frame to show the results of the interaction—is known as &lt;em&gt;interaction latency&lt;/em&gt;. This is an aspect of page performance that the &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;Interaction to Next Paint&lt;/a&gt; metric measures.&lt;/p&gt;
&lt;p&gt;The amount of time it takes for the browser to present the next frame in response to a user interaction is known as the interaction's &lt;em&gt;presentation delay&lt;/em&gt;. The goal of an interaction is to provide visual feedback in order to signal to the user that something has occurred, and visual updates can involve some amount of layout work in order to achieve that goal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To understand INP, we'll first need to understand what an interaction is. Simply, for the purposes of INP, &lt;strong&gt;only the following interaction types are observed&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clicking with a mouse.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tapping on a device with a touchscreen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pressing a key on either a physical or onscreen keyboard.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;INP consists of three subparts -&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Input Delay: The time before any callback for an interaction is handled.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Processing Duration: The time for all the callbacks to execute.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Presentation Delay: The time after the callbacks have been executed until the frame is presented on the user's screen.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-issue&quot;&gt;The Issue&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/8d3ec108-93aa-4d65-ae86-5c95264b8d04.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;As you can see in the gif, that I dropped the piece at around 1500ms but it stays in the air until about 1800ms. This resulted in a janky looking interaction when I made the first move.&lt;/p&gt;
&lt;h4 id=&quot;starting-the-analysis&quot;&gt;Starting the Analysis&lt;/h4&gt;
&lt;p&gt;Step 1: Go to Performance Tab in Chrome&lt;/p&gt;
&lt;p&gt;Step 2: In Environment settings, apply CPU throttling to 4x/6x slowdown to ensure consistent reproduction&lt;/p&gt;
&lt;p&gt;Step 3: Record (Ctrl+E)&lt;/p&gt;
&lt;p&gt;Step 4: Play the Move&lt;/p&gt;
&lt;p&gt;Step 5: Stop Recording&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/52def5f7-660f-4071-bd6d-324c73a31f79.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;With throttling enabled, we are able to get even bigger INP (352ms). For the rest of the article, this will be our basis of investigation.&lt;/p&gt;
&lt;h4 id=&quot;interpreting-the-analysis&quot;&gt;Interpreting the analysis&lt;/h4&gt;
&lt;p&gt;Hovering over &lt;em&gt;Interactions&lt;/em&gt; shows us that -&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/843d37c8-baaa-4965-bdf7-eb1923151054.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;the delay is due to Presentation delay.&lt;/p&gt;
&lt;p&gt;Clicking on &lt;em&gt;Main&lt;/em&gt; shows us the main culprit -&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/21341fe5-f99b-4a36-9200-ea6231df3d06.png&quot; alt=&quot;Almost all the time is taken by AudioContext&quot;&gt;&lt;/p&gt;
&lt;p&gt;We are using &lt;code&gt;AudioContext&lt;/code&gt; to generate a &quot;thud&quot; sound when a piece is dropped. Since I didn't want to increase the size of my bareboard unnecessarily, I thought it would be best to just generate it using Web Audio API. You can read more about it on &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API&quot;&gt;MDN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let's look at the code where it happens -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function playMoveSound() {
  if (!props.enableSound) return

  // If the user provided a custom URL, play that
  if (props.moveSoundUrl) {
    const audio = new Audio(props.moveSoundUrl)
    audio.play().catch((err) =&amp;gt; console.warn('Audio blocked:', err))
    return
  }

  try {
    const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
    const ctx = new AudioContext();
    const osc = ctx.createOscillator();
    const gainNode = ctx.createGain();

    osc.type = 'sine'; // Deep smooth tone
    osc.frequency.setValueAtTime(150, ctx.currentTime); // Start low
    osc.frequency.exponentialRampToValueAtTime(40, ctx.currentTime + 0.05); // Drop fast for a &quot;thud&quot;

    gainNode.gain.setValueAtTime(1, ctx.currentTime); // Start loud
    gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); // Fade out instantly

    osc.connect(gainNode);
    gainNode.connect(ctx.destination);

    osc.start();
    osc.stop(ctx.currentTime + 0.1);
  } catch (e) {
    // Silent fail if browser blocks AudioContext
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a closer look, we find that this line -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const ctx = new AudioContext();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is causing the issue. If we could somehow not do this operation when dropping the piece for the first time, it should resolve the problem.&lt;/p&gt;
&lt;h4 id=&quot;the-fix&quot;&gt;The Fix&lt;/h4&gt;
&lt;p&gt;On first instinct, it appears that we can just shift the initialization to when the user touches the piece instead of when he drops it, but that would be like playing Whack-a-Mole with the INP delay.&lt;/p&gt;
&lt;p&gt;A second thought would be that we can do it when the user clicks anywhere on the board, but usually the first click a user makes is on the piece itself, so that wouldn't work as well.&lt;/p&gt;
&lt;p&gt;Turns out, the proper solution is to do initialization when the component loads not on first user interaction -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let sharedAudioCtx: AudioContext | null = null;

onMounted(() =&amp;gt; {
  if (!props.enableSound || props.moveSoundUrl) return;

  try {
    const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
    sharedAudioCtx = new AudioContext();
    // State is naturally 'suspended' here due to browser policies
  } catch (e) {
    console.warn(&quot;Web Audio API not supported&quot;, e);
  }
})

function wakeUpAudio() {
  if (sharedAudioCtx &amp;amp;&amp;amp; sharedAudioCtx.state === 'suspended') {
    sharedAudioCtx.resume().catch(() =&amp;gt; {});
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's one issue though, when we were calling new AudioContext() upon user interaction, the browser didn't put it in a suspended state. You can only play sounds using AudioContext upon user interaction (to prevent website from playing annoying sounds to uninterested users).&lt;/p&gt;
&lt;p&gt;This fix initializes the audioContext without user interaction hence the browser would put it in a &lt;code&gt;suspended&lt;/code&gt; state. This is in accordance with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.w3.org/TR/webaudio-1.1/#dom-baseaudiocontext-onstatechange&quot;&gt;spec&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Due to this, we need to call &lt;code&gt;resume&lt;/code&gt; on the audioContext instance upon user interaction, thankfully that's an instantaneous operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function wakeUpAudio() {
  if (sharedAudioCtx &amp;amp;&amp;amp; sharedAudioCtx.state === 'suspended') {
    sharedAudioCtx.resume().catch(() =&amp;gt; {});
  }
}

&amp;lt;template&amp;gt;
  &amp;lt;div
      class=&quot;bareboard-wrapper&quot;
      @pointerdown.capture.once=&quot;wakeUpAudio&quot;
  &amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that the context is in &lt;code&gt;running&lt;/code&gt; state as soon as the user clicks anywhere on the board (whether it is on the piece or not).&lt;/p&gt;
&lt;p&gt;Now we only re-use the existing warm instance to play the sound -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;function playMoveSound() {
  if (!props.enableSound) return

  if (props.moveSoundUrl) {
    const audio = new Audio(props.moveSoundUrl)
    audio.play().catch((err) =&amp;gt; console.warn('Audio blocked:', err))
    return
  }

  try {
    if (!sharedAudioCtx) return;

    const osc = sharedAudioCtx.createOscillator();
    const gainNode = sharedAudioCtx.createGain();
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's check how we are doing now -&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/3b9a056b-fa76-489b-bfb0-86f7cb7663f2.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The INP is down to 102 ms !! This is well within the limits (200ms) and the &lt;code&gt;onDragEnd&lt;/code&gt; function is down to 50ms from around 250ms.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/0591f0df-46e7-4b30-8b96-cf00550cffd0.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;bonus&quot;&gt;Bonus&lt;/h4&gt;
&lt;p&gt;You might have notice &lt;code&gt;js-to-wasm::i&lt;/code&gt; and &lt;code&gt;generator.wasm&lt;/code&gt; in the screenshots. Since the board doesn't contain any calculation or move logic, we provide the valid sqaures on touch of a piece to the Bareboard from our consumer. The consumer in this case is a simple &lt;code&gt;index.html&lt;/code&gt; file which imports &lt;code&gt;generator.wasm&lt;/code&gt; file which is created by compiling &lt;code&gt;d4d5&lt;/code&gt; (another piece of the puzzle of 8bye8 suite that's written in Go for headless move verification) to wasm. &lt;code&gt;d4d5&lt;/code&gt; is going to run on the server to check if the move played by user is valid. We are compiling the same code to wasm to generate valid moves and to highlight the squares. The valid squares are passed to the BareBoard so that it can highlight the squares and allow a valid move to be played&lt;/p&gt;
&lt;p&gt;Let's look at what happens when we touch a piece and drop a piece -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function startDrag(event: MouseEvent, piece: string, rankIndex: number, fileIndex: number) {
  event.preventDefault()

  const isWhitePiece = piece === piece.toUpperCase()
  const isWhiteTurn = activeColor.value === 'w'

  if (isWhitePiece !== isWhiteTurn) {
    return
  }

  activeDrag.value = {
    piece,
    fromRank: rankIndex,
    fromFile: fileIndex,
    x: event.clientX,
    y: event.clientY,
  }

  const fromRank = activeDrag.value.fromRank
  const fromFile = activeDrag.value.fromFile

  emit('pieceTouched', {
    fileIndex: fromFile,
    rankIndex: fromRank,
  })

  window.addEventListener('mousemove', onDragMove)
  window.addEventListener('mouseup', onDragEnd)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function onDragEnd(event: MouseEvent) {
  if (!activeDrag.value) return

  window.removeEventListener('mousemove', onDragMove)
  window.removeEventListener('mouseup', onDragEnd)

  const fromRank = activeDrag.value.fromRank
  const fromFile = activeDrag.value.fromFile
  const piece = activeDrag.value.piece
  const path = event.composedPath() as HTMLElement[]
  const squareEl = path.find((el) =&amp;gt; el.classList &amp;amp;&amp;amp; el.classList.contains('grid__rank__square'))

  activeDrag.value = null

  if (squareEl) {
    const toRank = Number(squareEl.getAttribute('data-rank'))
    const toFile = Number(squareEl.getAttribute('data-file'))
    const sourcePiece = grid.value[fromRank][fromFile]

    if (toRank !== fromRank || toFile !== fromFile) {
      if (isValidMove(toFile, toRank)) {
        if ((sourcePiece === 'P' &amp;amp;&amp;amp; toRank === 0) || (sourcePiece === 'p' &amp;amp;&amp;amp; toRank === 7)) {
          promotionRank.value = toRank
          promotionFile.value = toFile
          promotionSourceRank.value = fromRank
          promotionSourceFile.value = fromFile
          validMovesSet.value = new Set()
          return
        }

        let movePlayed: Move = {
          sourceSquare: { fileIndex: fromFile, rankIndex: fromRank },
          targetSquare: { fileIndex: toFile, rankIndex: toRank },
        }

        grid.value[fromRank][fromFile] = null
        grid.value[toRank][toFile] = piece

        emit('movePlayed', movePlayed)
        playMoveSound()
      } else {
        grid.value[fromRank][fromFile] = piece
      }
    }
  } else {
    grid.value[fromRank][fromFile] = piece
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turns out that in both of these functions, we are emitting an event i.e. &lt;code&gt;pieceTouched&lt;/code&gt; and &lt;code&gt;movePlayed&lt;/code&gt;. The listeners attached to it are called synchronously. (Wait, What ? Aren't they scheduled onto callback queue ? What about the event loop and asynchronous nature of callbacks ?) Well here's the truth -&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An&lt;/strong&gt; &lt;code&gt;emit&lt;/code&gt; &lt;strong&gt;(like in Vue, React, or Node's&lt;/strong&gt; &lt;code&gt;EventEmitter&lt;/code&gt;&lt;strong&gt;) does not interact with the Event Loop or the Callback Queue at all.&lt;/strong&gt; It is just a regular, synchronous JavaScript function call in disguise.&lt;/p&gt;
&lt;p&gt;When you use a custom event system like Vue's &lt;code&gt;emit('myEvent')&lt;/code&gt; or Node's &lt;code&gt;emitter.emit()&lt;/code&gt;, you are using the &lt;strong&gt;Observer Pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Under the hood, an event emitter is literally just a JavaScript object with an array of functions attached to a string key.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This is roughly what an EventEmitter looks like under the hood
const fakeEmitter = {
  events: {},
  
  on(eventName, callback) {
    if (!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
  },

  emit(eventName, data) {
    const listeners = this.events[eventName];
    if (listeners) {
      listeners.forEach(callback =&amp;gt; callback(data)); 
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can read more on this &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codementor.io/@simenli/demystifying-asynchronous-programming-part-2-node-js-eventemitter-7r51ivby4&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Anyways, what's clear is that we are calling our wasm functions synchronously which was contributing to the presentation delay we faced earlier. To make the interaction seem instantaneous, we can fire the event after the frame has been painted, i.e.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;After the user can visually see that the piece has been picked and&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After the piece has been placed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Can you guess what's coming next ?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/cfeff26b-ed12-49be-b911-ca2f0399edf3.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let's defer emitting the events to the next iteration of the event loop -&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function startDrag(event: MouseEvent, piece: string, rankIndex: number, fileIndex: number) {
  ...
  setTimeout(() =&amp;gt; {
    emit('pieceTouched', {
      fileIndex: fromFile,
      rankIndex: fromRank,
    })
  }, 0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function onDragEnd(event: MouseEvent) {
  ...
  setTimeout(() =&amp;gt; {
    emit('movePlayed', movePlayed)
    playMoveSound()
  }, 0)
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's see how we are doing now -&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/6a3defecb0fdd40aaca7fd49/44391baa-93ab-4635-9c59-ad227c84c0ee.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;37ms!! That's the best we can pull off for this tiny BareBoard.&lt;/p&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;Performance optimization on the web is crucial for an open source library adoption, most of the times it is almost always about choreographing when those instructions run. By understanding how the browser’s Event Loop actually interacts with the Call Stack and Web APIs, we were able to turn a janky 350ms freeze into a smooth 37ms interaction.&lt;/p&gt;
&lt;p&gt;Github: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/8bye8/bareboard&quot;&gt;https://github.com/8bye8/bareboard&lt;/a&gt;&lt;br&gt;
Npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@8bye8/bareboard&quot;&gt;https://www.npmjs.com/package/@8bye8/bareboard&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;credits&quot;&gt;Credits&lt;/h4&gt;
&lt;p&gt;The Event Loop | Jake Archibald &lt;strong&gt;:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=qz6yDqjMVfw&quot;&gt;https://www.youtube.com/watch?v=qz6yDqjMVfw&lt;/a&gt;&lt;br&gt;
Optimizing INP | A deep dive: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=cmtfM4emG5k&quot;&gt;https://www.youtube.com/watch?v=cmtfM4emG5k&lt;/a&gt;&lt;br&gt;
Demystifying Asynchronous Programming Part 2: Node.js EventEmitter:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codementor.io/@simenli/demystifying-asynchronous-programming-part-2-node-js-eventemitter-7r51ivby4&quot;&gt;https://www.codementor.io/@simenli/demystifying-asynchronous-programming-part-2-node-js-eventemitter-7r51ivby4&lt;/a&gt;&lt;br&gt;
Interaction to Next Paint (INP): &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;https://web.dev/articles/inp&lt;/a&gt;&lt;br&gt;
Avoid large, complex layouts and layout thrashing: &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing&quot;&gt;https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26890/interaction-to-next-paint-breakdown-solving-first-move-lag-in-bareboard</guid>
<pubDate>Fri, 11 Sep 2026 20:33:16 +0000</pubDate>
</item>
<item>
<title>Understanding INP: The Newest Core Web Vital and Why It Matters</title>
<link>https://coderlegion.com/14050/understanding-inp-the-newest-core-web-vital-and-why-it-matters</link>
<description>&lt;p&gt;If you have been optimising for &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/category/core-web-vitals&quot;&gt;Core Web Vitals&lt;/a&gt; for a few years, you will remember &lt;strong&gt;First Input Delay (FID)&lt;/strong&gt; as the “interactivity” metric. That role now belongs to &lt;strong&gt;Interaction to Next Paint (INP)&lt;/strong&gt;. Google &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/inp-cwv-march-12&quot;&gt;promoted INP to a stable Core Web Vital on 12 March 2024&lt;/a&gt; and retired FID from the programme at the same time. INP is not a minor rename—it measures a fuller slice of the experience, and it is the number you should expect in &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://pagespeed.web.dev/&quot;&gt;PageSpeed Insights&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://search.google.com/search-console&quot;&gt;Search Console&lt;/a&gt;, and any serious performance report.&lt;/p&gt;&lt;p&gt;This article explains what INP is, why the change happened, and what to do about it in day-to-day work—including when you are responsible for &lt;strong&gt;many&lt;/strong&gt; client sites rather than a single product. For step-by-step fixes, pair it with our deeper guide on &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lcp-inp-cls-what-each-core-web-vital-means-and-how-to-fix-it&quot;&gt;LCP, INP, and CLS&lt;/a&gt;; for the wider CWV picture, start with &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/what-are-core-web-vitals-a-practical-guide-for-2026&quot;&gt;What Are Core Web Vitals?&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;What INP measures (in plain terms)&lt;/h2&gt;&lt;p&gt;INP captures &lt;strong&gt;responsiveness&lt;/strong&gt;: how long it takes from a user’s discrete action until the browser can &lt;strong&gt;paint the next frame&lt;/strong&gt; that reflects that action. Eligible interactions are &lt;strong&gt;clicks&lt;/strong&gt;, &lt;strong&gt;taps&lt;/strong&gt;, and &lt;strong&gt;key presses&lt;/strong&gt; on the page. Hovering and scrolling are out of scope for INP, which keeps the metric focused on deliberate gestures that expect immediate feedback.&lt;/p&gt;&lt;p&gt;Google’s documentation breaks each interaction into phases that developers actually debug: &lt;strong&gt;input delay&lt;/strong&gt; (waiting for the main thread), &lt;strong&gt;processing time&lt;/strong&gt; (your event handlers), and &lt;strong&gt;presentation delay&lt;/strong&gt; (work before the next paint). The slowest of those phases dominates the interaction’s latency. The page’s reported INP is derived from the interactions observed during the visit—for most pages that is effectively the &lt;strong&gt;worst&lt;/strong&gt; interaction; on very chatty pages, the methodology &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;discounts rare outliers&lt;/a&gt; so one freak delay does not drown out an otherwise snappy experience.&lt;/p&gt;&lt;p&gt;Field scoring uses the &lt;strong&gt;75th percentile&lt;/strong&gt; of page loads (split by mobile and desktop), consistent with other Core Web Vitals. The public thresholds are:&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;Rating&lt;/p&gt;&lt;/th&gt;&lt;th rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;INP (field)&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;Good&lt;/p&gt;&lt;/td&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;≤ 200 ms&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;Needs improvement&lt;/p&gt;&lt;/td&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;200–500 ms&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;Poor&lt;/p&gt;&lt;/td&gt;&lt;td rowspan=&quot;1&quot; colspan=&quot;1&quot;&gt;&lt;p&gt;&amp;gt; 500 ms&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Those numbers are not aspirational labels—they are what Google uses when it evaluates real-user experience at scale.&lt;/p&gt;&lt;h2&gt;Where INP sits next to LCP and CLS&lt;/h2&gt;&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/what-are-core-web-vitals-a-practical-guide-for-2026&quot;&gt;Core Web Vitals&lt;/a&gt; are still a set of three: &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/tag/lcp&quot;&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/a&gt; for loading, &lt;strong&gt;INP&lt;/strong&gt; for responsiveness, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/tag/cls&quot;&gt;&lt;strong&gt;CLS&lt;/strong&gt;&lt;/a&gt; for visual stability. They answer different questions. You can ship a fast first paint and still fail INP if the main thread is busy when someone opens a menu; you can pass INP on a lean marketing page and still fail CLS if images without dimensions push content around. In practice, teams prioritise &lt;strong&gt;LCP&lt;/strong&gt; first because it is easy to explain to stakeholders and often tied to hero assets and CDN configuration. &lt;strong&gt;INP&lt;/strong&gt; rewards the same discipline—JavaScript budget, tag governance, framework choices—but shows up in different URLs and flows, especially after hydration. Treat the three metrics as separate dials, not one score.&lt;/p&gt;&lt;h2&gt;Why FID was not enough&lt;/h2&gt;&lt;p&gt;FID only looked at the &lt;strong&gt;first&lt;/strong&gt; interaction on a page, and only at &lt;strong&gt;input delay&lt;/strong&gt;: time before the browser started handling the event. That made FID useful for catching catastrophic main-thread blocking during load, but it ignored everything that happens &lt;strong&gt;after&lt;/strong&gt; the page is interactive. Google notes that &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;Chrome usage data shows most of a typical visit happens after load&lt;/a&gt;; a slow menu, cart step, or client-rendered route change could leave FID looking fine while users still felt a sluggish product.&lt;/p&gt;&lt;p&gt;INP closes that gap by measuring responsiveness &lt;strong&gt;across the full session&lt;/strong&gt; and including &lt;strong&gt;processing and presentation&lt;/strong&gt;, not just the queue in front of the first handler. That is why INP is a better match for modern sites heavy on JavaScript, third-party widgets, and single-page transitions—exactly the stacks agencies ship for clients every week.&lt;/p&gt;&lt;h2&gt;Why INP matters for SEO and for users&lt;/h2&gt;&lt;p&gt;Core Web Vitals are part of Google’s &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/tag/seo&quot;&gt;page experience signals&lt;/a&gt;. INP is the responsiveness pillar: poor scores indicate real friction—double taps, abandoned forms, rage clicks—while good scores mean the UI keeps up with input. Search is not the only reason to care; conversion and support tickets follow the same physics.&lt;/p&gt;&lt;p&gt;For teams managing &lt;strong&gt;portfolios&lt;/strong&gt; of sites, INP adds a wrinkle: the worst interactions often sit on &lt;strong&gt;templates&lt;/strong&gt; (navigation, checkout, lead forms) or on &lt;strong&gt;third-party&lt;/strong&gt; scripts shared across properties. One slow pattern can drag INP for every page that uses it. That is less visible in a one-off Lighthouse run than in &lt;strong&gt;field&lt;/strong&gt; data or repeated URL-level checks over time—which is why operational monitoring and regression alerts matter alongside manual audits.&lt;/p&gt;&lt;h2&gt;How to see INP in practice&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;PageSpeed Insights&lt;/strong&gt; pulls &lt;strong&gt;field&lt;/strong&gt; data from the &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://developer.chrome.com/docs/crux&quot;&gt;Chrome User Experience Report (CrUX)&lt;/a&gt; when your origin or URL has enough traffic; that is the authoritative place to see whether you pass INP at the 75th percentile. &lt;strong&gt;Lab&lt;/strong&gt; tools do not compute INP directly; Lighthouse’s &lt;strong&gt;Total Blocking Time (TBT)&lt;/strong&gt; is a rough proxy for main-thread contention that often tracks with INP problems, but it is not interchangeable. When CrUX data is missing—common on small or new sites—use &lt;strong&gt;real user monitoring (RUM)&lt;/strong&gt; if you have it, or fall back to manual profiling in Chrome DevTools for representative flows.&lt;/p&gt;&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://search.google.com/search-console&quot;&gt;Search Console’s Core Web Vitals report&lt;/a&gt; surfaces INP (and no longer treats FID as a Core Web Vital) after the March 2024 transition, so keep INP in scope when you triage URL groups and templates.&lt;/p&gt;&lt;p&gt;If you are building a repeatable workflow for clients—baselines, fixes, then proof—our &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/core-web-vitals-monitoring-checklist-for-agencies&quot;&gt;Core Web Vitals monitoring checklist for agencies&lt;/a&gt; ties these metrics to review cadence and ownership. For setup across many monitored URLs, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt; walks through the operational pieces.&lt;/p&gt;&lt;h2&gt;What usually hurts INP&lt;/h2&gt;&lt;p&gt;These patterns show up repeatedly in audits:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long tasks on the main thread&lt;/strong&gt; — parsing and executing large JavaScript bundles, synchronously updating heavy DOM trees, or blocking styles/layout after an interaction.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Third-party tags&lt;/strong&gt; — analytics, chat, consent banners, and A/B snippets competing for the same thread as your UI handlers.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large DOMs and expensive selectors&lt;/strong&gt; — interactions that trigger wide reflows or style recalculations on complex pages.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-heavy rendering&lt;/strong&gt; — SPAs that wait on data or hydration before showing feedback; users perceive that as “nothing happened” even when the network is fast.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A concrete pattern we see on client sites: the &lt;strong&gt;first&lt;/strong&gt; click after load feels fine (FID would have looked healthy), but the &lt;strong&gt;fifth&lt;/strong&gt; interaction—opening a filtered product grid, submitting a multi-step form, or toggling a sticky nav—hits a long task left behind by a tag or a bundle split. INP catches that; FID did not. &lt;strong&gt;Embeds&lt;/strong&gt; deserve attention too: slow interactions inside &lt;strong&gt;iframes&lt;/strong&gt; still count toward the page-level INP users see, while your own JavaScript cannot inspect cross-origin iframe code—so field data and DevTools frame selection matter when &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/crux-and-rum-differences#iframes&quot;&gt;CrUX and RUM disagree&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Google’s own guides on &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/optimize-long-tasks&quot;&gt;optimising long tasks&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/optimize-input-delay&quot;&gt;input delay&lt;/a&gt;, and &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/find-slow-interactions-in-the-field&quot;&gt;interaction diagnostics&lt;/a&gt; are the right next step once you know &lt;strong&gt;which&lt;/strong&gt; interaction is slow.&lt;/p&gt;&lt;h2&gt;INP and Apogee Watcher&lt;/h2&gt;&lt;p&gt;Apogee Watcher is built to run &lt;strong&gt;scheduled PageSpeed-class tests&lt;/strong&gt; across many sites and routes, surface &lt;strong&gt;lab and field&lt;/strong&gt; signals where the API provides them, and alert you when scores move. INP is fundamentally a &lt;strong&gt;field-first&lt;/strong&gt; metric: fixing it means reproducing real interactions, trimming main-thread work, and re-checking user journeys—not a single synthetic number in isolation. Use Watcher to &lt;strong&gt;watch for regressions&lt;/strong&gt; when you ship framework upgrades, tag managers, or new client themes; pair those signals with DevTools and CrUX for the interactions CrUX cannot explain line-by-line.&lt;/p&gt;&lt;p&gt;If you are not monitoring yet, start with a baseline on your highest-traffic templates, then expand. &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;Create a free account&lt;/a&gt; to add sites and budgets without wiring up PSI by hand for every property.&lt;/p&gt;&lt;h2&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;When did INP replace FID as a Core Web Vital?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;INP became an official Core Web Vital and replaced FID on &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/inp-cwv-march-12&quot;&gt;12 March 2024&lt;/a&gt;, per Google’s Web Vitals programme.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What is a good INP score?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;At the 75th percentile of field data, &lt;strong&gt;200 ms or less&lt;/strong&gt; is “good”, &lt;strong&gt;over 500 ms&lt;/strong&gt; is “poor”, with a band between for “needs improvement”—see &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp#what_is_a_good_inp_score&quot;&gt;Google’s INP documentation&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Does INP include scrolling or hover?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. INP observes &lt;strong&gt;click, tap, and keyboard&lt;/strong&gt; interactions. Scrolling and hover are not part of the metric, though some gestures may include a click or tap that is measured.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Is INP the same as Total Blocking Time?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. &lt;strong&gt;TBT&lt;/strong&gt; is a &lt;strong&gt;lab&lt;/strong&gt; proxy related to main-thread blocking during load. &lt;strong&gt;INP&lt;/strong&gt; is a &lt;strong&gt;field&lt;/strong&gt; metric covering full-session interactions. They often move together when JavaScript is the culprit, but they are not identical.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Why should agencies track INP separately from LCP?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;LCP&lt;/strong&gt; measures loading; &lt;strong&gt;INP&lt;/strong&gt; measures responsiveness after content is on screen. A page can have an acceptable LCP and still fail INP because of client-side code, third parties, or heavy UI after load—common on marketing sites and apps your clients maintain for years.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Further reading (Google):&lt;/strong&gt; &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/inp&quot;&gt;Interaction to Next Paint (INP)&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/inp-cwv-march-12&quot;&gt;INP becomes a Core Web Vital — March 12&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/optimize-inp&quot;&gt;Optimize Interaction to Next Paint&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/14050/understanding-inp-the-newest-core-web-vital-and-why-it-matters</guid>
<pubDate>Thu, 23 Apr 2026 22:56:03 +0000</pubDate>
</item>
<item>
<title>Mobile vs Desktop Core Web Vitals: Why You Need to Monitor Both</title>
<link>https://coderlegion.com/14056/mobile-vs-desktop-core-web-vitals-why-you-need-to-monitor-both</link>
<description>&lt;p&gt;Most teams treat Core Web Vitals like one scoreboard. They run a quick test, pick a single set of scores, and move on. The problem is that Core Web Vitals are defined per &lt;strong&gt;page load&lt;/strong&gt;; &lt;strong&gt;device&lt;/strong&gt; shows up in &lt;em&gt;how&lt;/em&gt; you measure—mobile versus desktop in lab emulation, or in field data grouped by form factor. The same URL can therefore produce different LCP, INP, and CLS results across those contexts. Mobile and desktop travel different routes through your UI, assets, network conditions, and interaction patterns. If you monitor only one side, you will miss regressions that matter—and you will struggle to justify prioritisation when clients ask, “Why did this change?”&lt;/p&gt;&lt;h2&gt;The trap: one device, one story&lt;/h2&gt;&lt;p&gt;It’s easy to fall into a workflow like this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Mobile is “important”, so you track mobile CWV only.&lt;/li&gt;&lt;li&gt;Desktop is “nice to have”, so you leave it until later.&lt;/li&gt;&lt;li&gt;When something breaks, you re-run tests and pick the latest scores to explain the incident.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This approach has two predictable failure modes:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Hidden regressions&lt;/strong&gt; — A fix that improves desktop experience can still leave mobile failing (or vice versa).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Unclear communication&lt;/strong&gt; — You end up explaining different symptoms with one set of numbers. Clients rightly lose confidence.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;If you want monitoring that supports real delivery and client-ready reporting, you need a repeatable &lt;em&gt;paired&lt;/em&gt; view: &lt;strong&gt;mobile + desktop&lt;/strong&gt; for the same pages, the same cadence, and the same triage rules.&lt;/p&gt;&lt;p&gt;If you need a quick primer on the metrics themselves, start with &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/what-are-core-web-vitals-a-practical-guide-for-2026&quot;&gt;What Are Core Web Vitals?&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;Why mobile vs desktop Core Web Vitals diverge&lt;/h2&gt;&lt;p&gt;Core Web Vitals can look “similar” at a headline level, but the underlying causes often differ. The most common reasons:&lt;/p&gt;&lt;h3&gt;Different rendering and loading conditions&lt;/h3&gt;&lt;p&gt;Mobile is constrained: less CPU headroom, different caching patterns, and more aggressive resource variation. That shows up strongly in:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;LCP&lt;/strong&gt; (Largest Contentful Paint): often driven by large images, fonts, or other heavy elements on the critical path (the LCP element can also be a text block or a video poster, depending on the page). If those pieces behave differently across breakpoints, LCP will diverge.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Different interaction patterns (INP)&lt;/h3&gt;&lt;p&gt;INP captures interaction responsiveness, which often behaves differently by device due to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Touch vs pointer interactions&lt;/li&gt;&lt;li&gt;Different event targets and UI density&lt;/li&gt;&lt;li&gt;Varying execution cost on lower-end hardware&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you only look at one device’s INP, you can miss “works on desktop” but “feels slow on phones” issues.&lt;/p&gt;&lt;h3&gt;Different layout stability (CLS)&lt;/h3&gt;&lt;p&gt;CLS is highly sensitive to responsive layout, late-loading elements, and dynamic components that appear only under certain breakpoints (ads, banners, late injected UI, etc.). A page can be stable on desktop and still shift on mobile.&lt;/p&gt;&lt;p&gt;For a deeper explanation of what each metric actually means, see &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lcp-inp-cls-what-each-core-web-vital-means-and-how-to-fix-it&quot;&gt;LCP, INP, CLS: What Each Core Web Vital Means and How to Fix It&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;What to monitor in practice (beyond the total score)&lt;/h2&gt;&lt;p&gt;Monitoring both devices is necessary, but it’s not sufficient by itself.&lt;/p&gt;&lt;p&gt;You also need to monitor &lt;em&gt;the right dimensions&lt;/em&gt;:&lt;/p&gt;&lt;h3&gt;1. Treat each device as its own budget&lt;/h3&gt;&lt;p&gt;Instead of one “CWV target” for the page, define device-specific thresholds for the metrics you care about. A pass on desktop should not automatically compensate for a fail on mobile.&lt;/p&gt;&lt;p&gt;This keeps decisions defensible when you propose fixes to stakeholders.&lt;/p&gt;&lt;p&gt;If you want a structured way to set thresholds, use our &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/performance-budget-thresholds-template&quot;&gt;Performance Budget Thresholds Template&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;2. Compare by change, not by absolute numbers alone&lt;/h3&gt;&lt;p&gt;Absolute CWV numbers can vary slightly run-to-run. What should drive action is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;What changed since the last known good period&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Which metric moved first&lt;/strong&gt; (LCP vs INP vs CLS)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Which device changed&lt;/strong&gt; (mobile-first incidents vs desktop-only issues)&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;3. Triage using “worst metric per device”&lt;/h3&gt;&lt;p&gt;A practical triage rule:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;For mobile: focus on the single worst metric (LCP/INP/CLS) and the reason it is failing.&lt;/li&gt;&lt;li&gt;For desktop: do the same.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Then decide whether you have a shared root cause (both devices fail similarly) or breakpoint/device-specific problems.&lt;/p&gt;&lt;h2&gt;A practical monitoring workflow for agencies&lt;/h2&gt;&lt;p&gt;If you want this to work across clients and pages, keep it repeatable. Here’s a workflow you can run every week:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Choose pages that reflect real funnel risk&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Homepage (first impression)&lt;/li&gt;&lt;li&gt;Key category or service pages (high intent)&lt;/li&gt;&lt;li&gt;Conversion pages (where UX pain becomes business cost)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Run paired analysis for the same pages&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Mobile + desktop tests on the same cadence&lt;/li&gt;&lt;li&gt;Stable analysis conditions (avoid mixing “random checks” with planned ones)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Record findings in the same order&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Mobile: LCP → INP → CLS (and a quick “what it likely affects” note)&lt;/li&gt;&lt;li&gt;Desktop: LCP → INP → CLS&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Assign device-specific priority&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;If mobile fails and desktop passes, your “first fix” should usually start mobile unless you have a desktop-only user segment.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Set alerts that match the story&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Budget breaches on a device should trigger the device’s reporting, not just the overall page score.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Deliver client-ready reporting&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;Give clients a paired narrative: “what you saw” and “what you changed next”.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;For the setup side of automated monitoring across multiple sites, see &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And if you’re turning this into deliverables, the &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/client-ready-core-web-vitals-report-outline&quot;&gt;Client-Ready Core Web Vitals Report Outline&lt;/a&gt; helps you package the evidence into something stakeholders actually read.&lt;/p&gt;&lt;h2&gt;Interpreting results: three common scenarios&lt;/h2&gt;&lt;p&gt;When you review mobile vs desktop CWV together, you’ll usually fall into one of these buckets:&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;What it often means&lt;/th&gt;&lt;th&gt;How you should respond&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mobile failing, desktop ok&lt;/td&gt;&lt;td&gt;Mobile critical path or interactive cost is the bottleneck&lt;/td&gt;&lt;td&gt;Prioritise mobile-first fixes (hero/LCP and interaction latency)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Desktop failing, mobile ok&lt;/td&gt;&lt;td&gt;Desktop-only layout or heavy JS path&lt;/td&gt;&lt;td&gt;Focus on desktop breakpoints, scripts, and layout stability&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Both failing consistently&lt;/td&gt;&lt;td&gt;Shared design debt or performance regressions&lt;/td&gt;&lt;td&gt;Treat it as a system-level problem, then verify with paired reruns&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The important part is that you avoid the “one score explains everything” habit. Paired monitoring turns CWV from a vanity chart into a decision system.&lt;/p&gt;&lt;h2&gt;What your client update should say (example wording)&lt;/h2&gt;&lt;p&gt;When clients ask “why are there two scores?”, they usually want a single paragraph that explains what changed and what you will do next.&lt;/p&gt;&lt;p&gt;Here’s a simple template you can reuse:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Mobile-first incident (mobile fails, desktop ok):&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;“Mobile performance is out of budget right now (LCP/INP/CLS), while desktop is within budget. This pattern usually points to a mobile-specific bottleneck: hero rendering timing, touch interaction cost, or a late-loading component that behaves differently on smaller breakpoints.”&lt;/li&gt;&lt;li&gt;“Next step: we’ll apply a targeted first-fix on the affected mobile path, then re-run the same paired checks to confirm both the device budget and the user-story are improving.”&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Shared incident (both devices failing in a similar way):&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;“Both mobile and desktop are currently out of budget for the same metric area. That usually means a shared component, shared asset, or shared build/deploy change rather than a single device quirk.”&lt;/li&gt;&lt;li&gt;“Next step: we’ll verify what changed in the release window, then test the fix with paired checks to ensure the improvement holds across devices.”&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you include field data (for example, when you do CrUX-aware reporting), keep the wording consistent: explain what your lab checks caught, and how the field view supports the same story. The goal is less debate and more action.&lt;/p&gt;&lt;h2&gt;A quick consistency check for every report&lt;/h2&gt;&lt;p&gt;Before you send your update, run this quick self-check:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Are you using the same page set for both devices (so the comparison is fair)?&lt;/li&gt;&lt;li&gt;Did you name the &lt;em&gt;first&lt;/em&gt; metric to fail on each device (so the triage is clear)?&lt;/li&gt;&lt;li&gt;Does your next step match the device that is failing (or explain why you’re fixing something shared first)?&lt;/li&gt;&lt;li&gt;Did you state how you will verify after changes (re-run paired checks, not “we’ll look later”)?&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Client-ready reporting: avoid confusion with one sentence&lt;/h2&gt;&lt;p&gt;Clients often ask why you’re reporting two device scores.&lt;/p&gt;&lt;p&gt;Your answer should be simple:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;“Mobile and desktop measure different experiences. We monitor both so we catch issues that only appear on one device and so our fixes stay prioritised and verifiable.”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;When you present this in a consistent report format, it is easier for stakeholders to focus on the next actions instead of arguing over a single headline score.&lt;/p&gt;&lt;h2&gt;Operational checklist for your next report&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Pick the same set of pages every reporting cycle&lt;/li&gt;&lt;li&gt;Confirm mobile + desktop are both included in the schedule&lt;/li&gt;&lt;li&gt;Use device-specific thresholds for LCP/INP/CLS&lt;/li&gt;&lt;li&gt;Triage by worst metric per device, then explain likely root cause&lt;/li&gt;&lt;li&gt;Reference a repeatable next step (fix + verify, not just “we will look into it”)&lt;/li&gt;&lt;li&gt;Share a paired narrative that stakeholders can understand in under two minutes&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Do I need to monitor both mobile and desktop if mobile gets most traffic?&lt;/strong&gt;&lt;br&gt;Yes. Even if mobile dominates, desktop still represents business risk for a portion of your audience. More importantly, fixes can improve one device while leaving the other failing. Paired monitoring prevents blind spots.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Which metric should I start with: LCP, INP, or CLS?&lt;/strong&gt;&lt;br&gt;Start with the worst metric on each device (mobile and desktop separately). In many audits, improving LCP is the fastest high-impact win, while INP usually surfaces interaction pain and CLS highlights layout instability—but the right first lever depends on what is actually failing.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How often should we review paired CWV?&lt;/strong&gt;&lt;br&gt;For most agency workflows, weekly review is a good starting point. For active releases or client campaigns, you may need tighter cadence to verify that fixes actually hold.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What if a page behaves differently by device (e.g. different layout/components)?&lt;/strong&gt;&lt;br&gt;That’s exactly why paired monitoring helps. Explain the difference in your report: the breakpoint experience is part of the user journey, and device-specific findings lead to device-specific fixes.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Will paired monitoring slow down the workflow?&lt;/strong&gt;&lt;br&gt;It shouldn’t. The operational cost should live in automation, not manual re-checks. If your monitoring is scheduled and your reporting is templated, you get more reliable evidence without extra churn.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;If you want a repeatable way to &lt;a target=&quot;_blank&quot; rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/&quot;&gt;monitor mobile and desktop Core Web Vitals&lt;/a&gt; across client portfolios (with paired evidence, budgets, and client-ready reporting), Apogee Watcher is built for that workflow.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/14056/mobile-vs-desktop-core-web-vitals-why-you-need-to-monitor-both</guid>
<pubDate>Mon, 13 Apr 2026 23:52:33 +0000</pubDate>
</item>
</channel>
</rss>