Interaction
Interactions

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:

RatingDurationMeaning
🟢 Good≤ 200msFast, responsive interaction
🟡 Needs Improvement≤ 500msNoticeable delay
🔴 Poor> 500msFrustrating delay

INP Sub-Parts:

Every interaction consists of three phases:

Sub-partWhat it measuresCommon causes of delays
Input DelayTime from user input to processing startLong tasks blocking main thread
Processing TimeEvent handler executionComplex JavaScript, slow handlers
Presentation DelayRendering after processingLarge DOM updates, layout thrashing
User clicks → [Input Delay] → [Processing Time] → [Presentation Delay] → Paint
              ↑ waiting      ↑ JS executing       ↑ rendering

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:

MetricDescription
Total interactionsNumber of tracked interactions
WorstLongest interaction duration
P75 (INP)75th percentile - this is your INP score
AverageMean duration across all interactions
By ratingCount of good/needs-improvement/poor

Optimizing Each Sub-Part

Sub-partProblemSolutions
Input DelayLong tasks block main threadBreak up long tasks, yield to main thread, use scheduler.yield()
Processing TimeSlow event handlersOptimize handlers, debounce, use web workers
Presentation DelayExpensive renderingReduce DOM size, avoid layout thrashing, use content-visibility

Further Reading