<?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: Khriztian Moreno</title>
    <description>The latest articles on DEV Community by Khriztian Moreno (@khriztianmoreno).</description>
    <link>https://dev.to/khriztianmoreno</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%2F192167%2Fe34249a8-fbd3-48ce-8afc-1418d0b944a7.jpeg</url>
      <title>DEV Community: Khriztian Moreno</title>
      <link>https://dev.to/khriztianmoreno</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khriztianmoreno"/>
    <language>en</language>
    <item>
      <title>Hacks for effective Fullstack development with React and Node</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/hacks-for-effective-fullstack-development-with-react-and-node-4g4j</link>
      <guid>https://dev.to/khriztianmoreno/hacks-for-effective-fullstack-development-with-react-and-node-4g4j</guid>
      <description>&lt;p&gt;Today I'm going to show you an optimal workflow for effective development with Node.js and React. If you've ever worked on a project with multiple package.json files, you might know the pain of juggling multiple terminal tabs, remembering which commands start which server, or handling CORS errors.&lt;/p&gt;

&lt;p&gt;Fortunately, there are some tools available that can alleviate some of these headaches.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwet8098n2jqt9lgwk78a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwet8098n2jqt9lgwk78a.png" alt="Fullstack" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Monorepo Setup for React and Node
&lt;/h2&gt;

&lt;p&gt;Let's say we're working on a &lt;a href="https://en.wikipedia.org/wiki/Monorepo" rel="noopener noreferrer"&gt;monorepo &lt;/a&gt; with two &lt;code&gt;package.json&lt;/code&gt; files: one is in a &lt;code&gt;client&lt;/code&gt; directory for a React front-end powered by &lt;a href="https://create-react-app.dev/" rel="noopener noreferrer"&gt;Create React App&lt;/a&gt;, and one is at the root of the repository for a Node back-end that exposes an API that our React app uses. Our React app runs on &lt;code&gt;localhost:3000&lt;/code&gt; and our Node app runs on &lt;code&gt;localhost:8080&lt;/code&gt;. Both apps are started with &lt;code&gt;npm start&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Since we have two &lt;code&gt;package.json&lt;/code&gt; files, this means that in order to get our front-end and back-end up and running, we need to make sure we've run &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm start&lt;/code&gt; in both the root directory and the client directory. Here's how we simplify this.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Running two servers at the same time
&lt;/h2&gt;

&lt;p&gt;One improvement we can make to our development workflow is to add a build tool to run multiple &lt;strong&gt;npm&lt;/strong&gt; commands at the same time to save us the trouble of running &lt;code&gt;npm start&lt;/code&gt; in multiple terminal tabs. To do this, we can add an npm package called &lt;a href="https://www.npmjs.com/package/concurrently" rel="noopener noreferrer"&gt;concurrently&lt;/a&gt; to the root of our project.&lt;/p&gt;

&lt;p&gt;In the root of our project, we will install it as a development dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; concurrently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in our root &lt;code&gt;package.json&lt;/code&gt; scripts, we will update our &lt;code&gt;start&lt;/code&gt; script to use them simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently --kill-others-on-fail npm run server npm run client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cd client &amp;amp;&amp;amp; npm start"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.17.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"concurrently"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6.0.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have three npm scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run server&lt;/code&gt; starts our Node application,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run client&lt;/code&gt; runs &lt;code&gt;npm start&lt;/code&gt; in the &lt;em&gt;client&lt;/em&gt; directory to start our React application,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm start&lt;/code&gt; runs &lt;code&gt;npm run server&lt;/code&gt; and &lt;code&gt;npm run client&lt;/code&gt; at the same time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Installing front-end and back-end dependencies with a single command
&lt;/h2&gt;

&lt;p&gt;Another aspect of our workflow that we can improve is the installation of dependencies. Currently, we need to manually run &lt;code&gt;npm install&lt;/code&gt; for every &lt;code&gt;package.json&lt;/code&gt; file we have when setting up the project. Instead of going through that hassle, we can add a &lt;a href="https://docs.npmjs.com/cli/v7/using-npm/scripts" rel="noopener noreferrer"&gt;postinstall script&lt;/a&gt; to our root &lt;code&gt;package.json&lt;/code&gt; to automatically run &lt;code&gt;npm install&lt;/code&gt; in the &lt;em&gt;client&lt;/em&gt; directory after the installation has finished in the root directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cd client &amp;amp;&amp;amp; npm install"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when we install our monorepo, all we need to do to get it up and running is run &lt;code&gt;npm install&lt;/code&gt; and then &lt;code&gt;npm start&lt;/code&gt; at the root of the project. There is no need to enter any other directory to run other commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Proxy API requests from the backend
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, our Node backend exposes the API endpoints that our React app will use. Let's say our Node app has a &lt;code&gt;/refresh_token&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;Out of the box, if we tried to send a GET request to &lt;code&gt;http://localhost:8080/refresh_token&lt;/code&gt; from our React app at &lt;code&gt;http://localhost:3000&lt;/code&gt;, we would run into CORS issues. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS&lt;/a&gt; stands for cross-origin resource sharing.&lt;/p&gt;

&lt;p&gt;Typically when you encounter CORS errors, it's because you're trying to access resources from another domain (i.e., &lt;code&gt;http://localhost:3000&lt;/code&gt; and &lt;code&gt;http://localhost:8080&lt;/code&gt;), and the domain you're requesting resources from is not allowed.&lt;/p&gt;

&lt;p&gt;To tell the development server to proxy any unknown requests to our API server in development, we can set up a &lt;a href="https://create-react-app.dev/docs/proxying-api-requests-in-development" rel="noopener noreferrer"&gt;proxy&lt;/a&gt; in our React app's &lt;code&gt;package.json&lt;/code&gt; file. In &lt;code&gt;client/package.json&lt;/code&gt;, we'll add a proxy for &lt;code&gt;http://localhost:8080&lt;/code&gt; (where our Node app is running).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-dom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.0.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-scripts start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-scripts build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-scripts test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-scripts eject"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if we restart the server and set a request to our Node app's &lt;code&gt;/refresh_token&lt;/code&gt; endPoint (without &lt;code&gt;http://localhost:8080&lt;/code&gt;) using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch" rel="noopener noreferrer"&gt;fetch()&lt;/a&gt;, the CORS error should be resolved.&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/refresh_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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;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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next time you're working on a monorepo project like this, try these three tips to streamline your development workflow.&lt;/p&gt;

&lt;p&gt;That's all folks! I hope this helps you become a better dev!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>react</category>
    </item>
    <item>
      <title>Semantic markup for AI: turn your forms into WebMCP tools without JavaScript</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/semantic-markup-for-ai-turn-your-forms-into-webmcp-tools-without-javascript-1nod</link>
      <guid>https://dev.to/khriztianmoreno/semantic-markup-for-ai-turn-your-forms-into-webmcp-tools-without-javascript-1nod</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/khriztianmoreno/webmcp-why-agents-shouldnt-have-to-guess-how-your-ui-works-1af1"&gt;first post in this series&lt;/a&gt;, every WebMCP tool was a JavaScript object: a name, a description, a hand-written &lt;code&gt;inputSchema&lt;/code&gt;, and an &lt;code&gt;execute&lt;/code&gt; function that called the store. It works, and for logic that lives in JavaScript it's exactly what you want. But look at your app for a second: where does most of what a user can &lt;em&gt;do&lt;/em&gt; actually live? Search, filter, subscribe, book, pay, leave a comment. Almost all of it is a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And a form is already, to a large extent, a tool description. It has a name per field (&lt;code&gt;name&lt;/code&gt;), types (&lt;code&gt;type="date"&lt;/code&gt;, &lt;code&gt;type="email"&lt;/code&gt;), rules (&lt;code&gt;required&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;), closed sets of options (&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;), and a submission endpoint. The only thing it's missing for an agent to understand it is natural-language context: what the form does and what each field means.&lt;/p&gt;

&lt;p&gt;That's what the &lt;strong&gt;WebMCP declarative API&lt;/strong&gt; proposes: a handful of attributes that turn an ordinary &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; into a tool. &lt;strong&gt;The basic tool needs zero lines of JavaScript&lt;/strong&gt;, and it degrades perfectly: a human still sees a normal form. JavaScript only comes back for one thing: answering the agent without navigating.&lt;/p&gt;

&lt;p&gt;I built a demo in the same flight domain as the previous post. The code is in &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/webmcp-declarative-forms" rel="noopener noreferrer"&gt;&lt;code&gt;demos/webmcp-declarative-forms&lt;/code&gt;&lt;/a&gt; (the &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; explains how to run it).&lt;/p&gt;

&lt;h2&gt;
  
  
  How the demo works
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;
    &lt;br&gt;
    Your browser doesn't support embedded video — &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/webmcp-declarative-forms" rel="noopener noreferrer"&gt;clone the demo&lt;/a&gt; and run it locally instead.&lt;br&gt;
  &lt;/p&gt;&lt;br&gt;The full run: the agent searching on its own, stopping at the booking form for you to review, and the three ways to cancel.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The app is SkyFare again, but this time there's no &lt;code&gt;registerTool()&lt;/code&gt; anywhere. It's &lt;strong&gt;two plain HTML forms&lt;/strong&gt; with declarative attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search_flights&lt;/code&gt;&lt;/strong&gt; has &lt;code&gt;toolautosubmit&lt;/code&gt;: the agent fills it and the form submits itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;book_flight&lt;/code&gt;&lt;/strong&gt; doesn't: the agent fills it, the submit button gets focus, and &lt;strong&gt;you&lt;/strong&gt; review and confirm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next to them are two panels. One is a &lt;strong&gt;simulated agent&lt;/strong&gt; (no LLM, same as part 1: the plan is hard-coded) with the goal &lt;em&gt;find the cheapest nonstop flight BOG → MAD for 2 passengers and book it for Ana Gómez&lt;/em&gt;. The other is a &lt;strong&gt;schema inspector&lt;/strong&gt; that shows, live, the JSON Schema that comes out of each form.&lt;/p&gt;

&lt;p&gt;In my runs (headless Chrome 154, using the shim):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent discovers 2 tools&lt;/strong&gt; from the markup, without the page registering anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search_flights&lt;/code&gt; fills and submits itself.&lt;/strong&gt; The agent gets 745 characters of JSON with the flights, and the URL doesn't change: zero navigations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;book_flight&lt;/code&gt; fills and then waits.&lt;/strong&gt; The form gets highlighted, and the "Confirm booking" button pulses and has focus. The agent can't finish until you click. When you do, it gets 313 characters of JSON with the confirmation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While it waits, you can cancel in three different ways: the agent cancels, you reset the form, or the page changes the &lt;code&gt;toolname&lt;/code&gt;. All three cut off the in-flight invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: every tool needed JavaScript
&lt;/h2&gt;

&lt;p&gt;Part 1 left us with a core idea: a tool isn't a second implementation of the feature, it's another entry point into the one you already have. But with the imperative API, you have to build that entry point by hand. This is what &lt;code&gt;search_flights&lt;/code&gt; looked like in the first demo's &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-fundamentals/tools.js" rel="noopener noreferrer"&gt;&lt;code&gt;tools.js&lt;/code&gt;&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_flights&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search flights&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search available flights between two airports. Returns every matching flight with id, airline, times, number of stops and price in USD.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&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;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;origin&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IATA code of the departure airport, e.g. "BOG".&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IATA code of the arrival airport, e.g. "MAD".&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Departure date, YYYY-MM-DD.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&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;origin&lt;/span&gt;&lt;span class="dl"&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;destination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&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;flights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;searchFlights&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&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="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;flights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flights&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;Look at the &lt;code&gt;inputSchema&lt;/code&gt;. That block &lt;strong&gt;repeats information your form already has&lt;/strong&gt;: that &lt;code&gt;origin&lt;/code&gt; is a required string, that &lt;code&gt;date&lt;/code&gt; is a date. And because it's duplicated, it can drift: the day someone adds a &lt;code&gt;pattern&lt;/code&gt; to the origin input, the schema never hears about it. On top of that, not everything on your site sits behind a JavaScript function. Plenty of forms just &lt;code&gt;POST&lt;/code&gt; to the server and that's it. To expose them with the imperative API, you'd have to rewrite them first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 1: the same contract, written in HTML
&lt;/h2&gt;

&lt;p&gt;This is the demo's search form, in &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/index.html" rel="noopener noreferrer"&gt;&lt;code&gt;index.html&lt;/code&gt;&lt;/a&gt; (I left out the cabin and passenger fields so it reads better):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"search-form"&lt;/span&gt;
  &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"submitted.html"&lt;/span&gt;
  &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"get"&lt;/span&gt;
  &lt;span class="na"&gt;toolname=&lt;/span&gt;&lt;span class="s"&gt;"search_flights"&lt;/span&gt;
  &lt;span class="na"&gt;tooldescription=&lt;/span&gt;&lt;span class="s"&gt;"Search flights between two airports on a given date. Returns each flight's id, airline, times, number of stops and per-passenger price."&lt;/span&gt;
  &lt;span class="na"&gt;toolautosubmit&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tool-form grid grid-cols-2 gap-3 sm:grid-cols-4"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;From
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"origin"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"[A-Za-z]{3}"&lt;/span&gt; &lt;span class="na"&gt;maxlength=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"BOG"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"3-letter airport code"&lt;/span&gt;
      &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Origin airport, as a 3-letter IATA code (e.g. BOG)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;To
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"destination"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"[A-Za-z]{3}"&lt;/span&gt; &lt;span class="na"&gt;maxlength=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"MAD"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"3-letter airport code"&lt;/span&gt;
      &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Destination airport, as a 3-letter IATA code (e.g. MAD)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field col-span-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Date
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"2026-10-13"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"2027-10-12"&lt;/span&gt;
      &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Departure date"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern comes straight from the &lt;a href="https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md" rel="noopener noreferrer"&gt;declarative API explainer&lt;/a&gt;, which shows the same side-by-side with a car search. There are only a few attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;toolname&lt;/code&gt; (the equivalent of the imperative tool's &lt;code&gt;name&lt;/code&gt;), &lt;code&gt;tooldescription&lt;/code&gt; (the equivalent of &lt;code&gt;description&lt;/code&gt;), and &lt;code&gt;toolautosubmit&lt;/code&gt;, a boolean attribute we'll get to in building block 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On each control:&lt;/strong&gt; the usual &lt;code&gt;name&lt;/code&gt; becomes the property name, and &lt;code&gt;toolparamdescription&lt;/code&gt; provides its description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail you'll see wrong in a few places: &lt;strong&gt;there is no &lt;code&gt;toolparam&lt;/code&gt; attribute&lt;/strong&gt;. The property name is the control's &lt;code&gt;name&lt;/code&gt;, the same one that already travels with the form submission. The only new thing per control is the description.&lt;/p&gt;

&lt;p&gt;And notice what &lt;em&gt;isn't&lt;/em&gt; there: no &lt;code&gt;execute&lt;/code&gt;, no &lt;code&gt;registerTool()&lt;/code&gt;, no &lt;code&gt;AbortController&lt;/code&gt; to unregister. According to the explainer, when a form with these attributes is inserted, removed, or has those attributes changed, the browser creates a new declarative tool. The tool's lifecycle is the element's lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 2: from form to JSON Schema (and why it's TBD)
&lt;/h2&gt;

