effidevFlutter · Cloudflare edge · Cloud cost optimization
English

React/Next.js Web Vitals: Sub-Second LCP and INP (Interaction to Next Paint) Optimization

React Next.js INP and LCP Web Vitals Optimization Guide

With Google replacing FID with INP (Interaction to Next Paint) as a core Web Vital, performance measurement has evolved beyond initial page load speed. Modern web applications are judged on UI responsiveness—how quickly the page updates after a user clicks, taps, or types.

In React and Next.js applications, heavy JavaScript execution can block the main thread, resulting in poor INP scores (> 200ms).

This article covers actionable techniques for optimizing INP and driving LCP under 1 second in React 19 and Next.js 15+ using useTransition, scheduler.yield(), and priority asset streaming.

Key Takeaways

  • INP Threshold: Measures latency from user interaction until the next frame paints. Aim for under 100ms.
  • Breaking Long Tasks: Heavy JS tasks > 50ms block the main thread. Use scheduler.yield() to yield control back to the browser renderer.
  • React 19 Concurrent API: useTransition deprioritizes heavy state updates to ensure user input handlers execute immediately without UI lag.
  • Sub-Second LCP: Combine fetchpriority="high", AVIF image formats, and Edge CDN HTML streaming.

1. Understanding INP Latency Breakdown

Total INP = [Input Delay] + [Processing Time] + [Presentation Delay]

2. INP Optimization: scheduler.yield() & useTransition

2.1 Chunking Long Tasks with scheduler.yield()

export async function yieldToMain() {
  if ('scheduler' in window && 'yield' in (window as any).scheduler) {
    await (window as any).scheduler.yield();
  } else {
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
}

export async function processLargeDataset(items: Array<any>) {
  const CHUNK_SIZE = 50;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    await yieldToMain();
    renderChunk(items.slice(i, i + CHUNK_SIZE));
  }
}

2.2 React 19 useTransition for Non-Blocking Updates

import React, { useState, useTransition } from 'react';

export function SearchFilter() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setQuery(value); // Immediate input response

    startTransition(() => {
      setResults(heavyFilterAlgorithm(value)); // Deferred non-blocking filtering
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="Search..." />
      {isPending && <span>Updating...</span>}
    </div>
  );
}

3. Performance Benchmark Comparison

Metric Before Optimization After Optimization Good Threshold
INP 340 ms 48 ms $\le 200\text{ ms}$
LCP 2.8 s 0.82 s $\le 2.5\text{ s}$
Long Tasks (>50ms) 14 0 0 recommended
Total Blocking Time (TBT) 620 ms 15 ms $\le 200\text{ ms}$