What is INP
What is INP
Interaction to Next Paint measures the latency of all interactions throughout the page lifecycle.
How INP Works
- Records all interactions (clicks, taps, keyboard)
- Groups interactions by session
- Reports the worst-case interaction latency
- 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
- Long JavaScript tasks blocking the main thread
- Excessive DOM size slowing down event handling
- Layout thrashing from forced synchronous layouts
- 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
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 neededWrite 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 testsOptimize 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 analysisQuiz
1. What is a good INP score?
2. What is the difference between FID and INP?
3. What causes long tasks on the main thread?
4. How do web workers help with INP?
5. What is event delegation?
Flashcards
Question
What does INP measure?
Click to reveal answer
Answer
The latency of all interactions throughout the page lifecycle, reported as the worst-case latency.
Question
What is a long task?
Click to reveal answer
Answer
A JavaScript task that takes more than 50ms to complete, blocking the main thread.
Question
How do web workers improve INP?
Click to reveal answer
Answer
They run JavaScript off the main thread, preventing long tasks from blocking interactions.
Question
What is event delegation?
Click to reveal answer
Answer
Handling events on a parent element instead of individual children, reducing the number of event listeners.
Question
What is Interaction to Next Paint?
Click to reveal answer
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
- Break up long tasks
- Use web workers for heavy computation
- Implement event delegation
- Use passive event listeners
- Debounce/throttle event handlers
Tools
- PerformanceObserver for long tasks
- Chrome DevTools Performance panel
- web-vitals library