&lt;p&gt;For the agent to know how to fill the form, the browser has to "compile" it into an &lt;code&gt;inputSchema&lt;/code&gt;. The explainer calls this &lt;em&gt;input schema synthesis&lt;/em&gt;, and here's the part I most want to be clear about: &lt;strong&gt;the algorithm doesn't exist in the spec yet.&lt;/strong&gt; The section literally says TODO: it still needs to specify how &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, and attributes like &lt;code&gt;step&lt;/code&gt; and &lt;code&gt;min&lt;/code&gt; reduce to a JSON Schema with &lt;code&gt;anyOf&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;, &lt;code&gt;minimum&lt;/code&gt;, and &lt;code&gt;maximum&lt;/code&gt;. Chromium is implementing "a loose version" of it to trial.&lt;/p&gt;

&lt;p&gt;So the demo ships &lt;strong&gt;our own approximation&lt;/strong&gt;, in &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/schema-synthesis.js" rel="noopener noreferrer"&gt;&lt;code&gt;schema-synthesis.js&lt;/code&gt;&lt;/a&gt;, and the inspector shows it with a label that says so. This table is what I'd &lt;em&gt;expect&lt;/em&gt; to happen, not what a standard says:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the HTML&lt;/th&gt;
&lt;th&gt;Our approximation in JSON Schema&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The property goes into &lt;code&gt;required&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pattern="[A-Za-z]{3}"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"pattern": "^(?:[A-Za-z]{3})$"&lt;/code&gt; (HTML anchors patterns implicitly; JSON Schema doesn't)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;minlength&lt;/code&gt; / &lt;code&gt;maxlength&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;minLength&lt;/code&gt; / &lt;code&gt;maxLength&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type="email"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"format": "email"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;type="date"&lt;/code&gt; with &lt;code&gt;min&lt;/code&gt; / &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"format": "date"&lt;/code&gt;; the range goes into a &lt;code&gt;$comment&lt;/code&gt;, because JSON Schema has no standard keyword for date ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type="number" min="0" max="3"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"type": "integer", "minimum": 0, "maximum": 3&lt;/code&gt; (the default &lt;code&gt;step&lt;/code&gt; is 1, hence integer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type="checkbox"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"type": "boolean"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"oneOf": [{ "const": "economy", "title": "Economy" }, …]&lt;/code&gt;; the empty placeholder is dropped when the select is &lt;code&gt;required&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form-associated custom element&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"type": "string"&lt;/code&gt; with a &lt;code&gt;$comment&lt;/code&gt;: the mapping is unspecified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Building this taught me two things. First, &lt;strong&gt;your HTML validation becomes documentation for the agent&lt;/strong&gt;. That &lt;code&gt;pattern&lt;/code&gt; you added for the user now also tells the model what format the field expects. Second, some things don't translate well: a date range has no standard keyword, and a custom control is opaque. Validation still runs when the form is submitted (it's HTML, after all), but the agent can't see it ahead of time.&lt;/p&gt;

&lt;p&gt;The inspector updates through a &lt;code&gt;MutationObserver&lt;/code&gt;. Tick the &lt;code&gt;toolautosubmit&lt;/code&gt; checkbox or rename the tool, and you'll see the schema re-synthesized on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 3: &lt;code&gt;toolautosubmit&lt;/code&gt; or human review
&lt;/h2&gt;

&lt;p&gt;This is the most important design decision in the API, and it's a single boolean attribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;With &lt;code&gt;toolautosubmit&lt;/code&gt;&lt;/strong&gt;, the agent fills the form and submits it on the user's behalf. That makes sense for a search: it's read-only and has no consequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without &lt;code&gt;toolautosubmit&lt;/code&gt;&lt;/strong&gt;, once the agent is done filling, &lt;strong&gt;the browser brings the submit button into focus&lt;/strong&gt;, and the agent should tell the user to check the form and submit it manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In part 1, the "Approve / Deny" before booking was implemented by my simulated agent reading &lt;code&gt;consequentialHint&lt;/code&gt;. Here, the human in the loop comes for free: &lt;strong&gt;it's the form itself&lt;/strong&gt;, with the agent's data in plain view, and the same button as always. No JavaScript, no separate dialog.&lt;/p&gt;

&lt;p&gt;To make sure the user notices a form waiting for them, the explainer proposes two CSS pseudo-classes: &lt;code&gt;:tool-form-active&lt;/code&gt; for the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; whose tool is "running", and &lt;code&gt;:tool-submit-active&lt;/code&gt; for its submit button. A script can't make a real pseudo-class match, so the shim mirrors them as &lt;code&gt;data-*&lt;/code&gt; attributes, and &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/style.css" rel="noopener noreferrer"&gt;&lt;code&gt;style.css&lt;/code&gt;&lt;/a&gt; has both versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="nd"&gt;:tool-form-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;dashed&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* ... */&lt;/span&gt;

&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-tool-form-active&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;dashed&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&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;They're separate rules on purpose: a browser that doesn't know &lt;code&gt;:tool-form-active&lt;/code&gt; drops the whole rule, so if you put both versions in a single selector list, that browser would drop the shim's version too.&lt;/p&gt;

&lt;p&gt;The invocation can also be cut off. According to the explainer, if the form is reset or its tool declaration changes (say, its &lt;code&gt;toolname&lt;/code&gt;), the in-flight invocation is canceled and the agent is notified. In the demo you can try it with the "Clear" and "Rename to reserve_flight" buttons while the agent is waiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find, preview, commit
&lt;/h3&gt;

&lt;p&gt;After part 1, a reader asked a question worth its weight in gold: if the agent no longer has to guess which button to press, how does a site express the difference between &lt;strong&gt;finding&lt;/strong&gt; an option, &lt;strong&gt;previewing&lt;/strong&gt; the result, and &lt;strong&gt;committing&lt;/strong&gt; a change? Those are three distinct intents, and today WebMCP covers them unevenly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;th&gt;Imperative API (part 1)&lt;/th&gt;
&lt;th&gt;Declarative API (this part)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Find&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;readOnlyHint&lt;/code&gt; on the tool&lt;/td&gt;
&lt;td&gt;a search form with &lt;code&gt;toolautosubmit&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preview&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not a concept: you model it as a separate read-only tool (e.g. &lt;code&gt;quote_booking&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;the agent-filled form waiting for review (&lt;code&gt;:tool-form-active&lt;/code&gt;, focus on the submit button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;consequentialHint&lt;/code&gt;, plus your own approval UI&lt;/td&gt;
&lt;td&gt;the human presses submit (no &lt;code&gt;toolautosubmit&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The awkward part: the declarative API has &lt;strong&gt;no&lt;/strong&gt; equivalent to &lt;code&gt;readOnlyHint&lt;/code&gt; or &lt;code&gt;consequentialHint&lt;/code&gt;. The only attributes the explainer proposes are &lt;code&gt;toolname&lt;/code&gt;, &lt;code&gt;tooldescription&lt;/code&gt;, &lt;code&gt;toolautosubmit&lt;/code&gt;, and &lt;code&gt;toolparamdescription&lt;/code&gt;. The only signal that "this has consequences" is the absence of &lt;code&gt;toolautosubmit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What follows is my own reading, not something in the explainer: the web has had this distinction for decades. In HTTP, &lt;code&gt;GET&lt;/code&gt; is a &lt;em&gt;safe&lt;/em&gt; method, meant for reading, and &lt;code&gt;POST&lt;/code&gt; isn't. A well-built form already declares its intent through its &lt;code&gt;method&lt;/code&gt;: search goes over &lt;code&gt;GET&lt;/code&gt;, booking over &lt;code&gt;POST&lt;/code&gt;. Note that the demo's booking form uses &lt;code&gt;GET&lt;/code&gt; only because a static server rejects &lt;code&gt;POST&lt;/code&gt;; a real app would use &lt;code&gt;POST&lt;/code&gt;. If tool synthesis ended up using that signal, agents would inherit semantics the web already understands instead of inventing new ones. Solving this properly, with permissions and confirmations, is the topic of this series' human-in-the-loop post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 4: &lt;code&gt;agentInvoked&lt;/code&gt; and &lt;code&gt;respondWith()&lt;/code&gt;, where JavaScript comes back
&lt;/h2&gt;

&lt;p&gt;So far, zero JavaScript. But there's a gap: when the agent submits the search form, what does it get back? Without JavaScript, the form navigates to its &lt;code&gt;action&lt;/code&gt;, as always. The explainer had an idea for that case: use the first &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; on the destination page as the response. That idea is now &lt;strong&gt;under debate&lt;/strong&gt; in &lt;a href="https://github.com/webmachinelearning/webmcp/issues/135" rel="noopener noreferrer"&gt;issue #135&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The alternative, which the demo uses, is to answer from the page. The explainer adds two members to &lt;code&gt;SubmitEvent&lt;/code&gt;: &lt;code&gt;agentInvoked&lt;/code&gt;, to tell whether an agent triggered the submission, and &lt;code&gt;respondWith(promise)&lt;/code&gt;, to hand the agent a response without navigating. The rule: you must call &lt;code&gt;preventDefault()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;respondWith()&lt;/code&gt;. Here's how it looks in &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/app.js" rel="noopener noreferrer"&gt;&lt;code&gt;app.js&lt;/code&gt;&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="nx"&gt;searchForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="c1"&gt;// Enhanced path: answer in-page instead of navigating to the GET action.&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;flights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;searchFlights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchForm&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nf"&gt;renderResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultsEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bookForm&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentInvoked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The agent gets structured data back; the page never navigates.&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&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;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;flights&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 is what I liked most about the approach: &lt;strong&gt;it's the same handler for humans and agents.&lt;/strong&gt; If you fill in the search yourself, &lt;code&gt;agentInvoked&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, the results render on the page, and that's it. If the agent fills it, exactly the same thing happens, and the agent also gets &lt;code&gt;{ flights }&lt;/code&gt; as data, so it doesn't have to read the results list. The only agent-specific line is the &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One detail the explainer doesn't settle: when the agent fills &lt;code&gt;book_flight&lt;/code&gt; and you click "Confirm booking", is that submission &lt;code&gt;agentInvoked&lt;/code&gt;? The shim says yes, so the agent receives the confirmation. That's my call, not something the text says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 5: Web Components that take part
&lt;/h2&gt;

&lt;p&gt;The explainer talks about &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; and &lt;a href="https://html.spec.whatwg.org/#form-associated-element" rel="noopener noreferrer"&gt;form-associated elements&lt;/a&gt;. It doesn't mention &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, tables, or microformats. But here's an important detail: a custom element with &lt;code&gt;static formAssociated = true&lt;/code&gt; and &lt;code&gt;ElementInternals&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; a form-associated element per HTML. So a Web Component can take part. The demo has one, &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/passenger-stepper.js" rel="noopener noreferrer"&gt;&lt;code&gt;passenger-stepper.js&lt;/code&gt;&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PassengerStepper&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;formAssociated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachInternals&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It shows up in &lt;code&gt;form.elements&lt;/code&gt;, submits its value with &lt;code&gt;setFormValue()&lt;/code&gt;, validates with &lt;code&gt;setValidity()&lt;/code&gt;, and responds to reset with &lt;code&gt;formResetCallback()&lt;/code&gt;. To the shim, it's just another control.&lt;/p&gt;

&lt;p&gt;Now, what follows is &lt;strong&gt;my inference, not spec&lt;/strong&gt;: the explainer doesn't say how a custom element maps to a schema. An &lt;code&gt;&amp;lt;input type="number" min="1" max="6"&amp;gt;&lt;/code&gt; tells the agent "integer between 1 and 6". My custom element, on the other hand, is a black box: the browser has no way of knowing which values it accepts. That's why, in our approximation, it comes out as a &lt;code&gt;string&lt;/code&gt; with a &lt;code&gt;$comment&lt;/code&gt;, and the demo's agent sends &lt;code&gt;"2"&lt;/code&gt;. Validation still works on submit, but the agent loses the type and the range. If you can use a native control, the agent comes out ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building block 6: real progressive enhancement
&lt;/h2&gt;

&lt;p&gt;Since it's all HTML, the demo works without JavaScript. Both forms &lt;code&gt;GET&lt;/code&gt; &lt;code&gt;submitted.html&lt;/code&gt;, a static page that shows you what arrived. The stepper has a fallback too: in the markup, it wraps a plain &lt;code&gt;&amp;lt;input type="number" name="passengers"&amp;gt;&lt;/code&gt;, and when the custom element upgrades, it copies its contract (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, and &lt;code&gt;toolparamdescription&lt;/code&gt;) and replaces it. Without JavaScript, the element never upgrades and the form submits the plain input.&lt;/p&gt;

&lt;p&gt;To try it without touching your browser settings, the "View without the page script" link loads the page with &lt;code&gt;?nojs&lt;/code&gt;, and &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-declarative-forms/main.js" rel="noopener noreferrer"&gt;&lt;code&gt;main.js&lt;/code&gt;&lt;/a&gt; stops before doing anything:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nojs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nojs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;boot&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;It's a simulation, and the UI says so. For the real test, disable JavaScript in DevTools. That's how I verified it with Playwright: with JavaScript off, the booking form navigates to &lt;code&gt;submitted.html?flight=IB-6584&amp;amp;passenger=Ana+G%C3%B3mez&amp;amp;…&lt;/code&gt; like any form from the 2000s.&lt;/p&gt;

&lt;p&gt;That's the underlying argument: &lt;strong&gt;declarative attributes cost nobody anything.&lt;/strong&gt; A browser that doesn't understand them ignores them, a human sees the same form as always, and the page that already worked keeps working.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honest caveat: this is even earlier than part 1
&lt;/h2&gt;

&lt;p&gt;If the imperative API is a proposal, the declarative one is a step further back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only exists in an explainer.&lt;/strong&gt; It's not in the &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;spec&lt;/a&gt;, which is itself a Draft Community Group Report off the standards track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema synthesis is TBD.&lt;/strong&gt; Everything in the table is our approximation. Chromium is trialing its own loose version, and the result may differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How the response gets back to the agent is under debate&lt;/strong&gt; (&lt;a href="https://github.com/webmachinelearning/webmcp/issues/135" rel="noopener noreferrer"&gt;issue #135&lt;/a&gt;). &lt;code&gt;respondWith()&lt;/code&gt; is in the explainer, but the no-JavaScript path (&lt;code&gt;application/ld+json&lt;/code&gt; on the destination page) is still being discussed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The event names don't match.&lt;/strong&gt; The explainer talks about &lt;code&gt;toolactivated&lt;/code&gt; and &lt;code&gt;toolcanceled&lt;/code&gt;. The spec IDL, on the other hand, defines &lt;code&gt;ontoolcancel&lt;/code&gt;. It's also still open whether they fire at &lt;code&gt;Window&lt;/code&gt; or at the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. Meanwhile, the shim fires both &lt;code&gt;toolcanceled&lt;/code&gt; and &lt;code&gt;toolcancel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with &lt;code&gt;getTools()&lt;/code&gt; / &lt;code&gt;executeTool()&lt;/code&gt; is TBD.&lt;/strong&gt; The explainer says declarative tools will almost certainly be invokable through them, but the details aren't there. That's why the shim exposes its own &lt;code&gt;getTools()&lt;/code&gt; / &lt;code&gt;executeTool()&lt;/code&gt; and leaves &lt;code&gt;document.modelContext&lt;/code&gt; alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I haven't tested the native path.&lt;/strong&gt; There's no documented way to detect support for the declarative API, so the demo doesn't invent one: the declarative behavior is &lt;strong&gt;always&lt;/strong&gt; the shim. If your browser exposes the imperative &lt;code&gt;document.modelContext&lt;/code&gt;, the badge shows it, but that tells you nothing about forms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your forms were already a contract.&lt;/strong&gt; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;, and &lt;code&gt;pattern&lt;/code&gt; have been describing what your app expects for years. The declarative API doesn't ask for a new contract; it asks you to add the descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop can be an attribute.&lt;/strong&gt; Leaving out &lt;code&gt;toolautosubmit&lt;/code&gt; is enough to make the user review a consequential action, with a UI they already know. It's the cheapest form of control I've seen so far in this series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One handler for humans and agents.&lt;/strong&gt; &lt;code&gt;agentInvoked&lt;/code&gt; + &lt;code&gt;respondWith()&lt;/code&gt; doesn't make you fork your logic: you just add a structured response at the end of the path that already existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native beats custom, at least for the agent.&lt;/strong&gt; A Web Component can take part, but a native &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; gives the model types and ranges that an opaque component hides.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the next posts in the series, we'll look at what happens when state lives in an SPA's hooks and stores, how to design human-in-the-loop permissions beyond a form, and how to debug all of this with DevTools.&lt;/p&gt;




&lt;h3&gt;
  
  
  To keep exploring
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The declarative API explainer:&lt;/strong&gt; &lt;a href="https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md" rel="noopener noreferrer"&gt;&lt;code&gt;declarative-api-explainer.md&lt;/code&gt;&lt;/a&gt;, the source for everything in this post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The issue about the agent response:&lt;/strong&gt; &lt;a href="https://github.com/webmachinelearning/webmcp/issues/135" rel="noopener noreferrer"&gt;webmachinelearning/webmcp#135&lt;/a&gt;, where the way a form's result gets back to the agent is being discussed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The WebMCP spec:&lt;/strong&gt; &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;webmachinelearning.github.io/webmcp&lt;/a&gt;, the Draft Community Group Report (the imperative API).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 of the series:&lt;/strong&gt; &lt;a href="https://dev.to/blog/webmcp-fundamentals"&gt;WebMCP — Why agents shouldn't have to guess how your UI works&lt;/a&gt;, with &lt;code&gt;registerTool()&lt;/code&gt;, &lt;code&gt;getTools()&lt;/code&gt;, and &lt;code&gt;executeTool()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full demo:&lt;/strong&gt; &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/webmcp-declarative-forms" rel="noopener noreferrer"&gt;&lt;code&gt;demos/webmcp-declarative-forms&lt;/code&gt;&lt;/a&gt; — run the agent, leave it waiting on the booking form, and try all three ways to cancel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How many of your app's forms could be tools tomorrow just by adding descriptions? I'd love to hear which one you'd expose first. Let's talk on &lt;a href="https://twitter.com/khriztianmoreno" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/khriztianmoreno/" rel="noopener noreferrer"&gt;LinkedIn&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>chromeai</category>
    </item>
    <item>
      <title>How to mock an HTTP request with Jest 💻</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/how-to-mock-an-http-request-with-jest-3k3g</link>
      <guid>https://dev.to/khriztianmoreno/how-to-mock-an-http-request-with-jest-3k3g</guid>
      <description>&lt;p&gt;Today I wanted to show you how to properly write a test.&lt;/p&gt;

&lt;p&gt;But anyone can figure out how to run a simple test. And here, we're looking to help you find answers you won't find anywhere else.&lt;/p&gt;

&lt;p&gt;So I thought we'd take things one step further.&lt;/p&gt;

&lt;p&gt;Let's run a more complex test, where you'll have to mock 1 or 2 parts of the function you're testing.&lt;/p&gt;

&lt;p&gt;[In case you're new here: mock is like using a stunt double in a movie. It's a way to replace a complicated part of your code (like calling an API) with something simpler that pretends to be real, so you can test the rest of your code easily.]&lt;/p&gt;

&lt;p&gt;MY testing framework of choice is Jest, because it makes everything so much easier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Configuration:&lt;/strong&gt; One of the main advantages of Jest is its zero-configuration setup. It is designed to work out of the box with minimal configuration, making it very attractive for projects that want to implement tests quickly and efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot Testing:&lt;/strong&gt; Jest introduced the concept of Snapshot Testing, which is particularly useful for testing UI components. It takes a snapshot of a component’s rendered output and ensures that it doesn’t change unexpectedly in future tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-In Mocking and Spies:&lt;/strong&gt; Jest comes with built-in support for mock functions, modules, and timers, making it easy to test components or functions in isolation without worrying about their dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous Testing Support:&lt;/strong&gt; Jest supports asynchronous testing out of the box, which is essential for testing in modern JavaScript applications that often rely on asynchronous operations like API calls or database queries.&lt;/li&gt;
&lt;/ol&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%2Fau3m8kw0rtsrclm6s8fo.jpg" 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%2Fau3m8kw0rtsrclm6s8fo.jpg" alt=" " width="540" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;De todos modos, entremos en las pruebas:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setting up your project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new project directory and navigate to it&lt;/li&gt;
&lt;li&gt;Initialize a new npm project: &lt;code&gt;npm init -y&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Install Jest: &lt;code&gt;npm install --save-dev jest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Install axios to make HTTP requests: &lt;code&gt;npm install axios&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are the basic requirements. Nothing new or fancy here. Let's get started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Write a function with an API call
&lt;/h3&gt;

&lt;p&gt;Now, let's say you log into some kind of application. StackOverflow, for example. Most likely, at the top right you'll see information about your profile. Maybe your full name and username, for example.&lt;/p&gt;

&lt;p&gt;In order to get these, we typically have to make an API call to get them. So, let's see how we would do that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a file called &lt;code&gt;user.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;user.js&lt;/code&gt;, write a function that makes an API call. For example, using axios to retrieve user data:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&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;getUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&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="s2"&gt;`https://api.example.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;h3&gt;
  
  
  Step 3: Create the Test File
&lt;/h3&gt;

&lt;p&gt;Okay, now that we have a function that brings us the user based on the ID we requested, let's see how we can test it.&lt;/p&gt;

&lt;p&gt;Remember, we want something that works always and for all developers.&lt;/p&gt;

&lt;p&gt;Which means we don't want to depend on whether the server is running or not (since this is not what we are testing).&lt;/p&gt;

&lt;p&gt;And we don't want to depend on the users we have in the database.&lt;/p&gt;

&lt;p&gt;Because in my database, ID1 could belong to my admin user, while in your database, ID1 could belong to YOUR admin user.&lt;/p&gt;

&lt;p&gt;This means that the same function would give us different results. Which would cause the test to fail, even though the function works correctly.&lt;/p&gt;

&lt;p&gt;Read on to see how we tackled this problem using mocks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a file called &lt;code&gt;user.test.js&lt;/code&gt; in the same directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside this file, import the function you want to test:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Write your test case, mock the call, and retrieve mock data.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should fetch user data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="c1"&gt;// Mock data to be returned by the Axios request&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockUserData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mockUserData&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Call the function&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert that the Axios get method was called correctly&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/users/1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert that the function returned the correct data&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockUserData&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;h3&gt;
  
  
  Step 4: Run the test
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add a test script to your &lt;code&gt;package.json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jest"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run your tests with &lt;code&gt;npm test&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Step 5: Review the results
&lt;/h4&gt;

&lt;p&gt;Jest will display the result of your test in the terminal. The test should pass, indicating that &lt;code&gt;getUser&lt;/code&gt; is returning the mocked data as expected.&lt;/p&gt;

&lt;p&gt;Congratulations, you now have a working test with Jest and Mocking.&lt;/p&gt;

&lt;p&gt;I hope this was helpful and/or made you learn something new!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>testing</category>
      <category>node</category>
      <category>jest</category>
    </item>
    <item>
      <title>Some methods beyond console.log in Javascript</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/some-methods-beyond-consolelog-in-javascript-34kp</link>
      <guid>https://dev.to/khriztianmoreno/some-methods-beyond-consolelog-in-javascript-34kp</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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk34yrc4herbviu81crvq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk34yrc4herbviu81crvq.png" alt="Some methods beyond console.log in Javascript" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Often, during debugging, JavaScript developers tend to use the &lt;code&gt;console.log()&lt;/code&gt; method to print values. But there are some other console methods that make life much easier. Want to know what these methods are? Let's get to know them!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;console.table()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Displaying long arrays or objects is a headache using the &lt;code&gt;console.log()&lt;/code&gt; method, but with &lt;code&gt;console.table()&lt;/code&gt; we have a much more elegant way to do it.&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="c1"&gt;// Matrix&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;banana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cherry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rs 80/kg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rs 100/kg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rs 120/kg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5 ⭐&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4 ⭐&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.5 ⭐&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Maps&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastName&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mother&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Jane&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;father&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daughter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Emily&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&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%2F37w9ez8w43n3ybe3bl94.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%2F37w9ez8w43n3ybe3bl94.png" alt="console.table()" width="681" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;console.trace()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Having trouble debugging a function? Wondering how the execution flows? &lt;code&gt;console.trace()&lt;/code&gt; is your friend!&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;outerFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;innerFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;innerFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;outerFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&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%2F5422ywzh99jnppjifyqi.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%2F5422ywzh99jnppjifyqi.png" alt="console.trace()" width="681" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;console.error()&lt;/code&gt; and &lt;code&gt;console.warn()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Tired of boring logs? Spice things up with &lt;code&gt;console.error()&lt;/code&gt; and &lt;code&gt;console.warn()&lt;/code&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is an error message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a warning message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a log message&lt;/span&gt;&lt;span class="dl"&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;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%2Faqjpe6dwijmt7gvi3ra7.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%2Faqjpe6dwijmt7gvi3ra7.png" alt="console.error()" width="681" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;console.assert()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is another brilliant debugging tool! If the assertion fails, the console will print the trace.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;func&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a is not equal to -1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a is negative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&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%2Fmfqqjbfcb6592mcjar01.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%2Fmfqqjbfcb6592mcjar01.png" alt="console.assert()" width="678" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;console.time()&lt;/code&gt;, &lt;code&gt;console.timeEnd()&lt;/code&gt;, and &lt;code&gt;console.timeLog()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Need to check how long something is taking? Timer methods are there to rescue you!&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout-timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout-timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout-timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&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%2Fp23gnvotxhkr7ejvklhf.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%2Fp23gnvotxhkr7ejvklhf.png" alt="console.time()" width="680" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NOTE: setTimeouts are not executed immediately, which results in a slight deviation from the expected time.&lt;/p&gt;

&lt;p&gt;That's all folks! I hope this helps you become a better dev!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt; on &lt;a href="https://twitter.com/khriztianmoreno" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://github.com/khriztianmoreno" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Demystifying Core Web Vitals - A Developer's Guide to LCP, INP, and CLS</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/demystifying-core-web-vitals-a-developers-guide-to-lcp-inp-and-cls-2f14</link>
      <guid>https://dev.to/khriztianmoreno/demystifying-core-web-vitals-a-developers-guide-to-lcp-inp-and-cls-2f14</guid>
      <description>&lt;p&gt;Core Web Vitals are ranking signals, but most teams still optimize them like lab-only scorecards. This guide turns CWV into actionable engineering work: how to measure (field + lab), how to debug root causes in DevTools, and which fixes actually move the 75th percentile.&lt;/p&gt;

&lt;h1&gt;
  
  
  Core Web Vitals are engineering work, not a Lighthouse mini-game
&lt;/h1&gt;

&lt;p&gt;If you work in web performance, you’ve probably seen the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The team “optimizes Core Web Vitals”&lt;/strong&gt; by chasing a green Lighthouse score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The score improves&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Search Console report doesn’t&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real users still complain&lt;/strong&gt; that the site “feels janky” or “lags when I tap”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap exists because &lt;strong&gt;Core Web Vitals are field metrics&lt;/strong&gt;. They’re evaluated on real user experiences (CrUX / RUM) at the &lt;strong&gt;75th percentile&lt;/strong&gt;, segmented by device + network. Lighthouse is still useful, but it’s not the authority.&lt;/p&gt;

&lt;p&gt;There’s also a newer tension that many teams haven’t fully internalized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INP quietly replaced FID on March 12, 2024&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The switch matters because it changes what “good performance” means. FID mostly rewarded “fast to start handling the first tap”. INP rewards something stricter and closer to how users judge sites: &lt;strong&gt;consistent responsiveness across the entire session&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to build pages that &lt;em&gt;rank&lt;/em&gt; and &lt;em&gt;feel&lt;/em&gt; fast, you need to treat CWV as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instrumented production signals&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debuggable traces&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixable bottlenecks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/crux/api?hl=en" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/crux/api?hl=en&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/blog/2023/05/introducing-inp" rel="noopener noreferrer"&gt;https://developers.google.com/search/blog/2023/05/introducing-inp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Core Web Vitals (quick overview)
&lt;/h1&gt;

&lt;p&gt;One table. No fluff.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;Good (75th percentile)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;Main content load&lt;/td&gt;
&lt;td&gt;≤ 2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP&lt;/td&gt;
&lt;td&gt;Interaction responsiveness&lt;/td&gt;
&lt;td&gt;≤ 200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;Visual stability&lt;/td&gt;
&lt;td&gt;≤ 0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Field data vs lab data (why your “green Lighthouse” can still fail)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Field (CrUX / RUM)&lt;/strong&gt;: what Google ranks on and what users feel.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aggregated over real sessions.&lt;/li&gt;
&lt;li&gt;Evaluated at the &lt;strong&gt;75th percentile&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Sensitive to slow devices, poor networks, CPU contention, third-party scripts, and long sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lab (Lighthouse / DevTools)&lt;/strong&gt;: what you use to reproduce, regress-test, and debug.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Controlled environment.&lt;/li&gt;
&lt;li&gt;Great for identifying causes.&lt;/li&gt;
&lt;li&gt;Not authoritative for SEO.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you take only one rule from this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If it’s not measured in the field, it does not exist.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CrUX Dashboard: &lt;a href="https://developer.chrome.com/docs/crux/dashboard" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/crux/dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PageSpeed Insights docs: &lt;a href="https://developers.google.com/speed/docs/insights/v5/about" rel="noopener noreferrer"&gt;https://developers.google.com/speed/docs/insights/v5/about&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  LCP (Largest Contentful Paint)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What actually counts as LCP
&lt;/h2&gt;

&lt;p&gt;LCP is not “the time the page looks ready” and it’s not “when the hero image finishes”. Chrome continuously tracks &lt;strong&gt;LCP candidates&lt;/strong&gt; as the page renders, then reports the final winner.&lt;/p&gt;

&lt;p&gt;Common candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements&lt;/strong&gt; (including responsive images)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;video poster&amp;gt;&lt;/code&gt;&lt;/strong&gt; frames&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some background images&lt;/strong&gt; (when painted as an image contentful element)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large text blocks&lt;/strong&gt; (big headings, hero copy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What matters for debugging is &lt;em&gt;which element won&lt;/em&gt; and &lt;em&gt;why it was late&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In practice, LCP investigations start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Identify the LCP element&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break down its timeline&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;TTFB&lt;/li&gt;
&lt;li&gt;Resource load time (image/font)&lt;/li&gt;
&lt;li&gt;Render delay (main thread / hydration / layout)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/lcp/" rel="noopener noreferrer"&gt;https://web.dev/lcp/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DevTools LCP debugging: &lt;a href="https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure LCP (code)
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;web-vitals&lt;/code&gt; in production to capture field LCP and the element responsible.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onLCP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onLCP&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&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;If you’re doing serious work, don’t stop at &lt;code&gt;console.log&lt;/code&gt;. Send it to your RUM endpoint and segment it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device class&lt;/li&gt;
&lt;li&gt;Effective connection type&lt;/li&gt;
&lt;li&gt;Route/template&lt;/li&gt;
&lt;li&gt;A/B cohort&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common real-world LCP killers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Late-loading hero images&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hero requested after CSS/JS resolves.&lt;/li&gt;
&lt;li&gt;Image is discovered late because it’s in CSS or injected after hydration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Server TTFB&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow origin.&lt;/li&gt;
&lt;li&gt;Cold starts.&lt;/li&gt;
&lt;li&gt;Heavy SSR compute.&lt;/li&gt;
&lt;li&gt;Cache misses.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client-side rendering waterfalls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Blank shell” + fetch + render.&lt;/li&gt;
&lt;li&gt;Framework hydration blocking paint.&lt;/li&gt;
&lt;li&gt;Fonts delaying text paint (FOIT) or relayout (FOUT).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fixes that actually work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Make the hero request early (and intentionally)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/hero.webp"&lt;/span&gt; &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Preload vs prefetch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;preload&lt;/code&gt; is a strong “I need this for this navigation” signal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefetch&lt;/code&gt; is speculative and can be ignored under pressure.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetchpriority&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;high&lt;/code&gt; for the actual LCP element.&lt;/li&gt;
&lt;li&gt;Don’t blanket-apply it to everything, or you’ll just reorder contention.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Stop hydration from blocking the first meaningful render
&lt;/h3&gt;

&lt;p&gt;Frameworks differ, but the principle is the same: &lt;strong&gt;ship less JS before first paint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your stack supports it, prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSR or static rendering for the LCP region&lt;/li&gt;
&lt;li&gt;Partial hydration / islands for below-the-fold widgets&lt;/li&gt;
&lt;li&gt;Streaming SSR where it reduces blocking time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Fix TTFB before you touch images
&lt;/h3&gt;

&lt;p&gt;If your LCP breakdown shows TTFB dominates, optimizing images won’t move the needle.&lt;/p&gt;

&lt;p&gt;Typical “TTFB-first” wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDN caching for HTML&lt;/li&gt;
&lt;li&gt;Server render caching&lt;/li&gt;
&lt;li&gt;Reduce backend fan-out&lt;/li&gt;
&lt;li&gt;Avoid expensive personalization before first byte&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Framework callout (Next.js)
&lt;/h3&gt;

&lt;p&gt;The exact knob depends on version and routing (Pages vs App), but the intent is to reduce runtime JS when possible.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;unstable_runtimeJS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;Treat this as a reminder, not a copy/paste fix: &lt;strong&gt;validate what your framework version actually supports&lt;/strong&gt;, and measure the impact on hydration + interactivity.&lt;/p&gt;

&lt;h1&gt;
  
  
  INP (Interaction to Next Paint)
&lt;/h1&gt;

&lt;p&gt;This is the heart of CWV work in 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FID was misleading (and why Google replaced it)
&lt;/h2&gt;

&lt;p&gt;FID measured only one thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The delay between a user’s &lt;strong&gt;first&lt;/strong&gt; interaction and when the browser could begin processing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It did &lt;em&gt;not&lt;/em&gt; measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event handler execution time&lt;/li&gt;
&lt;li&gt;Rendering time after the handler&lt;/li&gt;
&lt;li&gt;The 20th interaction on a long-lived page&lt;/li&gt;
&lt;li&gt;Responsiveness when the app is “fully loaded” but the main thread is constantly busy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why teams could “fix FID” with small tweaks (or by shifting work later) and still ship experiences that feel laggy.&lt;/p&gt;

&lt;p&gt;INP fixes that. It tracks responsiveness across the page lifetime and is designed to align with perceived UX pain.&lt;/p&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/inp/" rel="noopener noreferrer"&gt;https://web.dev/inp/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How INP works (the mental model)
&lt;/h2&gt;

&lt;p&gt;INP is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The worst (or near-worst) interaction latency&lt;/strong&gt; observed during the page’s lifetime&lt;/li&gt;
&lt;li&gt;For each interaction, it measures from &lt;strong&gt;input&lt;/strong&gt; to the &lt;strong&gt;next paint&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So INP doesn’t just punish “slow click handlers”. It punishes any moment where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The main thread is tied up (long tasks)&lt;/li&gt;
&lt;li&gt;Rendering is expensive&lt;/li&gt;
&lt;li&gt;Updates are synchronous and block paint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure INP (code)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onINP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attribution&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;The attribution payload is your bridge from “INP is bad” to “this exact interaction is bad”. Fields vary by browser + library version, but you should expect to see (or derive):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eventType&lt;/code&gt;&lt;/strong&gt; (e.g. &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;pointerdown&lt;/code&gt;, &lt;code&gt;keydown&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;target&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;interaction target&lt;/strong&gt; (which element)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;interactionId&lt;/code&gt;&lt;/strong&gt; (to correlate related events)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Typical INP problems (what I see most often)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Long JS tasks
&lt;/h3&gt;

&lt;p&gt;If the main thread is blocked for 200ms+, you’re effectively rolling the dice on INP.&lt;/p&gt;

&lt;p&gt;Usual causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy synchronous parsing/serialization&lt;/li&gt;
&lt;li&gt;Expensive formatting&lt;/li&gt;
&lt;li&gt;Large list reconciliation&lt;/li&gt;
&lt;li&gt;“One big reducer” updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Overhydration / excessive client work
&lt;/h3&gt;

&lt;p&gt;Hydrating everything up front often creates a permanently busy main thread:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many components hydrating at once&lt;/li&gt;
&lt;li&gt;Client-side routing transitions doing expensive work&lt;/li&gt;
&lt;li&gt;Recomputing derived state on every input&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Third-party scripts
&lt;/h3&gt;

&lt;p&gt;Third-party is a frequent INP saboteur because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It schedules work unpredictably&lt;/li&gt;
&lt;li&gt;It competes for CPU during user interactions&lt;/li&gt;
&lt;li&gt;It’s often shipped without performance budgets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debugging INP: go from metric → interaction → long task
&lt;/h2&gt;

&lt;p&gt;What works consistently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with field data&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Confirm INP is actually a production issue.&lt;/li&gt;
&lt;li&gt;Identify the worst templates/routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reproduce in DevTools Performance&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Record an interaction (click, type, open menu).&lt;/li&gt;
&lt;li&gt;Look for long tasks around the interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the “why is this slow?” breakdown&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Scripting vs rendering vs style/layout.&lt;/li&gt;
&lt;li&gt;Identify the specific function/component causing the stall.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevTools Performance panel: &lt;a href="https://developer.chrome.com/docs/devtools/performance" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/devtools/performance&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Long Tasks API: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix patterns that move INP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Defer non-critical work off the interaction path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestIdleCallback&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;initAnalytics&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;Rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idle work must be cancelable.&lt;/li&gt;
&lt;li&gt;Don’t schedule “idle” tasks that are actually required for UX.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Split long tasks (don’t just “optimize” them)
&lt;/h3&gt;

&lt;p&gt;If you have 300ms of work, shaving 20ms off rarely saves INP. You need to create paint opportunities.&lt;/p&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Break work into chunks with &lt;code&gt;setTimeout(0)&lt;/code&gt; / &lt;code&gt;queueMicrotask&lt;/code&gt; strategically&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;scheduler.postTask&lt;/code&gt; where available&lt;/li&gt;
&lt;li&gt;Move computation to a Web Worker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Make UI updates interruptible (React example)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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;startTransition&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;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expensiveUpdate&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;The key idea is not “use transitions everywhere”. It’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the immediate response lightweight&lt;/li&gt;
&lt;li&gt;Let expensive work yield so the browser can paint&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4) Reduce the amount of work per interaction
&lt;/h3&gt;

&lt;p&gt;High-leverage wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Virtualize long lists&lt;/li&gt;
&lt;li&gt;Avoid synchronous &lt;code&gt;JSON.stringify&lt;/code&gt; on large objects during input&lt;/li&gt;
&lt;li&gt;Memoize derived computations (but verify it actually reduces work)&lt;/li&gt;
&lt;li&gt;Don’t re-render entire pages on keystrokes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5) Put third-party on a leash
&lt;/h3&gt;

&lt;p&gt;If you can’t remove it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load it later&lt;/li&gt;
&lt;li&gt;Gate it behind consent or after first interaction&lt;/li&gt;
&lt;li&gt;Use Partytown or worker-based approaches where applicable&lt;/li&gt;
&lt;li&gt;Apply performance budgets and enforce them&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  CLS (Cumulative Layout Shift)
&lt;/h1&gt;

&lt;p&gt;CLS is the metric most teams think they understand… until ads, fonts, and “just one dynamic banner” wreck production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CLS still fails teams
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ads and embeds&lt;/strong&gt; that resize after load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts&lt;/strong&gt; that swap and change metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic components&lt;/strong&gt; that mount above existing content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late UI injection&lt;/strong&gt; (cookie banners, promos, consent, tooltips)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure CLS (code)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onCLS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onCLS&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;For debugging, capture attribution (shift sources) when possible, and pair this with DevTools “Layout Shift Regions”.&lt;/p&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/cls/" rel="noopener noreferrer"&gt;https://web.dev/cls/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DevTools layout shift regions: &lt;a href="https://web.dev/articles/debug-layout-shifts" rel="noopener noreferrer"&gt;https://web.dev/articles/debug-layout-shifts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fixes that matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Reserve space for media
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&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;Why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser can allocate space before the resource loads.&lt;/li&gt;
&lt;li&gt;You avoid “downward pushes” that score as layout shifts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can, also include explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes on images. &lt;code&gt;aspect-ratio&lt;/code&gt; is a great complement, especially when responsive sizing makes static dimensions less obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Don’t let fonts surprise layout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt;
  &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt;
  &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/inter.woff2"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt;
  &lt;span class="na"&gt;crossorigin&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important nuance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;font-display&lt;/code&gt; alone is not enough&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;It changes swap behavior, but it doesn’t guarantee metric compatibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;metric-compatible fallbacks&lt;/strong&gt; (or &lt;code&gt;size-adjust&lt;/code&gt;) to reduce reflow when the custom font loads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Treat late UI injection like a breaking change
&lt;/h3&gt;

&lt;p&gt;Cookie banners, promos, “new feature” banners: if it mounts above content after initial paint, it’s likely a CLS hit.&lt;/p&gt;

&lt;p&gt;Patterns that avoid CLS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render the space from the start (even if content fills later)&lt;/li&gt;
&lt;li&gt;Overlay instead of pushing (carefully; don’t block content)&lt;/li&gt;
&lt;li&gt;Insert below the fold&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Tooling stack (Google-first, engineer-friendly)
&lt;/h1&gt;

&lt;p&gt;Use each tool for what it’s good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lighthouse
&lt;/h2&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regression detection in CI&lt;/li&gt;
&lt;li&gt;Controlled before/after comparisons&lt;/li&gt;
&lt;li&gt;Catching obvious LCP/CLS issues early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proving SEO impact&lt;/li&gt;
&lt;li&gt;Predicting the 75th percentile&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PageSpeed Insights (PSI)
&lt;/h2&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-page snapshot that combines &lt;strong&gt;CrUX field data&lt;/strong&gt; + lab&lt;/li&gt;
&lt;li&gt;Communicating “field vs lab” to stakeholders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chrome DevTools (Performance panel)
&lt;/h2&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root cause analysis&lt;/li&gt;
&lt;li&gt;Interaction traces for INP&lt;/li&gt;
&lt;li&gt;Identifying long tasks, layout thrash, expensive paint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CrUX Dashboard
&lt;/h2&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SEO reality&lt;/li&gt;
&lt;li&gt;Trend analysis over time&lt;/li&gt;
&lt;li&gt;Segmenting by device class and connection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Search Console CWV report (use carefully)
&lt;/h2&gt;

&lt;p&gt;It’s useful for broad monitoring, but it has sharp edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aggregation can hide the worst templates&lt;/li&gt;
&lt;li&gt;“Needs Improvement” can hide how bad the tail is&lt;/li&gt;
&lt;li&gt;It’s not a debugging tool&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  SEO impact (brief and accurate)
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Web Vitals are ranking signals&lt;/strong&gt; (part of page experience).&lt;/li&gt;
&lt;li&gt;They behave like &lt;strong&gt;tie-breakers&lt;/strong&gt;, not magic bullets.&lt;/li&gt;
&lt;li&gt;Bad CWV can cap growth because:

&lt;ul&gt;
&lt;li&gt;You lose tie-breaks on competitive queries.&lt;/li&gt;
&lt;li&gt;User behavior (bounce/back-to-SERP) gets worse.&lt;/li&gt;
&lt;li&gt;Crawl and render efficiency can suffer on heavy pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/appearance/page-experience" rel="noopener noreferrer"&gt;https://developers.google.com/search/docs/appearance/page-experience&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion: optimize how users feel, not how pages score
&lt;/h1&gt;

&lt;p&gt;Core Web Vitals stop being ambiguous the moment you treat them as engineering metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP&lt;/strong&gt; is about delivering the main content with minimal server delay and minimal render blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP&lt;/strong&gt; is about keeping the main thread available for humans, not just for boot-time metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLS&lt;/strong&gt; is about honoring layout as a contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest shift (and the reason INP matters so much) is that performance is now &lt;strong&gt;interaction-first&lt;/strong&gt;. It’s no longer enough to load quickly; you have to stay responsive.&lt;/p&gt;

&lt;p&gt;And the non-negotiable rule still stands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If it is not measured in the field, it does not exist.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimizing Core Web Vitals means optimizing how users feel, not how pages load.&lt;/p&gt;

&lt;h1&gt;
  
  
  Checklist (high leverage)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Baseline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify field reality&lt;/strong&gt;: CrUX/PSI 28-day field data for key routes/templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment&lt;/strong&gt;: mobile vs desktop; slow network vs fast; logged-in vs logged-out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a target&lt;/strong&gt;: move the 75th percentile, not the median.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  LCP
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identify the LCP element&lt;/strong&gt; (field + lab).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload the LCP resource&lt;/strong&gt; (usually hero image) and set &lt;code&gt;fetchpriority="high"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill render-blocking work&lt;/strong&gt; on the LCP path (CSS/JS/hydration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix TTFB&lt;/strong&gt; if it dominates the breakdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  INP
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record real interactions&lt;/strong&gt; in DevTools Performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find long tasks&lt;/strong&gt; around the interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split tasks&lt;/strong&gt; and create paint opportunities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defer non-critical work&lt;/strong&gt; (&lt;code&gt;requestIdleCallback&lt;/code&gt;, post-task scheduling).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce per-interaction work&lt;/strong&gt; (virtualize, memoize, avoid full rerenders).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit third-party scripts&lt;/strong&gt; with budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CLS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reserve space&lt;/strong&gt; for images, embeds, and ads (&lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt;, &lt;code&gt;aspect-ratio&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload critical fonts&lt;/strong&gt; and use metric-compatible fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate late-inserted UI&lt;/strong&gt; above existing content.&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>corewebvitals</category>
      <category>lighthouse</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A senior stands out for his thinking</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/a-senior-stands-out-for-his-thinking-16d0</link>
      <guid>https://dev.to/khriztianmoreno/a-senior-stands-out-for-his-thinking-16d0</guid>
      <description>&lt;p&gt;&lt;strong&gt;The analysis of edge cases: a key to evaluating senior developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The world of programming is constantly changing, but many companies still use outdated evaluation methods to measure the skills of senior developers. While theoretical tests are useful for measuring basic knowledge, they do not accurately reflect a senior's ability to solve complex real-world problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="/posts/edge-cases.webp" class="article-body-image-wrapper"&gt;&lt;img src="/posts/edge-cases.webp" alt="The analysis of edge cases"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are traditional technical tests not enough?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests based solely on theoretical concepts fail to capture the true value of a senior developer. A senior's experience is measured by their ability to anticipate problems, design scalable solutions, and optimize systems. A good senior developer not only masters the code but also knows how to handle exceptions, manage complexity, and think critically in uncertain scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical approach: edge case analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A key exercise to evaluate a senior developer is to present them with a realistic scenario and ask them to identify and solve the edge cases. Take, for example, the development of a page with search and pagination. In a technical interview, an experienced senior should be able to think of extreme scenarios that could break the functionality, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search with no results&lt;/strong&gt;: How would you handle a search that returns no results? How would you inform the user in a friendly manner and what impact would this have on performance?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malicious terms&lt;/strong&gt;: What if a user enters malicious code or unexpected characters in the search field? How would you protect the application from SQL injection or XSS attacks?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-existent pages&lt;/strong&gt;: If the system receives a request for a non-existent page or with incorrect parameters, how would you handle URL validation to avoid 404 errors? What alternatives would you offer the user?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excessive results&lt;/strong&gt;: Imagine the search returns millions of results. How would you optimize pagination to prevent the application from crashing or becoming slow? What techniques would you use to ensure efficient loading?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why is this approach important?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Evaluating a candidate in these scenarios not only measures their technical knowledge but also their ability to think proactively, manage exceptions, and anticipate problems. A senior developer must have the ability to design robust solutions that not only work in ideal cases but also cover those exceptional cases that may arise during the daily operation of the system.&lt;/p&gt;

&lt;p&gt;Edge case analysis is a clear way to measure a senior's mindset. It is not enough for the candidate to solve a problem in the ideal scenario. The true test is how they respond when things do not go as planned, and if they can find effective solutions to maintain the stability, security, and usability of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should a senior be evaluated?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Realistic and practical tests&lt;/strong&gt;: Beyond theoretical concepts, interviews should focus on challenges that reflect the real problems a senior developer faces. This involves evaluating how they manage edge cases and their ability to design scalable and secure solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation of architectural decision-making&lt;/strong&gt;: Seniors must be able to make informed decisions about software architecture, scalability, performance, and security. Interviews should explore these capabilities in depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation of interpersonal and leadership skills&lt;/strong&gt;: Interviews should also include dynamics that explore communication, teamwork, and leadership skills, as these are crucial aspects of a senior's role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In conclusion:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional technical tests have their place, but they are not enough to effectively evaluate a senior developer. Companies should focus on a more holistic evaluation, where edge cases and critical decisions about software architecture and design are at the center of the assessment. Only then can we ensure that the selected candidates are truly capable of bringing the expected value to a development team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you think about this?&lt;/strong&gt; Do you agree that technical interviews for seniors should focus more on edge case analysis? What other aspects do you consider important for evaluating a senior?&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Special thanks 🙏🏻 for their &lt;a href="https://x.com/fmontes/status/1863583811130343597" rel="noopener noreferrer"&gt;example&lt;/a&gt; to &lt;a href="https://x.com/fmontes" rel="noopener noreferrer"&gt;Freddy Montes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/swlh/taking-the-edge-off-of-edge-cases-7b3008d83a57" rel="noopener noreferrer"&gt;Taking the Edge off of Edge Cases&lt;/a&gt;&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt; 🚀
&lt;/h4&gt;

</description>
      <category>webdev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Using await at the top level in ES modules</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/using-await-at-the-top-level-in-es-modules-51ae</link>
      <guid>https://dev.to/khriztianmoreno/using-await-at-the-top-level-in-es-modules-51ae</guid>
      <description>&lt;h1&gt;
  
  
  Using await at the top level in ES modules
&lt;/h1&gt;

&lt;p&gt;As I was refactoring one of my React projects last week, I realized how much JavaScript has evolved since I first started coding. Remember when we had to wrap everything in callbacks? Then came Promises, followed by async/await. And now, in 2025, I find myself regularly using a feature that quietly changed the game for JavaScript developers: top-level await in ES modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evolution of Asynchronous JavaScript
&lt;/h2&gt;

&lt;p&gt;Through my journey as a developer, I've witnessed the transformation of asynchronous JavaScript. In the early days, handling async operations meant callback hell—nested functions that became increasingly difficult to read and maintain. Then Promises arrived, offering a more structured approach but still requiring &lt;code&gt;.then()&lt;/code&gt; chains.&lt;/p&gt;

&lt;p&gt;When async/await appeared, it was a revelation. Suddenly our asynchronous code could look synchronous! But there was still one limitation that frustrated me daily: we could only use &lt;code&gt;await&lt;/code&gt; inside async functions. This meant wrapping our module initialization code in unnecessary boilerplate:&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="c1"&gt;// The old way we had to do things&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchImportantData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;ES2022 changed everything by introducing top-level await. After using it extensively in my projects over the past few years, I can confidently say it's one of those features that once you start using, you never want to go back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly Is Top-Level await?
&lt;/h2&gt;

&lt;p&gt;Top-level await is exactly what it sounds like—the ability to use the &lt;code&gt;await&lt;/code&gt; keyword directly at the top level of your ES modules, outside of async functions:&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="c1"&gt;// This now works in ES modules&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchImportantData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more wrapping in async IIFEs (Immediately Invoked Function Expressions). No more unnecessary nesting. Just clean, straightforward code that expresses your intent directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases From My Projects
&lt;/h2&gt;

&lt;p&gt;Over the past year, I've found myself reaching for top-level await in several common scenarios:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Configuration Loading
&lt;/h3&gt;

&lt;p&gt;In my latest web app, I needed to fetch configuration before initializing the application. Top-level await made this beautifully simple:&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="c1"&gt;// config.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/configuration&lt;/span&gt;&lt;span class="dl"&gt;'&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;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Other modules can now import this config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Dynamic Module Loading
&lt;/h3&gt;

&lt;p&gt;I recently built a dashboard where features needed to be loaded based on user permissions. Top-level await paired perfectly with dynamic imports:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;analyticsModule&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="nx"&gt;userPermissions&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsModule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./features/analytics.js&lt;/span&gt;&lt;span class="dl"&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabledFeatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analyticsModule&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// other features...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Database Connection Initialization
&lt;/h3&gt;

&lt;p&gt;For a Node.js backend service I worked on, establishing the database connection before exporting the module was crucial:&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="c1"&gt;// db.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDatabaseConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&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;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&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;h2&gt;
  
  
  Gotchas I've Learned the Hard Way
&lt;/h2&gt;

&lt;p&gt;After using top-level await on several projects, I've encountered some pitfalls worth sharing:&lt;/p&gt;

&lt;h3&gt;
  
  
  Module Execution Is Not What You Might Expect
&lt;/h3&gt;

&lt;p&gt;I once spent hours debugging a mysterious load order issue before I understood a key behavior: when a module uses top-level await, any module that imports it will pause execution until those awaited promises resolve.&lt;/p&gt;

&lt;p&gt;In one particularly painful debugging session, I discovered that having a chain of modules with top-level await created a cascading delay effect that significantly slowed my application startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch Out for Circular Dependencies
&lt;/h3&gt;

&lt;p&gt;In a complex React application I was developing, I created an unintentional circular dependency between two modules that both used top-level await. The result? A runtime error that was much harder to debug than usual circular dependency issues.&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="c1"&gt;// Avoid patterns like this:&lt;/span&gt;
&lt;span class="c1"&gt;// moduleA.js&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;dataA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchDataA&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dataB&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./moduleB.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// moduleB.js&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;dataB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchDataB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dataA&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./moduleA.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compatibility in 2025
&lt;/h2&gt;

&lt;p&gt;The good news is that in 2025, top-level await is well-supported across the JavaScript ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All modern browsers fully support it&lt;/li&gt;
&lt;li&gt;Node.js v16+ has stable support&lt;/li&gt;
&lt;li&gt;Major frameworks like React, Vue, and Angular have embraced it&lt;/li&gt;
&lt;li&gt;Bundlers like Vite, Webpack, and Rollup handle it elegantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just remember that top-level await only works in ES modules (not CommonJS), so you'll need to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files with the &lt;code&gt;.mjs&lt;/code&gt; extension, or&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.js&lt;/code&gt; files with &lt;code&gt;"type": "module"&lt;/code&gt; in your package.json&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why You Should Embrace Top-Level await Today
&lt;/h2&gt;

&lt;p&gt;As I look back at my JavaScript code from just a few years ago, I'm amazed at how much more elegant and maintainable it's become thanks to features like top-level await. It's not just syntactic sugar—it fundamentally changes how we structure our modules.&lt;/p&gt;

&lt;p&gt;By eliminating boilerplate async wrappers, our code becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More readable&lt;/li&gt;
&lt;li&gt;More maintainable&lt;/li&gt;
&lt;li&gt;More directly expressive of our intent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? This isn't some experimental feature we need to be cautious about anymore. It's a stable part of the language that you can confidently use in production code today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you're still not using top-level await in your projects, start today! Update your Node.js version if needed, ensure you're using ES modules, and begin refactoring those async IIFEs into clean, top-level async operations.&lt;/p&gt;

&lt;p&gt;In my experience, this small change will make a noticeable difference in your code quality and developer happiness. As we move further into 2025 and beyond, I expect top-level await to become as standard in our JavaScript modules as imports themselves.&lt;/p&gt;

&lt;p&gt;Have you started using top-level await in your projects yet? I'd love to hear about your experiences in the comments below!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What is immutability?</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/what-is-immutability-42m</link>
      <guid>https://dev.to/khriztianmoreno/what-is-immutability-42m</guid>
      <description>&lt;p&gt;Immutability in React and Redux&lt;/p&gt;

&lt;p&gt;Immutability can be a confusing topic, and it appears everywhere in React, Redux, and JavaScript in general.&lt;/p&gt;

&lt;p&gt;You may have encountered errors where your &lt;strong&gt;React&lt;/strong&gt; components do not re-render, even though you know you have changed the &lt;em&gt;props&lt;/em&gt;, and someone says, "You should be making immutable state updates." Maybe you or one of your teammates regularly writes &lt;em&gt;reducers&lt;/em&gt; in &lt;strong&gt;Redux&lt;/strong&gt; that mutate the state, and we have to constantly correct them (the &lt;em&gt;reducers&lt;/em&gt;, or our teammates 😄).&lt;/p&gt;

&lt;p&gt;It's complicated. It can be very subtle, especially if you're not sure what to look for. And honestly, if you're not sure why it's important, it's hard to care.&lt;/p&gt;

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

&lt;p&gt;First of all, immutable is the opposite of mutable, and mutable means changeable, modifiable... capable of being altered.&lt;/p&gt;

&lt;p&gt;So, something that is &lt;strong&gt;im&lt;/strong&gt;mutable, then, is something that cannot be changed.&lt;/p&gt;

&lt;p&gt;Taken to the extreme, this means that instead of having traditional variables, you would constantly be creating new values and replacing the old ones. JavaScript is not that extreme, but some languages do not allow mutation (Elixir, Erlang, and ML come to mind).&lt;/p&gt;

&lt;p&gt;While JavaScript is not a purely functional language, it can sometimes pretend to be. Certain operations with arrays in JS are immutable (meaning they return a new array, rather than modifying the original). String operations are always immutable (they create a new string with the changes). And you can also write your own functions that are immutable. You just need to know a few rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  A code example with mutation
&lt;/h2&gt;

&lt;p&gt;Let's look at an example to see how mutability works. We'll start with this person object here:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiv873gqgdmqlppn0wjx2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiv873gqgdmqlppn0wjx2.png" width="656" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So let's say we write a function that gives a person special powers:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2wjmsvs89b8691dx4wd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2wjmsvs89b8691dx4wd.png" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok, everyone has the same power. No matter, invisibility is cool. Let's give some special powers to &lt;strong&gt;super Mafe.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qdtskiwbab3a67vu372.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qdtskiwbab3a67vu372.png" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This function giveAwesomePowers &lt;em&gt;mutates&lt;/em&gt; the person object that is passed into it. If you run this code, you'll see that the first time we print a person, &lt;strong&gt;Mafe&lt;/strong&gt; has no specialPower property. But then, the second time, she suddenly has the specialPower of invisibility.&lt;/p&gt;

&lt;p&gt;The object returned by giveAwesomePowers is the same object that was passed in, but it has been modified internally. Its properties have changed. It has been &lt;em&gt;mutated&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I want to say this again because it's important: the internal parts of the object have changed, but the object reference has not. It's the same object on the outside (so an equality check like person === samePerson will be true).&lt;/p&gt;

&lt;p&gt;If we want the giveAwesomePowers function not to modify the person, we'll have to make some changes.&lt;/p&gt;

&lt;p&gt;We won't go into much detail about the following code, because it will take center stage in the next post of this series.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx25evo3ksjq85bfclfjs.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx25evo3ksjq85bfclfjs.png" alt="**Object.assign()**" width="800" height="614"&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;Object.assign()&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you haven't seen Object.assign, what it does is assign properties from one object to another. Without modifying the original object, in short, Object.assign creates a copy of the original object with a new specialPower property.&lt;/p&gt;

&lt;p&gt;👋 In my next post, we will address what a &lt;strong&gt;pure function&lt;/strong&gt; is and why it is closely related to immutability, and we will focus on understanding the concept of &lt;strong&gt;side effects&lt;/strong&gt;. 🤝&lt;/p&gt;

&lt;p&gt;I hope this was helpful and/or taught you something new!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt; 🚀
&lt;/h4&gt;

</description>
      <category>javascript</category>
      <category>redux</category>
      <category>react</category>
    </item>
    <item>
      <title>Mastering Chrome DevTools for Web Performance Optimization</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/mastering-chrome-devtools-for-web-performance-optimization-45jf</link>
      <guid>https://dev.to/khriztianmoreno/mastering-chrome-devtools-for-web-performance-optimization-45jf</guid>
      <description>&lt;p&gt;Turn Chrome DevTools from a viewer into a performance debugging weapon.&lt;/p&gt;

&lt;p&gt;Most teams open DevTools too late. Or they look at the wrong panels, drowning in noise while missing the signals that actually affect user experience.&lt;/p&gt;

&lt;p&gt;If you are a senior frontend engineer or performance owner, you know that "it feels slow" isn't a bug report—it's a symptom. This guide is for those who need to diagnose that symptom, understand the root cause, and verify the fix.&lt;/p&gt;

&lt;p&gt;We are focusing on &lt;strong&gt;Chrome DevTools features that directly map to Core Web Vitals&lt;/strong&gt;. No fluff, just the workflows you need to fix Interaction to Next Paint (INP), Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Mental model: from symptom to root cause
&lt;/h2&gt;

&lt;p&gt;Before clicking anything, you need the right mental model.&lt;br&gt;
&lt;strong&gt;Metrics tell you &lt;em&gt;what&lt;/em&gt; is wrong. DevTools explains &lt;em&gt;why&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Performance isn't about magic numbers; it's about the &lt;strong&gt;Main Thread&lt;/strong&gt;.&lt;br&gt;
The browser's main thread is where JavaScript runs, HTML is parsed, and styles are calculated. It is a single-lane highway. If a heavy truck (a long task) is blocking the lane, fast cars (user clicks, animations) are stuck in traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Rule:&lt;/strong&gt; If the main thread is blocked, UX is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Performance panel: the center of truth
&lt;/h2&gt;

&lt;p&gt;The Performance panel allows you to record exactly what the browser is doing over a period of time. It records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main thread activity:&lt;/strong&gt; JS execution, parsing, GC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering pipeline:&lt;/strong&gt; Style calc, Layout, Paint, Compositing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network timing:&lt;/strong&gt; When resources are requested and received relative to execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User input handling:&lt;/strong&gt; How long the browser took to respond to a click.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recording a useful trace
&lt;/h3&gt;

&lt;p&gt;Idle traces are useless. You need &lt;strong&gt;interaction traces&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools (&lt;strong&gt;Cmd+Option+I&lt;/strong&gt; / &lt;strong&gt;Ctrl+Shift+I&lt;/strong&gt;) and go to the &lt;strong&gt;Performance&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Screenshots&lt;/strong&gt; and &lt;strong&gt;Web Vitals&lt;/strong&gt; in the capture settings. Memory is usually optional unless you suspect a leak.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Record&lt;/strong&gt; button (circle icon).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interact with the page&lt;/strong&gt; (click the button, scroll the list, open the modal).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Stop&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Reading the Performance timeline
&lt;/h2&gt;

&lt;p&gt;The resulting trace can be intimidating. Ignore 90% of it initially. Focus on these sections:&lt;br&gt;
&lt;strong&gt;FPS &amp;amp; CPU:&lt;/strong&gt; High-level health check. Solid blocks of color in CPU mean the main thread is busy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; Thin lines showing resource loading order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Main:&lt;/strong&gt; The flame chart of call stacks. This is where you spend most of your time.
&lt;strong&gt;Frames:&lt;/strong&gt; Screenshots of what the user saw at that millisecond.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Experience Track
&lt;/h3&gt;

&lt;p&gt;This is your best friend. It explicitly marks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP:&lt;/strong&gt; Where the Largest Contentful Paint occurred.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Layout Shifts:&lt;/strong&gt; Red diamonds indicating CLS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long Tasks:&lt;/strong&gt; Tasks taking &amp;gt;50ms (red triangles).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Spotting Long Tasks
&lt;/h3&gt;

&lt;p&gt;A "Long Task" is any task that keeps the main thread busy for more than &lt;strong&gt;50ms&lt;/strong&gt;.&lt;br&gt;
In the &lt;strong&gt;Main&lt;/strong&gt; section, look for gray bars with &lt;strong&gt;red triangles&lt;/strong&gt; at the top corner. These are the tasks blocking the browser from responding to user input (INP).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Debugging LCP with DevTools
&lt;/h2&gt;

&lt;p&gt;LCP measures loading performance. To fix it, you need to know &lt;em&gt;what&lt;/em&gt; the element is and &lt;em&gt;why&lt;/em&gt; it was late.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the LCP element:&lt;/strong&gt; In the &lt;strong&gt;Timings&lt;/strong&gt; or &lt;strong&gt;Experience&lt;/strong&gt; track, find the &lt;code&gt;LCP&lt;/code&gt; marker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect the element:&lt;/strong&gt; Hovering over the LCP marker often highlights the actual DOM node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze the delay:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Load Delay:&lt;/strong&gt; Was the image discovery late? (e.g., lazy-loaded hero image).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Load Duration:&lt;/strong&gt; Was the network slow or the image too large?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render Delay:&lt;/strong&gt; Was the image loaded but waiting for a main-thread task to finish before painting?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Typical LCP root causes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Late discovery:&lt;/strong&gt; The &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag is generated by JavaScript or has &lt;code&gt;loading="lazy"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render blocking:&lt;/strong&gt; Huge CSS bundles or synchronous JS in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; pausing the parser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server TTFB:&lt;/strong&gt; The backend took too long to send the initial HTML.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Bad: Lazy loading the LCP element (e.g. Hero image) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero Image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- ✅ Good: Eager loading + fetchpriority --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"eager"&lt;/span&gt; &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero Image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Reference: &lt;a href="https://web.dev/articles/optimize-lcp" rel="noopener noreferrer"&gt;Optimize Largest Contentful Paint&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Debugging INP with DevTools
&lt;/h2&gt;

&lt;p&gt;INP is the metric that kills single-page applications (SPAs). It measures the latency of user interactions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the Interactions track:&lt;/strong&gt; Look for the specific interaction (click, keypress) you recorded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand the Interaction:&lt;/strong&gt; You will see it broken down into three phases:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input Delay:&lt;/strong&gt; Time waiting for the main thread to become free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing Time:&lt;/strong&gt; Time running your event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation Delay:&lt;/strong&gt; Time waiting for the browser to paint the next frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Visually correlate with the Main Thread:&lt;/strong&gt;&lt;br&gt;
Click the interaction bar. Look directly below it in the &lt;strong&gt;Main&lt;/strong&gt; track.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you see a massive yellow block of JavaScript &lt;em&gt;under&lt;/em&gt; the interaction, your event handler is too slow (&lt;strong&gt;Processing Time&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;If you see a massive block of JS &lt;em&gt;before&lt;/em&gt; the interaction starts, the main thread was busy doing something else (&lt;strong&gt;Input Delay&lt;/strong&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common offenders:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON parsing&lt;/strong&gt; large payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React/Vue reconciliation&lt;/strong&gt; (rendering too many components).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous loops&lt;/strong&gt; or expensive calculations.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Bad: Blocking the main thread with heavy work&lt;/span&gt;
&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavyCalculation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Blocks for 200ms&lt;/span&gt;
  &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Good: Yielding to main thread&lt;/span&gt;
&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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;showSpinner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Yield to main thread so browser can paint the spinner&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="mi"&gt;0&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavyCalculation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;strong&gt;Fix workflow:&lt;/strong&gt;&lt;br&gt;
Identify the function in the flame chart → Optimize or defer it → Record again → Verify the block is smaller.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reference: &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;Interaction to Next Paint (INP)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Debugging CLS with DevTools
&lt;/h2&gt;

&lt;p&gt;Layout shifts are annoying and confusing. DevTools visualizes them clearly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Command Menu&lt;/strong&gt; (&lt;code&gt;Cmd+Shift+P&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+P&lt;/code&gt;) and type "Rendering".&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;"Layout Shift Regions"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;As you interact with the page, shifted elements will flash &lt;strong&gt;blue&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;In the Performance Trace:&lt;/strong&gt;&lt;br&gt;
Look at the &lt;strong&gt;Experience&lt;/strong&gt; track for red diamonds. Click one.&lt;br&gt;
The &lt;strong&gt;Summary&lt;/strong&gt; tab at the bottom will list exactly which nodes moved and their previous/current coordinates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common CLS patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Font swaps (FOUT/FOIT):&lt;/strong&gt; Text renders, then the web font loads, changing the size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image resize:&lt;/strong&gt; Images without &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late injected UI:&lt;/strong&gt; Banners or ads inserting themselves at the top of the content.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* ❌ Bad: No space reserved for image */&lt;/span&gt;
&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* ✅ Good: Reserve space with aspect-ratio */&lt;/span&gt;
&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&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;em&gt;Reference: &lt;a href="https://web.dev/articles/optimize-cls" rel="noopener noreferrer"&gt;Optimize Cumulative Layout Shift&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Live Metrics screen
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Live Metrics&lt;/strong&gt; view (in the Performance panel sidebar or landing page) provides real-time feedback without a full trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instant feedback:&lt;/strong&gt; See LCP and CLS values update as you resize the window or navigate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field-aligned:&lt;/strong&gt; It uses the same implementation as the Web Vitals extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing hover states and small interactions.&lt;/li&gt;
&lt;li&gt;Validating SPA route transitions.&lt;/li&gt;
&lt;li&gt;Quick sanity checks before committing code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: This is still "Lab Data" running on your machine, not real user data (CrUX).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Insights panel
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Performance Insights&lt;/strong&gt; panel is an experimental but powerful automated analysis layer. It uses the trace data to highlight risks automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout Shift Culprits:&lt;/strong&gt; It points directly to the animation or DOM update that caused a shift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render blocking requests:&lt;/strong&gt; identifies CSS/JS that delayed the First Contentful Paint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long main-thread tasks:&lt;/strong&gt; suggestions on how to break them up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Insights as a &lt;strong&gt;hint&lt;/strong&gt;, not a verdict. It points you to the right place in the flame chart, but you still need to interpret the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. CPU and Network throttling (Mandatory)
&lt;/h2&gt;

&lt;p&gt;Developing on a MacBook Pro with fiber internet is a lie. Your users are on mid-tier Android devices with spotty 4G.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU Throttling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set to &lt;strong&gt;4x slowdown&lt;/strong&gt;. This roughly simulates a mid-range Android device.
It exposes "death by a thousand cuts"—small scripts that feel instant on desktop but freeze a phone for 300ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network Throttling:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Fast 4G&lt;/strong&gt; or &lt;strong&gt;Slow 4G&lt;/strong&gt;.&lt;br&gt;
Critical for debugging LCP (image load times) and font loading behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast Wi-Fi hides bad engineering.&lt;/strong&gt; Always throttle when testing performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Putting it all together: a repeatable workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect:&lt;/strong&gt; Use PageSpeed Insights or CrUX to identify &lt;em&gt;which&lt;/em&gt; metric is failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce:&lt;/strong&gt; Open DevTools, enable Throttling (CPU 4x, Network 4G).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record:&lt;/strong&gt; Start tracing, perform the user action, stop tracing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect:&lt;/strong&gt; Find the red/yellow markers in the Experience/Main tracks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Apply the code change (defer JS, optimize images, reduce DOM depth).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify:&lt;/strong&gt; Re-record and compare the trace. Did the long task disappear? Did the LCP marker move left?&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;DevTools is not optional. Performance is observable. Every Core Web Vitals issue leaves a trace; you just need to know where to look.&lt;/p&gt;

&lt;p&gt;If you cannot explain a performance problem in DevTools, &lt;strong&gt;you do not understand it yet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/devtools/" rel="noopener noreferrer"&gt;Chrome DevTools Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/learn/performance" rel="noopener noreferrer"&gt;web.dev Performance Guides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/appearance/core-web-vitals" rel="noopener noreferrer"&gt;Google Search Central CWV Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this has been helpful and/or taught you something new!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>devtools</category>
      <category>chrome</category>
      <category>chromedevtools</category>
    </item>
    <item>
      <title>The Web Stack You Cannot Ignore in 2026</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/the-web-stack-you-cannot-ignore-in-2026-1nmh</link>
      <guid>https://dev.to/khriztianmoreno/the-web-stack-you-cannot-ignore-in-2026-1nmh</guid>
      <description>&lt;p&gt;&lt;a href="/posts/the-web-stack-you-cannot-ignore-in-2026.png" class="article-body-image-wrapper"&gt;&lt;img src="/posts/the-web-stack-you-cannot-ignore-in-2026.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After going through roadmaps, specs, Chrome Dev Summit talks, and real signals from production, my prediction is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Web development in 2026 moves toward more native capabilities, less unnecessary JavaScript, and performance you can measure in the real world.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn’t a “cool tools” list.&lt;br&gt;
These are the areas that become &lt;strong&gt;non-optional&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Performance (Core Web Vitals + Soft Navigation)
&lt;/h2&gt;

&lt;p&gt;If you only fix one thing, fix this.&lt;br&gt;
&lt;strong&gt;Performance is the priority. No debate.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it will be vital in 2026
&lt;/h3&gt;

&lt;p&gt;Google is doubling down on &lt;em&gt;real user experience&lt;/em&gt;, not synthetic benchmarks.&lt;br&gt;
Soft Navigation also changes how modern SPAs (and “MPA-like” apps) are evaluated.&lt;/p&gt;

&lt;p&gt;In 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you don’t improve &lt;strong&gt;INP&lt;/strong&gt; and &lt;strong&gt;LCP&lt;/strong&gt;, you don’t just “lose SEO” — you lose conversions.&lt;/li&gt;
&lt;li&gt;If you don’t measure soft navigations correctly, you’ll ship “faster” routes with &lt;strong&gt;fake metrics&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CLS stops being “cosmetic”.&lt;/li&gt;
&lt;li&gt;INP fully replaces the old “FID mindset”.&lt;/li&gt;
&lt;li&gt;SPA performance gets judged like an MPA.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you should master
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web-vitals&lt;/code&gt; in production&lt;/li&gt;
&lt;li&gt;Long tasks (and what creates them)&lt;/li&gt;
&lt;li&gt;Soft navigation heuristics&lt;/li&gt;
&lt;li&gt;RUM &amp;gt; Lighthouse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/vitals" rel="noopener noreferrer"&gt;Web Vitals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/web-platform/soft-navigations-experiment" rel="noopener noreferrer"&gt;Soft Navigation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/crux" rel="noopener noreferrer"&gt;CrUX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Identity: Passkeys + FedCM
&lt;/h2&gt;

&lt;p&gt;Traditional login is dying.&lt;br&gt;
It just doesn’t know it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it will be vital in 2026
&lt;/h3&gt;

&lt;p&gt;Passwords are both a technical and legal liability.&lt;br&gt;
Passkeys reduce friction &lt;em&gt;and&lt;/em&gt; fraud.&lt;br&gt;
FedCM is the browser’s real answer to identity in a world without third‑party cookies.&lt;/p&gt;

&lt;p&gt;In 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A product without passkeys will be perceived as &lt;strong&gt;outdated&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;“Classic OAuth” without FedCM will degrade (or break) flows users care about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Passwordless becomes normal.&lt;/li&gt;
&lt;li&gt;Browser-native login UI becomes the expectation.&lt;/li&gt;
&lt;li&gt;Less JS. More platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you should master
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;WebAuthn&lt;/li&gt;
&lt;li&gt;Passkeys UX patterns&lt;/li&gt;
&lt;li&gt;FedCM flows&lt;/li&gt;
&lt;li&gt;Privacy-preserving identity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/identity/fedcm/overview" rel="noopener noreferrer"&gt;FedCM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/identity/passkeys" rel="noopener noreferrer"&gt;Passkeys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/identity/webauthn" rel="noopener noreferrer"&gt;WebAuthn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Fugu / PWA APIs
&lt;/h2&gt;

&lt;p&gt;The web talks to hardware now.&lt;br&gt;
The debate is over — what’s left is execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it will be vital in 2026
&lt;/h3&gt;

&lt;p&gt;Web apps compete directly with native when the capability gap is small.&lt;br&gt;
Browsers keep shipping standards-based APIs, which means fewer dependencies and less glue code.&lt;/p&gt;

&lt;p&gt;In 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WebUSB, File System Access, and Badging stop being “rare”.&lt;/li&gt;
&lt;li&gt;PWAs feel more and more like first-class apps when the use case fits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real offline capabilities&lt;/li&gt;
&lt;li&gt;Deeper OS integration&lt;/li&gt;
&lt;li&gt;Faster UX without native wrappers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you should master
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;File System Access API&lt;/li&gt;
&lt;li&gt;Background Sync&lt;/li&gt;
&lt;li&gt;Badging API&lt;/li&gt;
&lt;li&gt;PWA install heuristics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/capabilities/" rel="noopener noreferrer"&gt;Web capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/progressive-web-apps/" rel="noopener noreferrer"&gt;Progressive Web Apps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. AI for Web Developers (Built-in AI APIs)
&lt;/h2&gt;

&lt;p&gt;AI stops being “just a SaaS”.&lt;br&gt;
It becomes part of the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it will be vital in 2026
&lt;/h3&gt;

&lt;p&gt;Lower latency.&lt;br&gt;
More privacy (because local is the new default).&lt;br&gt;
And better UX without forcing every product to build an expensive AI backend.&lt;/p&gt;

&lt;p&gt;This is not “embed ChatGPT”.&lt;br&gt;
This is &lt;strong&gt;native AI&lt;/strong&gt;, progressively enhanced.&lt;/p&gt;

&lt;p&gt;In 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-device AI becomes the default &lt;em&gt;when available&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;AI-driven UX becomes a real differentiator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Smaller, faster models running locally&lt;/li&gt;
&lt;li&gt;Fewer external calls&lt;/li&gt;
&lt;li&gt;UI patterns that adapt in context&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you should master
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;On-device inference constraints (and fallbacks)&lt;/li&gt;
&lt;li&gt;AI UX patterns (assistive, not intrusive)&lt;/li&gt;
&lt;li&gt;Privacy-first AI&lt;/li&gt;
&lt;li&gt;Progressive enhancement with AI&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/ai/" rel="noopener noreferrer"&gt;AI in Chrome&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. DevTools &amp;amp; Browser Automation
&lt;/h2&gt;

&lt;p&gt;Traditional debugging doesn’t scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it will be vital in 2026
&lt;/h3&gt;

&lt;p&gt;Apps get more complex.&lt;br&gt;
Performance issues get more subtle.&lt;br&gt;
And manual testing simply isn’t viable if you want speed &lt;em&gt;and&lt;/em&gt; quality.&lt;/p&gt;

&lt;p&gt;In 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observability from DevTools becomes a daily habit.&lt;/li&gt;
&lt;li&gt;Automation becomes part of the workflow, not a “QA phase”.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Smarter DevTools&lt;/li&gt;
&lt;li&gt;More integrated testing&lt;/li&gt;
&lt;li&gt;Debugging centered on real UX&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you should master
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Advanced Performance panel workflows&lt;/li&gt;
&lt;li&gt;Lighthouse CI&lt;/li&gt;
&lt;li&gt;Puppeteer / Playwright&lt;/li&gt;
&lt;li&gt;Tracing and deep profiling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/devtools/" rel="noopener noreferrer"&gt;Chrome DevTools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/lighthouse/" rel="noopener noreferrer"&gt;Lighthouse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My final prediction (no marketing)
&lt;/h2&gt;

&lt;p&gt;If I had to bet on only one foundation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Performance + Identity&lt;/strong&gt; will be the base.&lt;br&gt;
Everything else sits on top of that.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The web in 2026 will be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More native&lt;/li&gt;
&lt;li&gt;Faster&lt;/li&gt;
&lt;li&gt;More private&lt;/li&gt;
&lt;li&gt;Less dependent on “framework magic”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is noise.&lt;/p&gt;

&lt;p&gt;I hope this has been helpful and/or taught you something new!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile" width="148" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>identity</category>
      <category>pwa</category>
      <category>ai</category>
    </item>
    <item>
      <title>Rust is revolutionizing JavaScript development!</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/rust-is-revolutionizing-javascript-development-1541</link>
      <guid>https://dev.to/khriztianmoreno/rust-is-revolutionizing-javascript-development-1541</guid>
      <description>&lt;p&gt;&lt;a href="https://rspack.dev/" rel="noopener noreferrer"&gt;Rspack&lt;/a&gt; just released &lt;a href="https://rspack.dev/blog/announcing-1-0" rel="noopener noreferrer"&gt;version 1.0 &lt;/a&gt;, and the dream of using Rust-based build tools to speed up the JavaScript ecosystem is more alive than ever. How did we get here? Early last year, a team of developers at ByteDance was facing performance issues maintaining the company’s “many large monolithic applications.” So they did what any good developer would do: they blamed webpack. But they didn’t stop there. In March 2023, they released Rspack v0.1, a high-performance JavaScript bundler written in Rust and designed to be fully compatible with the webpack ecosystem.&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%2Fassets.rspack.dev%2Frspack%2Frspack-logo.svg" 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%2Fassets.rspack.dev%2Frspack%2Frspack-logo.svg" alt="Rspack" width="578" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fast forward to today, and Rspack now has 100k weekly downloads and has introduced key improvements that make it production-ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better performance: New features like lazy compilation and other performance optimizations make Rspack 1.0 build times over &lt;a href="https://github.com/rspack-contrib/performance-compare" rel="noopener noreferrer"&gt;20x faster&lt;/a&gt; than webpack 5.&lt;/li&gt;
&lt;li&gt;Increased webpack compatibility: Over 80% of the top 50 downloaded webpack plugins can now be used in Rspack, bringing it closer to becoming a true drop-in replacement for webpack.&lt;/li&gt;
&lt;li&gt;Reduced complexity: They created a new toolchain called Rstack that includes separate projects like Rsbuild, Rspress, and Rslib, each targeting different use cases. This reduces the complexity of setting up an all-in-one tool like Rspack (or webpack), while still maintaining flexibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bottom line: Rspack offers a pretty simple value proposition for developers: if you already use webpack, it will make it much easier for you to migrate to its bundler which is faster, easier to use, and still fully compatible with the webpack API. Time will tell if that will be enough to convince the masses to try it out.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Streaming AI without destroying your INP</title>
      <dc:creator>Khriztian Moreno</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/khriztianmoreno/streaming-ai-without-destroying-your-inp-27no</link>
      <guid>https://dev.to/khriztianmoreno/streaming-ai-without-destroying-your-inp-27no</guid>
      <description>&lt;p&gt;Your AI feature is the new INP culprit.&lt;/p&gt;

&lt;p&gt;For years, the usual suspects for a bad &lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;Interaction to Next Paint&lt;/a&gt; score were the same: bloated bundles, unthrottled scroll handlers, a chart library re-rendering on every mouse move. Now there's a new one, and it's the feature everyone is shipping right now — streaming a language model's answer straight into the DOM.&lt;/p&gt;

&lt;p&gt;This post has a full, runnable demo behind it — &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/streaming-ai-without-destroying-your-inp" rel="noopener noreferrer"&gt;&lt;code&gt;demos/streaming-ai-without-destroying-your-inp&lt;/code&gt;&lt;/a&gt;, in the repo that powers this site. It needs Chrome flags most readers won't have enabled, so there's no hosted version to click into — clone it (or just read the files on GitHub) and run it locally per the &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;. Rather than pasting isolated snippets, I'm going to walk through the actual files: a naive implementation that wrecks your INP, and an optimized one that doesn't, both wired to a live INP readout so the difference is something you can watch happen, not just take my word for.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the demo works
&lt;/h2&gt;

&lt;p&gt;Run it locally and you'll see three things: a prompt box with a strategy switch above it, an output area, and an INP log below. The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a strategy&lt;/strong&gt; — "Naive (per-chunk, no yield)" or "Optimized (sentence batching + yield)" — with the same prompt in both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit Generate.&lt;/strong&gt; The model starts streaming into the output area using whichever strategy you picked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While it's still generating, click "🔘 Click me while it's generating"&lt;/strong&gt; a few times. That button does nothing on purpose — its only job is to give you something to interact with mid-stream, the same way a user would tap "stop" or scroll while your chat UI is still talking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the INP log.&lt;/strong&gt; Every click reports its measured latency there, in real time, via &lt;code&gt;web-vitals&lt;/code&gt;' &lt;code&gt;onINP&lt;/code&gt; — no DevTools required, though you should open DevTools' Performance panel too for the full trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit Stop&lt;/strong&gt; any time to see the &lt;code&gt;AbortController&lt;/code&gt; actually cancel the in-flight generation, instead of leaving a zombie session writing to a component nobody's looking at anymore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run the naive strategy first, mash the probe button, and watch the log fill with high numbers. Then switch to optimized, same prompt, and watch the same clicks come back low. That side-by-side is the whole point of the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why streaming text breaks INP
&lt;/h2&gt;

&lt;p&gt;On-device models like Gemini Nano expose their output through the &lt;a href="https://developer.chrome.com/docs/ai/prompt-api" rel="noopener noreferrer"&gt;Prompt API&lt;/a&gt;'s &lt;code&gt;promptStreaming()&lt;/code&gt;, which returns a &lt;code&gt;ReadableStream&lt;/code&gt; of text chunks:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promptStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Explain quantum entanglement.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;Two things about that loop are easy to miss and both matter for performance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each &lt;code&gt;chunk&lt;/code&gt; is an &lt;strong&gt;incremental delta&lt;/strong&gt;, not the full text so far. You have to concatenate them yourself to reconstruct the answer.&lt;/li&gt;
&lt;li&gt;Chunks arrive fast — often several per second — and the obvious thing to do with each one is to append it to the DOM immediately, so the text "types itself" on screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second instinct is exactly what the demo's &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/naive.js" rel="noopener noreferrer"&gt;&lt;code&gt;naive.js&lt;/code&gt;&lt;/a&gt; implements:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;naiveStreamAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promptStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;delta&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&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;Every DOM write triggered by &lt;code&gt;onChunk&lt;/code&gt; is a task on the main thread: a text node mutation, a reflow, sometimes a re-render if you're piping the chunk through a framework's state. Do that dozens of times per second and you've built a wall of small-but-frequent tasks that never lets the main thread breathe. If the user clicks "stop" or types into another field while the model is still talking, that input sits in the queue behind your DOM churn — and that queueing delay is exactly what INP measures.&lt;/p&gt;

&lt;p&gt;The fix isn't "stream slower." It's controlling &lt;strong&gt;where&lt;/strong&gt; you flush to the DOM and &lt;strong&gt;how often&lt;/strong&gt; you hand control back to the browser — which is what the demo's other implementation, &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/lib.js" rel="noopener noreferrer"&gt;&lt;code&gt;lib.js&lt;/code&gt;&lt;/a&gt;, does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The building blocks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Batch by sentence, not by token
&lt;/h3&gt;

&lt;p&gt;Painting per-token feels responsive in a demo, but it's the most expensive possible cadence — and for languages like Spanish, breaking mid-word or mid-punctuation looks broken. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter" rel="noopener noreferrer"&gt;&lt;code&gt;Intl.Segmenter&lt;/code&gt;&lt;/a&gt; (Baseline since April 2024) gives you locale-aware sentence boundaries for free:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&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;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sentence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for &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;segment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segmenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// one full sentence at a time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segmenting by sentence means one DOM write per sentence instead of one per token — often an order of magnitude fewer updates — and it respects abbreviations and punctuation rules per locale, which a naive &lt;code&gt;text.split('. ')&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;Here's what that looks like on an actual run, prompting the demo with "Write two short sentences describing a sunrise over the mountains." Gemini Nano streamed back 37 deltas — mostly word fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Golden" " light" " kissed" " the" " jagged" " peaks" "," " slowly"
" chasing" " away" " the" " lingering" " shadows" " of" " night" "."
"  " "Warm" " hues" " painted" " the" " sky" "," " transforming"
" the" " mountains" " into" " majestic" " silhouettes" " bathed"
" in" " a" " new" " dawn" "." " " "\n\n\n\n"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naive strategy turns every one of those into its own DOM write — 37 tasks. Run the exact same deltas through the sentence-batching logic above and you get 5 writes, 2 of which carry all the actual content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. "Golden light kissed the jagged peaks, slowly chasing away the lingering shadows of night.  "
2. "Warm hues painted the sky, transforming the mountains into majestic silhouettes bathed in a new dawn. \n"
3. "\n"   4. "\n"   5. "\n"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same model output, same total text — 37 DOM writes down to 5, and the last three are trailing newlines the model tacked on at the end, not meaningful content. That's the entire optimization in one concrete before/after.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Give the main thread room to breathe
&lt;/h3&gt;

&lt;p&gt;Even at sentence granularity, a long answer can still queue up enough DOM writes to block input. Between updates, &lt;code&gt;lib.js&lt;/code&gt; hands control back to the browser with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield" rel="noopener noreferrer"&gt;&lt;code&gt;scheduler.yield()&lt;/code&gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&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="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="k"&gt;yield&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;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="mi"&gt;0&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;scheduler.yield()&lt;/code&gt; isn't Baseline yet, so the fallback matters — without it, this whole strategy silently stops working on browsers that don't support it. The point of yielding isn't to slow the stream down; it's to insert a checkpoint where a pending click or keystroke can be processed before you resume writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Let people cancel it
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;AbortController&lt;/code&gt; should exist from the moment generation starts, not as an afterthought — a model that keeps flushing sentences to a component the user has already navigated away from is its own kind of bug. In the demo, &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/main.js" rel="noopener noreferrer"&gt;&lt;code&gt;main.js&lt;/code&gt;&lt;/a&gt; creates the controller on every "Generate" click and wires the "Stop" button straight to &lt;code&gt;controller.abort()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;This is the full &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/lib.js" rel="noopener noreferrer"&gt;&lt;code&gt;lib.js&lt;/code&gt;&lt;/a&gt; — sentence batching, yielding, and cancellation, in one function:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&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;segmenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&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;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sentence&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promptStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;delta&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// The last segment in a partial buffer might still be growing,&lt;/span&gt;
    &lt;span class="c1"&gt;// so only flush the ones that are already complete.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;segmenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&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;complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &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;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&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="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;onSentence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&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="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onSentence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&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;One honest caveat, straight from testing this in the demo: re-segmenting the whole buffer on every chunk means the cost grows with the length of the answer, and &lt;code&gt;Intl.Segmenter&lt;/code&gt; can occasionally revise where a boundary falls once more text arrives (an abbreviation like "Dr." resolving differently once the next word shows up). For sentence-level batching this is rarely noticeable, but it's worth knowing before you assume the output is byte-identical to segmenting the final text in one pass.&lt;/p&gt;

&lt;p&gt;The on-screen INP log comes from &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/metrics.js" rel="noopener noreferrer"&gt;&lt;code&gt;metrics.js&lt;/code&gt;&lt;/a&gt;, which is short enough to paste in full — it just wires &lt;code&gt;web-vitals&lt;/code&gt;' &lt;code&gt;onINP&lt;/code&gt; straight to a &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onINP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://unpkg.com/web-vitals@5?module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;target&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;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;unknown target&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;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;ms — &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&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-log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;If &lt;code&gt;LanguageModel&lt;/code&gt; isn't defined yet in your browser when you run it, the page will tell you — the &lt;a href="https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/streaming-ai-without-destroying-your-inp/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; covers the Chrome flag you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring before and after
&lt;/h2&gt;

&lt;p&gt;Don't ship this on faith — profile it. When I ran the naive strategy in this demo and let a long answer stream while clicking the probe button, DevTools' Performance panel showed a dense row of small tasks in the main thread track, back-to-back, no gaps — and the probe clicks logged high in the INP log. Switching to the optimized strategy with the same prompt, the trace shows short tasks separated by visible gaps, and the same probe clicks come back low.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;metric.entries&lt;/code&gt; in the &lt;code&gt;onINP&lt;/code&gt; callback points you straight at the specific interaction that was slow and which task blocked it — that's your evidence, not a guess, for whether the batching actually helped. Run it yourself with your own prompts; the numbers will vary by device and answer length, but the shape of the two traces won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This same shape — small, frequent updates fighting the main thread — shows up anywhere a model streams into a live page, not just chat UIs. Chrome's built-in AI stack keeps expanding (the &lt;a href="https://developer.chrome.com/docs/ai/rewriter-api" rel="noopener noreferrer"&gt;Rewriter API&lt;/a&gt; for on-device rewriting, &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; for exposing page actions to agents, both covered at &lt;a href="https://developer.chrome.com/blog/chrome-at-io26" rel="noopener noreferrer"&gt;Chrome at I/O '26&lt;/a&gt;), and every one of them will hand you output the same way: incrementally, on your terms for when to paint it.&lt;/p&gt;

&lt;p&gt;The API gives you the tokens. Keeping INP healthy is still your job.&lt;/p&gt;

&lt;p&gt;I hope this has been helpful and/or taught you something new!&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%2F0rdbt84oh93va3vi70m5.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%2F0rdbt84oh93va3vi70m5.png" alt="Profile"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/khriztianmoreno"&gt;@khriztianmoreno&lt;/a&gt; 🚀&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>corewebvitals</category>
      <category>inp</category>
      <category>chromeai</category>
    </item>
  </channel>
</rss>
