Skip to content
intermediate Phase 10 · Web Performance

Interaction to Next Paint

Minimize INP by optimizing event handlers and main thread work.

30m
0 problems
Topic Progress 0%

What is INP

What is INP

Interaction to Next Paint measures the latency of all interactions throughout the page lifecycle.

How INP Works

  1. Records all interactions (clicks, taps, keyboard)
  2. Groups interactions by session
  3. Reports the worst-case interaction latency
  4. Excludes outliers (highest and lowest)

INP vs FID

Feature FID INP
Measures First interaction only All interactions
Scope Input delay only Full interaction lifecycle
Accuracy Less representative More representative

Measuring INP

import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value, 'ms');
  console.log('Rating:', metric.rating);
  
  // Get detailed timing
  metric.entries.forEach((entry) => {
    console.log('Interaction:', entry.name);
    console.log('Start:', entry.startTime);
    console.log('Duration:', entry.duration);
  });
});

Thresholds

Rating Threshold
Good ≤ 200ms
Needs Improvement ≤ 500ms
Poor > 500ms

Common INP Issues

  1. Long JavaScript tasks blocking the main thread
  2. Excessive DOM size slowing down event handling
  3. Layout thrashing from forced synchronous layouts
  4. Memory pressure causing garbage collection pauses

Optimizing Event Handlers

Optimizing Event Handlers

Debounce and Throttle

// Debounce: Wait for pause in events
function debounce(fn, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Throttle: Execute at most once per interval
function throttle(fn, interval) {
  let lastTime = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      return fn(...args);
    }
  };
}

// Usage
function SearchInput({ onSearch }) {
  const debouncedSearch = useMemo(
    () => debounce(onSearch, 300),
    [onSearch]
  );

  return (
    <input
      type="search"
      onChange={(e) => debouncedSearch(e.target.value)}
      placeholder="Search..."
    />
  );
}

Passive Event Listeners

// Use passive listeners for scroll and touch events
useEffect(() => {
  const handleScroll = () => {
    // Handle scroll
  };

  window.addEventListener('scroll', handleScroll, { passive: true });
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

Event Delegation

// Instead of individual handlers
function TodoList({ todos, onToggle, onDelete }) {
  const handleClick = (e) => {
    const target = e.target;
    const todoId = target.dataset.todoId;
    
    if (target.matches('[data-action="toggle"]')) {
      onToggle(todoId);
    } else if (target.matches('[data-action="delete"]')) {
      onDelete(todoId);
    }
  };

  return (
    <ul onClick={handleClick}>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            data-action="toggle"
            data-todo-id={todo.id}
          />
          <span>{todo.text}</span>
          <button data-action="delete" data-todo-id={todo.id}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

Non-Blocking Event Handlers

// Use requestIdleCallback for non-urgent work
function handleButtonClick() {
  // Urgent: Update UI immediately
  setIsLoading(true);

  // Non-urgent: Analytics can wait
  requestIdleCallback(() => {
    analytics.track('button-click');
  });
}

// Use setTimeout to break up work
function processItems(items) {
  const chunkSize = 100;
  let index = 0;

  function processChunk() {
    const end = Math.min(index + chunkSize, items.length);
    for (; index < end; index++) {
      processItem(items[index]);
    }

    if (index < items.length) {
      setTimeout(processChunk, 0);
    }
  }

  processChunk();
}

Main Thread Work

Main Thread Work

Identifying Long Tasks

// Performance observer for long tasks
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.duration > 50) {
      console.warn('Long task:', entry.duration, 'ms');
      console.log('Start:', entry.startTime);
      console.log('End:', entry.startTime + entry.duration);
    }
  });
});

observer.observe({ entryTypes: ['longtask'] });

Web Workers

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// Main thread
const worker = new Worker('/worker.js');

worker.postMessage(largeData);
worker.onmessage = (e) => {
  setResult(e.data);
};

React Concurrent Features

import { startTransition, useDeferredValue } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      <Results query={deferredQuery} />
    </div>
  );
}

function handleSearch(e) {
  const value = e.target.value;
  startTransition(() => {
    setSearchQuery(value);
  });
}

Code Splitting

// Route-based splitting
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Component-based splitting
function HeavyComponent() {
  const [loaded, setLoaded] = useState(false);
  const [Component, setComponent] = useState(null);

  const load = async () => {
    const mod = await import('./HeavyComponent');
    setComponent(() => mod.default);
    setLoaded(true);
  };

  if (!loaded) {
    return <button onClick={load}>Load Component</button>;
  }

  return <Component />;
}

Quiz

1. What is a good INP score?

Question 1 options

2. What is the difference between FID and INP?

Question 2 options

3. What causes long tasks on the main thread?

Question 3 options

4. How do web workers help with INP?

Question 4 options

5. What is event delegation (specific to inp)?

Question 5 options

Flashcards

Question

What does INP measure?

Answer

The latency of all interactions throughout the page lifecycle, reported as the worst-case latency.

Question

What is a long task?

Answer

A JavaScript task that takes more than 50ms to complete, blocking the main thread.

Question

How do web workers improve INP?

Answer

They run JavaScript off the main thread, preventing long tasks from blocking interactions.

Question

What is event delegation?

Answer

Handling events on a parent element instead of individual children, reducing the number of event listeners.

Revision Notes

Key Takeaways

  • 1. INP measures all interaction latency, not just the first
  • 2. Good INP is ≤ 200ms
  • 3. Long tasks block the main thread and hurt INP
  • 4. Web workers offload work from the main thread
  • 5. Event delegation reduces event listener overhead

Interview Tips

  • Explain the difference between FID and INP
  • Discuss techniques for reducing long tasks
  • Know how web workers improve interactivity

Cheat Sheet

INP Cheat Sheet

Threshold

  • Good: ≤ 200ms
  • Needs Improvement: ≤ 500ms
  • Poor: > 500ms

Optimization

  1. Break up long tasks
  2. Use web workers for heavy computation
  3. Implement event delegation
  4. Use passive event listeners
  5. Debounce/throttle event handlers

Tools

  • PerformanceObserver for long tasks
  • Chrome DevTools Performance panel
  • web-vitals library