Interactions
Overview
Tracks all user interactions in real-time to help debug and improve Interaction to Next Paint (INP) (opens in a new tab). INP measures responsiveness by tracking the latency of all interactions during a page visit. This snippet breaks down each interaction into three phases to identify bottlenecks. Based on the Web Vitals Chrome Extension (opens in a new tab).
Why this matters:
INP is a Core Web Vital that directly affects user experience. Slow interactions make your site feel sluggish and frustrating. By identifying which phase causes delays (input delay, processing, or presentation), you can apply targeted optimizations to make your site feel more responsive.
INP Rating Thresholds:
| Rating | Duration | Meaning |
|---|---|---|
| 🟢 Good | ≤ 200ms | Fast, responsive interaction |
| 🟡 Needs Improvement | ≤ 500ms | Noticeable delay |
| 🔴 Poor | > 500ms | Frustrating delay |
INP Sub-Parts:
Every interaction consists of three phases:
| Sub-part | What it measures | Common causes of delays |
|---|---|---|
| Input Delay | Time from user input to processing start | Long tasks blocking main thread |
| Processing Time | Event handler execution | Complex JavaScript, slow handlers |
| Presentation Delay | Rendering after processing | Large DOM updates, layout thrashing |
Tip: The sub-part with the longest duration is usually where to focus optimization efforts.
Snippet
// Interaction Tracking
// https://webperf-snippets.nucliweb.net
(() => {
const formatMs = (ms) => `${Math.round(ms)}ms`;
// INP thresholds
const valueToRating = (score) =>
score <= 200 ? "good" : score <= 500 ? "needs-improvement" : "poor";
const RATING_COLORS = {
good: "#0CCE6A",
"needs-improvement": "#FFA400",
poor: "#FF4E42",
};
const RATING_ICONS = {
good: "🟢",
"needs-improvement": "🟡",
poor: "🔴",
};
// Track all interactions for summary
const allInteractions = [];
const observer = new PerformanceObserver((list) => {
const interactions = {};
for (const entry of list
.getEntries()
.filter((entry) => entry.interactionId)) {
interactions[entry.interactionId] = interactions[entry.interactionId] || [];
interactions[entry.interactionId].push(entry);
}
for (const interaction of Object.values(interactions)) {
const entry = interaction.reduce((prev, curr) =>
prev.duration >= curr.duration ? prev : curr
);
const value = entry.duration;
const rating = valueToRating(value);
const icon = RATING_ICONS[rating];
const color = RATING_COLORS[rating];
// Store for summary
allInteractions.push({
duration: value,
rating,
target: entry.target,
type: entry.name,
});
// Calculate sub-parts
const inputDelay = entry.processingStart - entry.startTime;
const processingTime = entry.processingEnd - entry.processingStart;
const presentationDelay = Math.max(
4,
entry.startTime + entry.duration - entry.processingEnd
);
const total = inputDelay + processingTime + presentationDelay;
// Find longest sub-part
const subParts = [
{ name: "Input Delay", value: inputDelay },
{ name: "Processing Time", value: processingTime },
{ name: "Presentation Delay", value: presentationDelay },
];
const longest = subParts.reduce((a, b) => (a.value > b.value ? a : b));
console.groupCollapsed(
`%c${icon} Interaction: ${formatMs(value)} (${rating})`,
`font-weight: bold; color: ${color};`
);
// Target info
console.log("%cTarget:", "font-weight: bold;", entry.target);
console.log(` Event type: ${entry.name}`);
// Sub-parts breakdown
console.log("");
console.log("%cSub-parts breakdown:", "font-weight: bold;");
const tableData = subParts.map((part) => {
const percent = ((part.value / total) * 100).toFixed(0);
const isLongest = part.name === longest.name;
return {
"Sub-part": isLongest ? `⚠️ ${part.name}` : part.name,
Duration: formatMs(part.value),
"%": `${percent}%`,
};
});
console.table(tableData);
// Visual bar
const barWidth = 40;
const inputBar = "█".repeat(Math.round((inputDelay / total) * barWidth));
const procBar = "▓".repeat(Math.round((processingTime / total) * barWidth));
const presBar = "░".repeat(Math.round((presentationDelay / total) * barWidth));
console.log(` ${inputBar}${procBar}${presBar}`);
console.log(" █ Input ▓ Processing ░ Presentation");
// Recommendation if slow
if (rating !== "good") {
console.log("");
console.log("%c💡 Optimization hint:", "font-weight: bold; color: #3b82f6;");
if (longest.name === "Input Delay") {
console.log(" Break up long tasks blocking the main thread");
console.log(" Use requestIdleCallback or setTimeout for non-critical work");
} else if (longest.name === "Processing Time") {
console.log(" Optimize event handlers, reduce JavaScript complexity");
console.log(" Consider debouncing or using web workers");
} else {
console.log(" Reduce DOM size or complexity of updates");
console.log(" Avoid forced synchronous layouts");
}
}
console.groupEnd();
}
});
observer.observe({
type: "event",
durationThreshold: 0,
buffered: true,
});
// Summary function
window.getInteractionSummary = () => {
if (allInteractions.length === 0) {
console.log("%c📊 No interactions recorded yet.", "font-weight: bold;");
console.log(" Interact with the page (click, type, etc.) and call this again.");
return;
}
console.group("%c📊 Interaction Summary", "font-weight: bold; font-size: 14px;");
const durations = allInteractions.map((i) => i.duration);
const worst = Math.max(...durations);
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const p75 = durations.sort((a, b) => a - b)[Math.floor(durations.length * 0.75)];
const worstRating = valueToRating(worst);
const p75Rating = valueToRating(p75);
console.log("");
console.log("%cStatistics:", "font-weight: bold;");
console.log(` Total interactions: ${allInteractions.length}`);
console.log(
` Worst: %c${formatMs(worst)} (${worstRating})`,
`color: ${RATING_COLORS[worstRating]};`
);
console.log(
` P75 (INP): %c${formatMs(p75)} (${p75Rating})`,
`color: ${RATING_COLORS[p75Rating]};`
);
console.log(` Average: ${formatMs(avg)}`);
// Rating breakdown
const good = allInteractions.filter((i) => i.rating === "good").length;
const needsImprovement = allInteractions.filter(
(i) => i.rating === "needs-improvement"
).length;
const poor = allInteractions.filter((i) => i.rating === "poor").length;
console.log("");
console.log("%cBy rating:", "font-weight: bold;");
console.log(` 🟢 Good (≤200ms): ${good}`);
console.log(` 🟡 Needs Improvement (≤500ms): ${needsImprovement}`);
console.log(` 🔴 Poor (>500ms): ${poor}`);
// Slow interactions
const slowInteractions = allInteractions.filter((i) => i.rating !== "good");
if (slowInteractions.length > 0) {
console.log("");
console.log("%c⚠️ Slow interactions:", "font-weight: bold; color: #ef4444;");
slowInteractions.forEach((i, idx) => {
const icon = RATING_ICONS[i.rating];
console.log(` ${idx + 1}. ${icon} ${i.type} - ${formatMs(i.duration)}`, i.target);
});
}
console.groupEnd();
};
console.log("%c👆 Interaction Tracking Active", "font-weight: bold; font-size: 14px;");
console.log(" Interact with the page to see interaction details.");
console.log(" Call %cgetInteractionSummary()%c for a summary.", "font-family: monospace; background: #f3f4f6; padding: 2px 4px;", "");
})();
Understanding the Results
Real-time Output:
Each interaction logs:
- Duration with rating indicator (🟢/🟡/🔴)
- Target element
- Event type (click, keydown, etc.)
- Sub-parts breakdown with percentages
- Visual bar showing time distribution
- Optimization hints for slow interactions
Summary Function:
Call getInteractionSummary() in the console to see:
| Metric | Description |
|---|---|
| Total interactions | Number of tracked interactions |
| Worst | Longest interaction duration |
| P75 (INP) | 75th percentile - this is your INP score |
| Average | Mean duration across all interactions |
| By rating | Count of good/needs-improvement/poor |
Optimizing Each Sub-Part
| Sub-part | Problem | Solutions |
|---|---|---|
| Input Delay | Long tasks block main thread | Break up long tasks, yield to main thread, use scheduler.yield() |
| Processing Time | Slow event handlers | Optimize handlers, debounce, use web workers |
| Presentation Delay | Expensive rendering | Reduce DOM size, avoid layout thrashing, use content-visibility |
Optimization Decision Tree:
Further Reading
- Interaction to Next Paint (INP) (opens in a new tab) | web.dev
- Optimize INP (opens in a new tab) | web.dev
- Find slow interactions (opens in a new tab) | web.dev
- Web Vitals Extension (opens in a new tab) | Chrome Web Store