Home
@jay_kalia07

Improving INP by Upgrading React from v17 to v18

...

What is INP?

INP (Interaction to Next Paint) is a user-centric performance metric that measures how long it takes for your page to respond to a user's interaction — such as a tap, click, or key press.

  1. • Each user session has multiple interactions
  2. • INP is the slowest (highest-latency) interaction in that session
  3. • The "global" INP reported by tools like PageSpeed Insights is the 75th percentile across real-user data over 28 days
Good INP: under 200ms
Poor INP: over 500ms

Why INP Matters?

Simple: Performance drives business.

  1. • Better INP → better user experience
  2. • Google uses INP as a Core Web Vital → directly affects search rankings
  3. • Better performance reduces ad costs (via Quality Score)

Every millisecond counts when it comes to user engagement, conversion, and SEO.

React 17 vs React 18: The INP Impact

Let's look at how upgrading from React 17 to React 18 can significantly improve your INP scores:

In Next.js 12 (React 17), SSR had inherent limitations that affected INP:

  1. • Load entire HTML
  2. • Start hydration process
  3. • Queue user interactions
  4. • Complete hydration
  5. • Finally process queued interactions

React 18 introduced two game-changing features:

  1. Streaming HTML - instead of the traditional way of creating the entire HTML page on the server and then sending it to the client, what streaming HTML does is send HTML progressively as it's generated. This helps with FCP.
  2. Selective Hydration - enables interactive parts of the page to hydrate independently and in priority order

Implementation Steps

To fully leverage these improvements:

  1. • Upgrade to React 18 (Next.js 13+)
  2. • Adopt the App Router
  3. • Use Server Components where possible
  4. • Implement Suspense boundaries strategically

Let's understand this with an example.

In an e-commerce product page:

  1. • The ProductDetails component (which includes critical UI like product images, title, and "Add to Cart" button) is loaded and hydrated first
  2. • Less critical components like RelatedProducts and Reviews are wrapped in Suspense boundaries
  3. • When a user lands on the page, they can immediately interact with the "Add to Cart" functionality, even if the reviews and related products are still loading
  4. • This prioritized hydration significantly improves the INP by ensuring the most important interactions are ready as soon as possible

This approach ensures that users can take critical actions (like adding items to cart) without waiting for the entire page to hydrate, resulting in better INP scores and user experience.

// app/product/[id]/page.js (Server Component) import { Suspense } from 'react' import ProductDetails from './ProductDetails' import RelatedProducts from './RelatedProducts' import Reviews from './Reviews' import RecommendationEngine from './RecommendationEngine' import ShowReviewsButton from './ShowReviewsButton' export default async function ProductPage({ params }) { const { id } = params const productPromise = fetchProduct(id) // This product data will be fetched first and sent immediately const product = await productPromise return ( <div className="product-page"> <ProductDetails product={product} /> {/* These components will stream in as their data becomes available */} <Suspense fallback={<div>Loading related products...</div>}> <RelatedProducts productId={id} /> </Suspense> <ShowReviewsButton /> <Suspense fallback={<div>Loading reviews...</div>}> <Reviews productId={id} /> </Suspense> <Suspense fallback={<div>Loading recommendations...</div>}> <RecommendationEngine productId={id} /> </Suspense> </div> ) }
// app/product/[id]/RelatedProducts.js (Server Component) export default async function RelatedProducts({ productId }) { // This data fetching won't block the initial HTML stream const relatedProducts = await fetchRelatedProducts(productId) return ( <div className="related-products"> {/* Content here */} </div> ) }
// app/product/[id]/ShowReviewsButton.js (Client Component) 'use client' import { useState } from 'react' export default function ShowReviewsButton() { const [reviewsVisible, setReviewsVisible] = useState(false) return ( <> <button onClick={()=> setReviewsVisible(true)}> Show Reviews </button> {/* This state will be controlled client-side */} <div style={{ display: reviewsVisible ? 'block' : 'none' }} id="reviews-container"></div> </> ) }
// app/product/[id]/Reviews.js (Server Component) export default async function Reviews({ productId }) { const reviews = await fetchReviews(productId) return ( <div id="reviews-content"> {/* Reviews content */} </div> ) }

The key differences:

Streaming HTML:

  1. • The ProductDetails component renders first and streams to the client immediately
  2. • RelatedProducts, Reviews, and RecommendationEngine components are wrapped in Suspense boundaries
  3. • Each Suspense boundary allows HTML for that section to stream independently as its data becomes available
  4. • Users see the product details first while other sections load progressively

Selective Hydration:

  1. • Server Components (the default) don't need client-side JavaScript
  2. • Only components marked with 'use client' (like ShowReviewsButton) get hydrated
  3. • Interactive elements are prioritized for hydration when users interact with them
  4. • If a user clicks the reviews button, that component gets hydrated with higher priority

This approach significantly improves INP and user experience because:

  1. • The page becomes interactive in a progressive manner
  2. • Less JavaScript is sent to the client overall
  3. • The main thread stays more responsive during hydration

Additional INP Optimization Techniques

Although this is one of the ways in which the INP of the page can be improved, it’s not always the solution. What if you already have this, the latest version of react which supports both selective hydration as well as streaming html. In those cases, there are some other ways which might help to improve the overall INP of the page.

  1. • Optimistic updates
  2. • Progressive loading
  3. • List virtualization for large datasets
  4. • Debounce/throttle frequent function calls
  5. • Event delegation
  6. • Prevent unnecessary re-renders

Remember: Every millisecond counts in delivering a great user experience! 🚀