[{"data":1,"prerenderedAt":1435},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F":1426},{"id":4,"title":5,"body":6,"description":1408,"extension":1409,"meta":1410,"navigation":856,"path":1421,"seo":1422,"stem":1424,"__hash__":1425},"content\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Findex.md","Profiling Event Handlers for INP",{"type":7,"value":8,"toc":1398},"minimark",[9,14,24,62,78,187,192,203,214,257,270,274,277,296,524,535,539,542,557,574,585,589,614,806,831,981,985,991,1039,1058,1062,1085,1107,1138,1153,1176,1197,1201,1204,1330,1347,1351,1383,1388,1391,1394],[10,11,13],"h1",{"id":12},"profiling-event-handlers-for-inp-attributing-slow-interactions-to-a-specific-handler","Profiling Event Handlers for INP: Attributing Slow Interactions to a Specific Handler",[15,16,17,18,23],"p",{},"This guide sits under ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement"," and turns a failing Interaction to Next Paint number into a named function, a named phase, and a fixable line of code.",[15,25,26,27,31,32,35,36,39,40,43,44,48,49,51,52,54,55,61],{},"Interaction to Next Paint fails when a single interaction's total latency crosses the ",[28,29,30],"code",{},"200ms"," \"good\" boundary, and the field metric reports the worst (or near-worst) interaction across the whole visit. That makes INP a needle-in-a-haystack problem: your median interaction can be ",[28,33,34],{},"40ms"," while one buried ",[28,37,38],{},"click"," on one route ships ",[28,41,42],{},"420ms"," and fails the page. You cannot fix it until you can answer three questions precisely — ",[45,46,47],"em",{},"which"," interaction, ",[45,50,47],{}," of the three INP phases dominates it, and ",[45,53,47],{}," function inside that phase owns the time. Profiling is the discipline that answers all three. This guide walks the full attribution loop: set up the capture environment, record a real baseline, isolate the dominant phase, and pin the cost to a specific event handler using the DevTools Performance panel, the ",[19,56,60],{"href":57,"rel":58},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPerformanceEventTiming",[59],"nofollow","Event Timing API",", and the Long Animation Frames (LoAF) API.",[15,63,64,65,69,70,73,74,77],{},"Every interaction's INP decomposes into ",[66,67,68],"strong",{},"input delay"," (action to first listener), ",[66,71,72],{},"processing duration"," (all listeners running), and ",[66,75,76],{},"presentation delay"," (listeners done to next paint). Attribution is the act of mapping a slow number onto one of those three phases and then onto the code that fills it. Get the phase wrong and you optimize the wrong layer — adding yield points to a paint-bound interaction, or rewriting a handler that was never the bottleneck.",[15,79,80],{},[81,82,89,90,89,94,89,98,89,108,89,115,89,123,89,130,89,134,89,140,89,145,89,149,89,152,89,156,89,158,89,164,89,169,89,174,89,178,89,182,89],"svg",{"xmlns":83,"viewBox":84,"width":85,"role":86,"ariaLabel":87,"style":88},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 320","100%","img","An INP interaction split into input delay, processing duration, and presentation delay, each mapped to its profiling tool","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[91,92,93],"title",{},"Attributing INP to a phase and a tool",[95,96,97],"desc",{},"The three INP phases — input delay, processing, presentation delay — with the budget and the profiling tool that attributes each one.",[99,100],"rect",{"x":101,"y":101,"width":102,"height":103,"rx":104,"fill":105,"stroke":106,"style":107},"1","758","318","10","none","currentColor","stroke-opacity:0.18",[109,110,114],"text",{"x":111,"y":112,"fill":106,"style":113},"24","40","font-size:18px;font-weight:700","INP = input delay + processing + presentation (budget 200ms)",[99,116],{"x":111,"y":117,"width":118,"height":119,"rx":120,"fill":121,"stroke":121,"style":122},"74","190","58","6","#0466c8","fill-opacity:0.14",[99,124],{"x":125,"y":117,"width":126,"height":119,"rx":120,"fill":127,"stroke":128,"style":129},"234","290","#ffc300","#b8860b","fill-opacity:0.22",[99,131],{"x":132,"y":117,"width":133,"height":119,"rx":120,"fill":121,"stroke":121,"style":122},"544","192",[109,135,139],{"x":136,"y":137,"fill":106,"style":138},"119","100","font-size:14px;font-weight:600;text-anchor:middle","Input delay",[109,141,144],{"x":136,"y":142,"fill":106,"style":143},"120","font-size:13px;text-anchor:middle","\u003C 50ms",[109,146,148],{"x":147,"y":137,"fill":106,"style":138},"379","Processing",[109,150,151],{"x":147,"y":142,"fill":106,"style":143},"\u003C 100ms",[109,153,155],{"x":154,"y":137,"fill":106,"style":138},"640","Presentation",[109,157,144],{"x":154,"y":142,"fill":106,"style":143},[159,160],"line",{"x1":111,"y1":161,"x2":162,"y2":161,"stroke":106,"style":163},"170","736","stroke-opacity:0.3",[109,165,168],{"x":111,"y":166,"fill":106,"style":167},"202","font-size:14px;font-weight:600","Tool per phase",[109,170,173],{"x":111,"y":171,"fill":106,"style":172},"232","font-size:14px","Input delay: LoAF blockingDuration + busy task before the click",[109,175,177],{"x":111,"y":176,"fill":106,"style":172},"260","Processing: Event Timing processingStart\u002FEnd + flame chart",[109,179,181],{"x":111,"y":180,"fill":106,"style":172},"288","Presentation: render\u002Fpaint gap after listeners finish",[109,183,186],{"x":111,"y":184,"fill":106,"style":185},"310","font-size:13px","Attribute the dominant phase first; ignore the other two until it is fixed.",[188,189,191],"h2",{"id":190},"_1-environment-setup-for-reproducible-interaction-capture","1. Environment Setup for Reproducible Interaction Capture",[15,193,194,195,198,199,202],{},"Profiling interactions is only useful when the capture is reproducible, so fix the variables that move INP before you record anything. Use a Chromium build at version 123 or later (the Performance panel's ",[66,196,197],{},"Interactions"," track and LoAF support are stable there), test in an incognito window to exclude extensions, and apply ",[28,200,201],{},"4x"," CPU throttling under the Performance panel's gear menu so your local run approximates a mid-tier Android device. INP is dominated by the slow tail of real hardware; an unthrottled M-series laptop will hide every regression that matters.",[15,204,205,206,213],{},"Install the ",[19,207,210],{"href":208,"rel":209},"https:\u002F\u002Fgithub.com\u002FGoogleChrome\u002Fweb-vitals",[59],[28,211,212],{},"web-vitals"," attribution build, which exposes the per-phase breakdown and the LoAF entries you will correlate against the flame chart:",[215,216,221],"pre",{"className":217,"code":218,"language":219,"meta":220,"style":220},"language-bash shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# trade-off: the attribution build is larger than the core build (it carries the\n# Event Timing + LoAF plumbing), so ship it only in a sampled RUM bundle, not to\n# 100% of traffic — load the core build for production beacons if size is tight.\nnpm install web-vitals@4\n","bash","",[28,222,223,231,237,243],{"__ignoreMap":220},[224,225,227],"span",{"class":159,"line":226},1,[224,228,230],{"class":229},"sIIH1","# trade-off: the attribution build is larger than the core build (it carries the\n",[224,232,234],{"class":159,"line":233},2,[224,235,236],{"class":229},"# Event Timing + LoAF plumbing), so ship it only in a sampled RUM bundle, not to\n",[224,238,240],{"class":159,"line":239},3,[224,241,242],{"class":229},"# 100% of traffic — load the core build for production beacons if size is tight.\n",[224,244,246,250,254],{"class":159,"line":245},4,[224,247,249],{"class":248},"seIZK","npm",[224,251,253],{"class":252},"s-_DF"," install",[224,255,256],{"class":252}," web-vitals@4\n",[15,258,259,260,263,264,266,267,269],{},"Decide up front whether you are profiling in the lab (DevTools, deterministic, reproducible) or in the field (RUM, real devices, the number that actually ships). Both are required: the lab tells you ",[45,261,262],{},"why"," an interaction is slow, the field tells you ",[45,265,47],{}," interaction to chase and whether your fix moved the p75. This is the same field-versus-lab split that governs all of ",[19,268,22],{"href":21}," — lab numbers locate the bottleneck, field p75 is the boundary that ships.",[188,271,273],{"id":272},"_2-capture-a-baseline-with-event-timing-and-the-performance-panel","2. Capture a Baseline with Event Timing and the Performance Panel",[15,275,276],{},"You cannot attribute what you have not recorded. Capture the baseline two ways — programmatically with the Event Timing API for breadth, and in the Performance panel for depth on the single worst interaction.",[15,278,279,280,283,284,287,288,291,292,295],{},"The Event Timing API surfaces every qualifying interaction with the exact timestamps that define each phase. ",[28,281,282],{},"startTime"," is when the input occurred, ",[28,285,286],{},"processingStart"," and ",[28,289,290],{},"processingEnd"," bracket your listeners, and the entry's ",[28,293,294],{},"duration"," runs to the next paint. From those four numbers the three phases fall out directly:",[215,297,301],{"className":298,"code":299,"language":300,"meta":220,"style":220},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","const observer = new PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (!entry.interactionId) continue; \u002F\u002F only real interactions, not raw events\n    const inputDelay = entry.processingStart - entry.startTime;\n    const processing = entry.processingEnd - entry.processingStart;\n    const presentation = entry.startTime + entry.duration - entry.processingEnd;\n    console.log(entry.name, { inputDelay, processing, presentation, total: entry.duration });\n  }\n});\n\u002F\u002F durationThreshold:40 captures interactions at\u002Fabove the threshold; 16 is the floor.\nobserver.observe({ type: 'event', durationThreshold: 40, buffered: true });\n\u002F\u002F trade-off: a low durationThreshold floods the callback with fast interactions and\n\u002F\u002F adds observer overhead on every event — keep it at 40+ in production and only drop\n\u002F\u002F to 16 during a focused local debugging session.\n","javascript",[28,302,303,339,364,386,405,423,447,459,465,471,477,506,512,518],{"__ignoreMap":220},[224,304,305,309,313,316,319,323,327,330,333,336],{"class":159,"line":226},[224,306,308],{"class":307},"sP5qI","const",[224,310,312],{"class":311},"sf6mN"," observer",[224,314,315],{"class":307}," =",[224,317,318],{"class":307}," new",[224,320,322],{"class":321},"ssM3C"," PerformanceObserver",[224,324,326],{"class":325},"syybb","((",[224,328,329],{"class":248},"list",[224,331,332],{"class":325},") ",[224,334,335],{"class":307},"=>",[224,337,338],{"class":325}," {\n",[224,340,341,344,347,349,352,355,358,361],{"class":159,"line":233},[224,342,343],{"class":307},"  for",[224,345,346],{"class":325}," (",[224,348,308],{"class":307},[224,350,351],{"class":311}," entry",[224,353,354],{"class":307}," of",[224,356,357],{"class":325}," list.",[224,359,360],{"class":321},"getEntries",[224,362,363],{"class":325},"()) {\n",[224,365,366,369,371,374,377,380,383],{"class":159,"line":239},[224,367,368],{"class":307},"    if",[224,370,346],{"class":325},[224,372,373],{"class":307},"!",[224,375,376],{"class":325},"entry.interactionId) ",[224,378,379],{"class":307},"continue",[224,381,382],{"class":325},"; ",[224,384,385],{"class":229},"\u002F\u002F only real interactions, not raw events\n",[224,387,388,391,394,396,399,402],{"class":159,"line":245},[224,389,390],{"class":307},"    const",[224,392,393],{"class":311}," inputDelay",[224,395,315],{"class":307},[224,397,398],{"class":325}," entry.processingStart ",[224,400,401],{"class":307},"-",[224,403,404],{"class":325}," entry.startTime;\n",[224,406,408,410,413,415,418,420],{"class":159,"line":407},5,[224,409,390],{"class":307},[224,411,412],{"class":311}," processing",[224,414,315],{"class":307},[224,416,417],{"class":325}," entry.processingEnd ",[224,419,401],{"class":307},[224,421,422],{"class":325}," entry.processingStart;\n",[224,424,426,428,431,433,436,439,442,444],{"class":159,"line":425},6,[224,427,390],{"class":307},[224,429,430],{"class":311}," presentation",[224,432,315],{"class":307},[224,434,435],{"class":325}," entry.startTime ",[224,437,438],{"class":307},"+",[224,440,441],{"class":325}," entry.duration ",[224,443,401],{"class":307},[224,445,446],{"class":325}," entry.processingEnd;\n",[224,448,450,453,456],{"class":159,"line":449},7,[224,451,452],{"class":325},"    console.",[224,454,455],{"class":321},"log",[224,457,458],{"class":325},"(entry.name, { inputDelay, processing, presentation, total: entry.duration });\n",[224,460,462],{"class":159,"line":461},8,[224,463,464],{"class":325},"  }\n",[224,466,468],{"class":159,"line":467},9,[224,469,470],{"class":325},"});\n",[224,472,474],{"class":159,"line":473},10,[224,475,476],{"class":229},"\u002F\u002F durationThreshold:40 captures interactions at\u002Fabove the threshold; 16 is the floor.\n",[224,478,480,483,486,489,492,495,497,500,503],{"class":159,"line":479},11,[224,481,482],{"class":325},"observer.",[224,484,485],{"class":321},"observe",[224,487,488],{"class":325},"({ type: ",[224,490,491],{"class":252},"'event'",[224,493,494],{"class":325},", durationThreshold: ",[224,496,112],{"class":311},[224,498,499],{"class":325},", buffered: ",[224,501,502],{"class":311},"true",[224,504,505],{"class":325}," });\n",[224,507,509],{"class":159,"line":508},12,[224,510,511],{"class":229},"\u002F\u002F trade-off: a low durationThreshold floods the callback with fast interactions and\n",[224,513,515],{"class":159,"line":514},13,[224,516,517],{"class":229},"\u002F\u002F adds observer overhead on every event — keep it at 40+ in production and only drop\n",[224,519,521],{"class":159,"line":520},14,[224,522,523],{"class":229},"\u002F\u002F to 16 during a focused local debugging session.\n",[15,525,526,527,529,530,534],{},"For the worst interaction, switch to the Performance panel. Start a recording, perform the interaction, stop, and read the ",[66,528,197],{}," track at the top of the timeline. Each interaction appears as a bar whose width is its measured latency; the widest bar is your INP candidate. Click it and the bar reveals the input-delay segment (a striped lead-in), the processing block, and the presentation gap before the next frame commits. The full mechanical workflow for hunting that bar in a long, messy recording is covered in ",[19,531,533],{"href":532},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffinding-the-slowest-interaction-in-the-performance-panel\u002F","finding the slowest interaction in the Performance panel",". Record the baseline number now — the worst interaction's total latency and the width of its dominant phase — because every later step is judged against it.",[188,536,538],{"id":537},"_3-isolate-the-dominant-phase-before-touching-any-handler","3. Isolate the Dominant Phase Before Touching Any Handler",[15,540,541],{},"With the phase breakdown from step 2, decide which of the three phases owns the latency. This is the single most important decision in the loop, because each phase has a different cure and optimizing the wrong one wastes effort.",[15,543,544,545,547,548,551,552,556],{},"If ",[66,546,68],{}," dominates, the main thread was already busy with an unrelated task when the interaction arrived — your handler had not even started yet. The fix lives in the ",[45,549,550],{},"upstream"," task, not the handler: split whatever long task was mid-flight at click time, often a hydration step, a route transition, or a third-party script. If a chat widget or analytics tag is the upstream culprit, ",[19,553,555],{"href":554},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Freducing-input-delay-from-third-party-tags\u002F","reducing input delay from third-party tags"," is the targeted playbook.",[15,558,544,559,561,562,566,567,569,570,573],{},[66,560,72],{}," dominates, your own listeners are the long task. This is the most common case in JavaScript-heavy apps, and the cure is splitting the handler with ",[19,563,565],{"href":564},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F","scheduler.yield()"," or moving the heavy compute off-thread. If ",[66,568,76],{}," dominates, listeners finished quickly but the browser could not paint in time — a large DOM, an expensive style recalc, or a forced synchronous layout. No amount of scheduling helps here; reduce the commit size with ",[28,571,572],{},"content-visibility"," and fewer layout-triggering reads.",[15,575,576,577,580,581,584],{},"A fast rule of thumb in the flame chart: if a task is running ",[45,578,579],{},"when the click arrives",", it is input delay; if your handler runs for a long stretch ",[45,582,583],{},"after"," the click, it is processing; if there is a gap between your handler finishing and the next frame, it is presentation delay.",[188,586,588],{"id":587},"_4-attribute-the-cost-to-a-specific-handler-with-loaf","4. Attribute the Cost to a Specific Handler with LoAF",[15,590,591,592,597,598,601,602,605,606,609,610,613],{},"Phase isolation tells you which layer to fix; LoAF tells you exactly which script and function fills it. A ",[19,593,596],{"href":594,"rel":595},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPerformanceLongAnimationFrameTiming",[59],"Long Animation Frame"," is any frame that takes longer than ",[28,599,600],{},"50ms"," to render, and unlike the older Long Tasks API it attributes the blocking work down to a source script, character position, and the responsible callback. This is what turns \"something is slow\" into \"this ",[28,603,604],{},"onChange"," handler in ",[28,607,608],{},"Filters.tsx"," ran for ",[28,611,612],{},"180ms",".\"",[215,615,617],{"className":298,"code":616,"language":300,"meta":220,"style":220},"const loafObserver = new PerformanceObserver((list) => {\n  for (const frame of list.getEntries()) {\n    \u002F\u002F blockingDuration is the part of the frame over the 50ms budget — the INP-relevant slice.\n    if (frame.blockingDuration === 0) continue;\n    for (const script of frame.scripts) {\n      console.log({\n        invoker: script.invoker,        \u002F\u002F e.g. \"BUTTON.onclick\" — the handler that ran\n        source: script.sourceURL,       \u002F\u002F the file\n        char: script.sourceCharPosition,\u002F\u002F the exact position in that file\n        duration: script.duration,\n        forcedReflow: script.forcedStyleAndLayoutDuration, \u002F\u002F layout thrash inside the handler\n      });\n    }\n  }\n});\nloafObserver.observe({ type: 'long-animation-frame', buffered: true });\n\u002F\u002F trade-off: LoAF reports at frame granularity, so a frame bundling several small\n\u002F\u002F callbacks attributes them together — when invoker is ambiguous, drop back to the\n\u002F\u002F Performance panel flame chart to separate them by stack.\n",[28,618,619,642,661,666,686,703,713,721,729,737,742,750,755,760,764,769,788,794,800],{"__ignoreMap":220},[224,620,621,623,626,628,630,632,634,636,638,640],{"class":159,"line":226},[224,622,308],{"class":307},[224,624,625],{"class":311}," loafObserver",[224,627,315],{"class":307},[224,629,318],{"class":307},[224,631,322],{"class":321},[224,633,326],{"class":325},[224,635,329],{"class":248},[224,637,332],{"class":325},[224,639,335],{"class":307},[224,641,338],{"class":325},[224,643,644,646,648,650,653,655,657,659],{"class":159,"line":233},[224,645,343],{"class":307},[224,647,346],{"class":325},[224,649,308],{"class":307},[224,651,652],{"class":311}," frame",[224,654,354],{"class":307},[224,656,357],{"class":325},[224,658,360],{"class":321},[224,660,363],{"class":325},[224,662,663],{"class":159,"line":239},[224,664,665],{"class":229},"    \u002F\u002F blockingDuration is the part of the frame over the 50ms budget — the INP-relevant slice.\n",[224,667,668,670,673,676,679,681,683],{"class":159,"line":245},[224,669,368],{"class":307},[224,671,672],{"class":325}," (frame.blockingDuration ",[224,674,675],{"class":307},"===",[224,677,678],{"class":311}," 0",[224,680,332],{"class":325},[224,682,379],{"class":307},[224,684,685],{"class":325},";\n",[224,687,688,691,693,695,698,700],{"class":159,"line":407},[224,689,690],{"class":307},"    for",[224,692,346],{"class":325},[224,694,308],{"class":307},[224,696,697],{"class":311}," script",[224,699,354],{"class":307},[224,701,702],{"class":325}," frame.scripts) {\n",[224,704,705,708,710],{"class":159,"line":425},[224,706,707],{"class":325},"      console.",[224,709,455],{"class":321},[224,711,712],{"class":325},"({\n",[224,714,715,718],{"class":159,"line":449},[224,716,717],{"class":325},"        invoker: script.invoker,        ",[224,719,720],{"class":229},"\u002F\u002F e.g. \"BUTTON.onclick\" — the handler that ran\n",[224,722,723,726],{"class":159,"line":461},[224,724,725],{"class":325},"        source: script.sourceURL,       ",[224,727,728],{"class":229},"\u002F\u002F the file\n",[224,730,731,734],{"class":159,"line":467},[224,732,733],{"class":325},"        char: script.sourceCharPosition,",[224,735,736],{"class":229},"\u002F\u002F the exact position in that file\n",[224,738,739],{"class":159,"line":473},[224,740,741],{"class":325},"        duration: script.duration,\n",[224,743,744,747],{"class":159,"line":479},[224,745,746],{"class":325},"        forcedReflow: script.forcedStyleAndLayoutDuration, ",[224,748,749],{"class":229},"\u002F\u002F layout thrash inside the handler\n",[224,751,752],{"class":159,"line":508},[224,753,754],{"class":325},"      });\n",[224,756,757],{"class":159,"line":514},[224,758,759],{"class":325},"    }\n",[224,761,762],{"class":159,"line":520},[224,763,464],{"class":325},[224,765,767],{"class":159,"line":766},15,[224,768,470],{"class":325},[224,770,772,775,777,779,782,784,786],{"class":159,"line":771},16,[224,773,774],{"class":325},"loafObserver.",[224,776,485],{"class":321},[224,778,488],{"class":325},[224,780,781],{"class":252},"'long-animation-frame'",[224,783,499],{"class":325},[224,785,502],{"class":311},[224,787,505],{"class":325},[224,789,791],{"class":159,"line":790},17,[224,792,793],{"class":229},"\u002F\u002F trade-off: LoAF reports at frame granularity, so a frame bundling several small\n",[224,795,797],{"class":159,"line":796},18,[224,798,799],{"class":229},"\u002F\u002F callbacks attributes them together — when invoker is ambiguous, drop back to the\n",[224,801,803],{"class":159,"line":802},19,[224,804,805],{"class":229},"\u002F\u002F Performance panel flame chart to separate them by stack.\n",[15,807,808,809,812,813,816,817,820,821,824,825,827,828,830],{},"The ",[28,810,811],{},"invoker"," field is the attribution key: it names the handler (",[28,814,815],{},"BUTTON.onclick",", ",[28,818,819],{},"INPUT.oninput",", or a framework-internal callback) that drove the long frame, and ",[28,822,823],{},"sourceCharPosition"," points at the line. Cross-reference the LoAF ",[28,826,282],{}," with the Event Timing entry's ",[28,829,282],{}," for the same interaction and you have closed the loop — a slow interaction, its dominant phase, and the named function that owns the time. Send both to your RUM endpoint so the field data carries attribution, not just a bare INP value:",[215,832,834],{"className":298,"code":833,"language":300,"meta":220,"style":220},"import { onINP } from 'web-vitals\u002Fattribution';\n\nonINP(({ value, attribution }) => {\n  navigator.sendBeacon('\u002Frum\u002Finp', JSON.stringify({\n    value,\n    target: attribution.interactionTarget,        \u002F\u002F CSS selector of the element\n    phase: {\n      input: attribution.inputDelay,\n      processing: attribution.processingDuration,\n      presentation: attribution.presentationDelay,\n    },\n    longestScript: attribution.longAnimationFrameEntries?.[0]?.scripts?.[0]?.invoker,\n  }));\n  \u002F\u002F trade-off: shipping the full LoAF script list per beacon is heavy; send only the\n  \u002F\u002F top invoker and duration, and reserve the full array for a low-sample debug cohort.\n});\n",[28,835,836,852,858,881,908,913,921,926,931,936,941,946,962,967,972,977],{"__ignoreMap":220},[224,837,838,841,844,847,850],{"class":159,"line":226},[224,839,840],{"class":307},"import",[224,842,843],{"class":325}," { onINP } ",[224,845,846],{"class":307},"from",[224,848,849],{"class":252}," 'web-vitals\u002Fattribution'",[224,851,685],{"class":325},[224,853,854],{"class":159,"line":233},[224,855,857],{"emptyLinePlaceholder":856},true,"\n",[224,859,860,863,866,869,871,874,877,879],{"class":159,"line":239},[224,861,862],{"class":321},"onINP",[224,864,865],{"class":325},"(({ ",[224,867,868],{"class":248},"value",[224,870,816],{"class":325},[224,872,873],{"class":248},"attribution",[224,875,876],{"class":325}," }) ",[224,878,335],{"class":307},[224,880,338],{"class":325},[224,882,883,886,889,892,895,897,900,903,906],{"class":159,"line":245},[224,884,885],{"class":325},"  navigator.",[224,887,888],{"class":321},"sendBeacon",[224,890,891],{"class":325},"(",[224,893,894],{"class":252},"'\u002Frum\u002Finp'",[224,896,816],{"class":325},[224,898,899],{"class":311},"JSON",[224,901,902],{"class":325},".",[224,904,905],{"class":321},"stringify",[224,907,712],{"class":325},[224,909,910],{"class":159,"line":407},[224,911,912],{"class":325},"    value,\n",[224,914,915,918],{"class":159,"line":425},[224,916,917],{"class":325},"    target: attribution.interactionTarget,        ",[224,919,920],{"class":229},"\u002F\u002F CSS selector of the element\n",[224,922,923],{"class":159,"line":449},[224,924,925],{"class":325},"    phase: {\n",[224,927,928],{"class":159,"line":461},[224,929,930],{"class":325},"      input: attribution.inputDelay,\n",[224,932,933],{"class":159,"line":467},[224,934,935],{"class":325},"      processing: attribution.processingDuration,\n",[224,937,938],{"class":159,"line":473},[224,939,940],{"class":325},"      presentation: attribution.presentationDelay,\n",[224,942,943],{"class":159,"line":479},[224,944,945],{"class":325},"    },\n",[224,947,948,951,954,957,959],{"class":159,"line":508},[224,949,950],{"class":325},"    longestScript: attribution.longAnimationFrameEntries?.[",[224,952,953],{"class":311},"0",[224,955,956],{"class":325},"]?.scripts?.[",[224,958,953],{"class":311},[224,960,961],{"class":325},"]?.invoker,\n",[224,963,964],{"class":159,"line":514},[224,965,966],{"class":325},"  }));\n",[224,968,969],{"class":159,"line":520},[224,970,971],{"class":229},"  \u002F\u002F trade-off: shipping the full LoAF script list per beacon is heavy; send only the\n",[224,973,974],{"class":159,"line":766},[224,975,976],{"class":229},"  \u002F\u002F top invoker and duration, and reserve the full array for a low-sample debug cohort.\n",[224,978,979],{"class":159,"line":771},[224,980,470],{"class":325},[188,982,984],{"id":983},"deconstructing-inp-the-three-phases-and-their-budgets","Deconstructing INP: The Three Phases and Their Budgets",[15,986,987,988,990],{},"INP for one interaction is the additive sum of three phases against the ",[28,989,30],{}," total, and each has a distinct diagnostic and budget:",[992,993,994,1007,1023],"ul",{},[995,996,997,999,1000,1002,1003,1006],"li",{},[66,998,139],{}," — from the user's action to the first listener starting. Caused by the main thread being busy when the interaction arrives. Budget well under ",[28,1001,600],{},". Attribute it with the LoAF entry ",[45,1004,1005],{},"whose frame overlaps the click"," and with the busy task visible in the flame chart before processing begins. The cure is upstream: split the blocking task or defer the script that owns it.",[995,1008,1009,1012,1013,1015,1016,1019,1020,1022],{},[66,1010,1011],{},"Processing duration"," — all listeners for the interaction, including framework state updates and re-render scheduling. Usually the largest phase in interactive apps. Budget ",[28,1014,151],{},". Attribute it with Event Timing's ",[28,1017,1018],{},"processingEnd - processingStart"," and the LoAF ",[28,1021,811],{},". The cure is splitting the handler and deferring non-urgent work to a lower priority.",[995,1024,1025,1028,1029,1031,1032,1034,1035,1038],{},[66,1026,1027],{},"Presentation delay"," — from listeners finishing to the next paint. Caused by style, layout, and paint cost on commit. Budget ",[28,1030,144],{},". Attribute it by the gap between ",[28,1033,290],{}," and the entry's end in the timeline, plus ",[28,1036,1037],{},"forcedStyleAndLayoutDuration"," from LoAF. The cure is a smaller commit, not scheduling.",[15,1040,1041,1042,1044,1045,1048,1049,1052,1053,1057],{},"The classic misattribution: a handler measures ",[28,1043,34],{}," of processing yet ships ",[28,1046,1047],{},"300ms"," INP because a ",[28,1050,1051],{},"220ms"," task was mid-flight at click time, inflating input delay. The phase breakdown sends you to the upstream task — exactly the failure mode dissected in ",[19,1054,1056],{"href":1055},"\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002Fimproving-inp-for-complex-single-page-applications\u002F","improving INP for complex single page applications",", where router transitions and hydration commonly fill the input-delay phase.",[188,1059,1061],{"id":1060},"advanced-diagnostics-and-framework-edge-cases","Advanced Diagnostics and Framework Edge Cases",[15,1063,1064,1067,1068,1070,1071,1074,1075,1078,1079,1081,1082,1084],{},[66,1065,1066],{},"LoAF frames that bundle multiple callbacks."," A single long animation frame can contain several scripts — a ",[28,1069,38],{}," handler, a ",[28,1072,1073],{},"requestAnimationFrame"," callback, and a microtask flush — and LoAF attributes them to one frame. When the ",[28,1076,1077],{},"scripts"," array has several entries with similar durations, the dominant one is not always the handler; sort by ",[28,1080,294],{}," and check ",[28,1083,1037],{}," to separate compute cost from layout thrash inside the same frame.",[15,1086,1087,1090,1091,1094,1095,1098,1099,1102,1103,1106],{},[66,1088,1089],{},"Framework re-renders hiding inside processing."," In React, Vue, and Angular the visible ",[28,1092,1093],{},"onClick"," is thin; the real cost is the state update it triggers, which runs as part of the same interaction's processing phase. The Event Timing entry attributes that to the originating event, so a ",[28,1096,1097],{},"12ms"," handler can show ",[28,1100,1101],{},"150ms"," of processing because the commit it scheduled ran synchronously. Read the flame chart under the processing block to see the framework's reconciliation work — that is where the budget actually goes, and it is why ",[19,1104,1105],{"href":564},"breaking up long tasks in React event handlers"," targets the update, not the listener.",[15,1108,1109,1112,1113,816,1116,1119,1120,1122,1123,1126,1127,1130,1131,1133,1134,1137],{},[66,1110,1111],{},"Pointer interactions reporting twice."," A tap produces ",[28,1114,1115],{},"pointerdown",[28,1117,1118],{},"pointerup",", and ",[28,1121,38],{},", and the browser groups them under one ",[28,1124,1125],{},"interactionId",". When you read raw ",[28,1128,1129],{},"event"," entries instead of grouping by ",[28,1132,1125],{},", you double-count and misattribute. Always filter on ",[28,1135,1136],{},"entry.interactionId"," before computing phases, as in the step 2 snippet.",[15,1139,1140,1143,1144,1148,1149,1152],{},[66,1141,1142],{},"Compute that should never be on the main thread."," If the processing phase is dominated by parsing, diffing, or serialization, profiling will keep pointing at the same function no matter how you slice it — the answer is to leave the thread entirely. See ",[19,1145,1147],{"href":1146},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002F","offloading work to web workers with Comlink","; yielding makes a ",[28,1150,1151],{},"400ms"," parse interruptible but never cheap, while a worker removes it from the interaction's critical path.",[15,1154,1155,1158,1159,1162,1163,1166,1167,1169,1170,287,1172,1175],{},[66,1156,1157],{},"Buffered entries you missed before the observer attached."," An interaction that happens during initial load can fire before your ",[28,1160,1161],{},"PerformanceObserver"," is registered, and without ",[28,1164,1165],{},"buffered: true"," you lose it entirely — the most damaging interactions (the first click after a slow hydration) are exactly the ones most likely to predate observer setup. Always pass ",[28,1168,1165],{}," on both the ",[28,1171,1129],{},[28,1173,1174],{},"long-animation-frame"," observers, and register them as early as possible in the document head. The buffer is bounded, so a page that fires hundreds of events before script runs can still drop the oldest entries; when you suspect that, move the observer registration ahead of every other inline script.",[15,1177,1178,1183,1184,1186,1187,1190,1191,1193,1194,1196],{},[66,1179,1180,1181,902],{},"Distinguishing input delay from a slow ",[28,1182,1073],{}," A common misread is to see a long frame straddling the interaction and assume the handler is slow, when in fact a heavy ",[28,1185,1073],{}," callback — an animation library, a canvas redraw — was running at click time and inflated input delay. LoAF attributes the ",[28,1188,1189],{},"rAF"," callback explicitly in its ",[28,1192,1077],{}," array with its own ",[28,1195,811],{},", so check whether the dominant script is your handler or an animation callback before deciding where to fix. The cure for the latter is to throttle or pause the animation while interactions are likely, not to touch the handler at all.",[188,1198,1200],{"id":1199},"validation-and-budgeting-in-ci","Validation and Budgeting in CI",[15,1202,1203],{},"Attribution is proven by the long-frame profile and the field INP after a fix ships, so assert both. In Lighthouse CI, gate the lab proxy for main-thread blocking so a processing regression cannot merge:",[215,1205,1207],{"className":298,"code":1206,"language":300,"meta":220,"style":220},"\u002F\u002F lighthouserc.js\nmodule.exports = {\n  ci: {\n    assert: {\n      assertions: {\n        'total-blocking-time': ['error', { maxNumericValue: 200 }],\n        'mainthread-work-breakdown': ['warn', { maxNumericValue: 2000 }],\n        'interactive': ['warn', { maxNumericValue: 3500 }],\n      },\n    },\n  },\n};\n\u002F\u002F trade-off: TBT is a load-time proxy and never sees post-load interactions — a green\n\u002F\u002F TBT with red field INP means the slow interaction is a route change or modal, so\n\u002F\u002F pair this gate with a scripted interaction assertion and RUM.\n",[28,1208,1209,1214,1228,1233,1238,1243,1263,1280,1296,1301,1305,1310,1315,1320,1325],{"__ignoreMap":220},[224,1210,1211],{"class":159,"line":226},[224,1212,1213],{"class":229},"\u002F\u002F lighthouserc.js\n",[224,1215,1216,1219,1221,1224,1226],{"class":159,"line":233},[224,1217,1218],{"class":311},"module",[224,1220,902],{"class":325},[224,1222,1223],{"class":311},"exports",[224,1225,315],{"class":307},[224,1227,338],{"class":325},[224,1229,1230],{"class":159,"line":239},[224,1231,1232],{"class":325},"  ci: {\n",[224,1234,1235],{"class":159,"line":245},[224,1236,1237],{"class":325},"    assert: {\n",[224,1239,1240],{"class":159,"line":407},[224,1241,1242],{"class":325},"      assertions: {\n",[224,1244,1245,1248,1251,1254,1257,1260],{"class":159,"line":425},[224,1246,1247],{"class":252},"        'total-blocking-time'",[224,1249,1250],{"class":325},": [",[224,1252,1253],{"class":252},"'error'",[224,1255,1256],{"class":325},", { maxNumericValue: ",[224,1258,1259],{"class":311},"200",[224,1261,1262],{"class":325}," }],\n",[224,1264,1265,1268,1270,1273,1275,1278],{"class":159,"line":449},[224,1266,1267],{"class":252},"        'mainthread-work-breakdown'",[224,1269,1250],{"class":325},[224,1271,1272],{"class":252},"'warn'",[224,1274,1256],{"class":325},[224,1276,1277],{"class":311},"2000",[224,1279,1262],{"class":325},[224,1281,1282,1285,1287,1289,1291,1294],{"class":159,"line":461},[224,1283,1284],{"class":252},"        'interactive'",[224,1286,1250],{"class":325},[224,1288,1272],{"class":252},[224,1290,1256],{"class":325},[224,1292,1293],{"class":311},"3500",[224,1295,1262],{"class":325},[224,1297,1298],{"class":159,"line":467},[224,1299,1300],{"class":325},"      },\n",[224,1302,1303],{"class":159,"line":473},[224,1304,945],{"class":325},[224,1306,1307],{"class":159,"line":479},[224,1308,1309],{"class":325},"  },\n",[224,1311,1312],{"class":159,"line":508},[224,1313,1314],{"class":325},"};\n",[224,1316,1317],{"class":159,"line":514},[224,1318,1319],{"class":229},"\u002F\u002F trade-off: TBT is a load-time proxy and never sees post-load interactions — a green\n",[224,1321,1322],{"class":159,"line":520},[224,1323,1324],{"class":229},"\u002F\u002F TBT with red field INP means the slow interaction is a route change or modal, so\n",[224,1326,1327],{"class":159,"line":766},[224,1328,1329],{"class":229},"\u002F\u002F pair this gate with a scripted interaction assertion and RUM.\n",[15,1331,1332,1333,1335,1336,1339,1340,1342,1343,902],{},"Add a Playwright or Puppeteer script that performs the target interaction while recording ",[28,1334,1174],{}," entries, then asserts that no frame's ",[28,1337,1338],{},"blockingDuration"," exceeds ",[28,1341,600],{},". This catches the regression TBT misses — a slow interaction that only happens after load, where page-load metrics stay green. Run it in the same pipeline stage as Lighthouse so one gate covers load-time and interaction-time blocking. Finally, confirm the field p75 INP moved in your RUM dashboard before and after; treat the fix as proven only when the field number drops, since synthetic devices rarely reproduce the slow tail. The lab gate blocks the regression from merging; the field check confirms the win. The complete CI harness is detailed in ",[19,1344,1346],{"href":1345},"\u002Fcore-web-vitals-measurement\u002Funderstanding-core-web-vitals-thresholds\u002Fbest-lighthouse-ci-setup-for-frontend-pipelines\u002F","the Lighthouse CI setup for frontend pipelines",[188,1348,1350],{"id":1349},"related","Related",[992,1352,1353,1359,1365,1371,1377],{},[995,1354,1355,1358],{},[19,1356,1357],{"href":564},"Optimizing INP with scheduler.yield()"," — once you have attributed a slow handler, split it with the modern scheduling APIs.",[995,1360,1361,1364],{},[19,1362,1363],{"href":532},"Finding the slowest interaction in the Performance panel"," — the mechanical hunt for the worst interaction bar in a long recording.",[995,1366,1367,1370],{},[19,1368,1369],{"href":554},"Reducing input delay from third-party tags"," — when analytics, ads, or chat scripts inflate the input-delay phase.",[995,1372,1373,1376],{},[19,1374,1375],{"href":1146},"Offloading work to web workers with Comlink"," — when a handler's compute is too heavy to keep on the main thread at all.",[995,1378,1379,1382],{},[19,1380,1381],{"href":1055},"Improving INP for complex single page applications"," — phase attribution applied to router transitions and hydration.",[1384,1385,1387],"script",{"type":1386},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Profiling Event Handlers for INP\",\n  \"description\": \"Attribute a slow Interaction to Next Paint to a specific event handler using the DevTools Performance panel, the Event Timing API, and the Long Animation Frames API.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"name\": \"Environment setup for reproducible interaction capture\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F#1-environment-setup-for-reproducible-interaction-capture\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Capture a baseline with Event Timing and the Performance panel\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F#2-capture-a-baseline-with-event-timing-and-the-performance-panel\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Isolate the dominant phase before touching any handler\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F#3-isolate-the-dominant-phase-before-touching-any-handler\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Attribute the cost to a specific handler with LoAF\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F#4-attribute-the-cost-to-a-specific-handler-with-loaf\" }\n  ]\n}\n",[1384,1389,1390],{"type":1386},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Profiling Event Handlers for INP: Attributing Slow Interactions to a Specific Handler\",\n  \"description\": \"Profile interactions in the DevTools Performance panel, the Event Timing API, and LoAF to attribute INP to a specific handler and phase.\",\n  \"datePublished\": \"2026-06-18\",\n  \"dateModified\": \"2026-06-18\",\n  \"mainEntityOfPage\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F\"\n}\n",[1384,1392,1393],{"type":1386},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Core Web Vitals & Measurement\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Profiling Event Handlers for INP\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F\" }\n  ]\n}\n",[1395,1396,1397],"style",{},"html pre.shiki code .sIIH1, html code.shiki .sIIH1{--shiki-default:#66707B;--shiki-dark:#66707B;--shiki-light:#66707B}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}html pre.shiki code .s-_DF, html code.shiki .s-_DF{--shiki-default:#032563;--shiki-dark:#032563;--shiki-light:#032563}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sP5qI, html code.shiki .sP5qI{--shiki-default:#A0111F;--shiki-dark:#A0111F;--shiki-light:#A0111F}html pre.shiki code .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}html pre.shiki code .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}html pre.shiki code .syybb, html code.shiki .syybb{--shiki-default:#0E1116;--shiki-dark:#0E1116;--shiki-light:#0E1116}",{"title":220,"searchDepth":233,"depth":233,"links":1399},[1400,1401,1402,1403,1404,1405,1406,1407],{"id":190,"depth":233,"text":191},{"id":272,"depth":233,"text":273},{"id":537,"depth":233,"text":538},{"id":587,"depth":233,"text":588},{"id":983,"depth":233,"text":984},{"id":1060,"depth":233,"text":1061},{"id":1199,"depth":233,"text":1200},{"id":1349,"depth":233,"text":1350},"Attribute INP to a specific handler with the Performance panel, Event Timing API, and LoAF.","md",{"slug":1411,"type":1412,"breadcrumb":1413,"datePublished":1420,"dateModified":1420},"profiling-event-handlers-for-inp","cluster",[1414,1417,1418],{"name":1415,"url":1416},"Home","\u002F",{"name":22,"url":21},{"name":5,"url":1419},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F","2026-06-18","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp",{"title":5,"description":1423},"Profile interactions in the DevTools Performance panel, the Event Timing API, and the Long Animation Frames API to attribute INP to a specific handler and phase.","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Findex","QYNoYiP6iAfdvWSaat_OkgyXLYDmihQ4msEmS7KBmjA",[1427,1431],{"title":1428,"path":1429,"stem":1430,"children":-1},"Breaking Up Long Tasks in React Event Handlers","\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002Findex",{"title":1432,"path":1433,"stem":1434,"children":-1},"Finding the Slowest Interaction in the Performance Panel","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffinding-the-slowest-interaction-in-the-performance-panel","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffinding-the-slowest-interaction-in-the-performance-panel\u002Findex",1782237170909]