Interactions
Tracks all user interactions in real-time to help debug and improve Interaction to Next Paint (INP) (opens in a new tab). Based on the Web Vitals Chrome Extension (opens in a new tab).
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 |
User clicks → [Input Delay] → [Processing Time] → [Presentation Delay] → Paint
↑ waiting ↑ JS executing ↑ renderingTip: 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 |
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