Skip to content
intermediatePhase 38 · Web Performance

Interaction to Next Paint

Minimize INP by optimizing event handlers and main thread work.

30m
0 problems
Topic Progress0%

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 />;
}

Practice Problems

0/3solved
Build Interaction to Next Paint Component

Create a reusable React component implementing Interaction to Next Paint. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Interaction to Next Paint Testing

Write unit and integration tests for Interaction to Next Paint using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Interaction to Next Paint Performance

Optimize Interaction to Next Paint for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

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?

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.

Question

What is Interaction to Next Paint?

Answer

Interaction to Next Paint is a key concept in frontend development.

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