[{"data":1,"prerenderedAt":2436},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api":2427},{"id":4,"title":5,"body":6,"description":2408,"extension":2409,"meta":2410,"navigation":1817,"path":2421,"seo":2422,"stem":2425,"__hash__":2426},"content\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Findex.md","Long Animation Frames (LoAF) API",{"type":7,"value":8,"toc":2392},"minimark",[9,14,29,41,56,214,219,225,276,289,293,343,347,358,566,570,581,730,733,737,790,919,933,937,940,984,1115,1119,1128,1221,1299,1307,1311,1334,1347,1369,1375,1385,1389,1392,1664,1675,1782,1786,1791,2061,2065,2068,2087,2102,2115,2134,2152,2265,2269,2288,2300,2312,2316,2343,2347,2377,2382,2385,2388],[10,11,13],"h1",{"id":12},"long-animation-frames-loaf-attributing-slow-frames-to-the-script-that-caused-them","Long Animation Frames (LoAF): Attributing Slow Frames to the Script That Caused Them",[15,16,17,18,23,24,28],"p",{},"This topic extends the interactivity tooling in ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement"," with the one browser API that tells you ",[25,26,27],"em",{},"which script"," made a frame slow, not merely that it was slow.",[15,30,31,32,36,37,40],{},"For years the only field signal for main-thread congestion was the Long Tasks API: an entry saying \"something ran for 180ms\" with an attribution field that, in practice, almost always read ",[33,34,35],"code",{},"unknown",". That was enough to prove a problem existed and useless for assigning it to a team. The Long Animation Frames API (LoAF), stable in Chromium since version 123, replaces the task as the unit of measurement with the ",[25,38,39],{},"frame"," — the full span from the start of work to the next paint — and attaches a list of the scripts that ran inside it, with their source URL, function name, character position, invoker type and how long each spent in forced style and layout.",[15,42,43,44,47,48,51,52,55],{},"That shift matters because Interaction to Next Paint is itself a frame-based measurement. An interaction fails the ",[33,45,46],{},"200ms"," budget when the frame that should present its result is late, and the reason a frame is late is rarely one task: it is a sequence of event listeners, promise continuations, a framework commit and a rendering phase that together overran. LoAF reports exactly that sequence, so the gap between \"INP is 340ms at p75\" and \"the ",[33,49,50],{},"onChange"," handler in ",[33,53,54],{},"filters.js"," spends 190ms in forced layout\" closes from days to minutes.",[15,57,58],{},[59,60,66,67,66,74,66,78,66,82,66,91,66,97,66,108,66,113,66,122,66,129,66,134,66,138,66,144,66,148,66,152,66,157,66,162,66,166,66,170,66,175,66,181,66,184,66,188,66,192,66,195,66,199,66,205,66,210,66],"svg",{"viewBox":61,"width":62,"role":63,"ariaLabel":64,"style":65},"0 0 760 244","100%","img","Timeline of a single 240ms animation frame showing an event listener, a promise continuation and a framework commit, followed by style and layout and paint, with the 50ms threshold marked near the start.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[68,69],"rect",{"className":70,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],"svg-canvas","0","#ffffff",[75,76,77],"title",{},"One long animation frame, as LoAF reports it",[79,80,81],"desc",{},"A 240 millisecond frame broken into three script entries, a style and layout phase and paint, with the 50 millisecond long-frame threshold marked.",[68,83],{"x":84,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"1","758","242","10","none","currentColor","stroke-opacity:0.18",[92,93,77],"text",{"x":94,"y":95,"fill":89,"style":96},"28.0","34.0","font-size:16px;font-weight:700",[98,99],"line",{"x1":100,"y1":101,"x2":100,"y2":102,"stroke":103,"strokeWidth":104,"strokeDashArray":105,"style":107},"231.8","72.0","156.0","#b8860b","1.5",[106,106],"4","stroke-opacity:0.9",[92,109,112],{"x":94,"y":110,"fill":89,"style":111},"96.0","font-size:12.5px;font-weight:700","Scripts",[68,114],{"x":115,"y":116,"width":117,"height":118,"rx":119,"fill":120,"stroke":120,"style":121},"109.0","78.0","171.5","26.0","3","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[92,123,128],{"x":124,"y":125,"fill":89,"style":126,"textAnchor":127},"194.7","95.5","font-size:11.5px","middle","click listener",[68,130],{"x":131,"y":116,"width":132,"height":118,"rx":119,"fill":89,"stroke":89,"style":133},"282.0","122.1","fill-opacity:0.06;stroke-opacity:0.4",[92,135,137],{"x":136,"y":125,"fill":89,"style":126,"textAnchor":127},"343.0","promise then",[68,139],{"x":140,"y":116,"width":141,"height":118,"rx":119,"fill":142,"stroke":103,"style":143},"405.5","159.1","#ffc300","fill-opacity:0.24;stroke-opacity:0.9",[92,145,147],{"x":146,"y":125,"fill":89,"style":126,"textAnchor":127},"485.1","commit",[92,149,151],{"x":94,"y":150,"fill":89,"style":111},"134.0","Rendering",[68,153],{"x":154,"y":155,"width":156,"height":118,"rx":119,"fill":120,"stroke":120,"style":121},"566.1","116.0","97.3",[92,158,161],{"x":159,"y":160,"fill":89,"style":126,"textAnchor":127},"614.8","133.5","style + layout",[68,163],{"x":164,"y":155,"width":165,"height":118,"rx":119,"fill":89,"stroke":89,"style":133},"665.0","35.6",[92,167,169],{"x":168,"y":160,"fill":89,"style":126,"textAnchor":127},"682.8","paint",[98,171],{"x1":172,"y1":102,"x2":173,"y2":102,"stroke":89,"style":174},"108.2","726.0","stroke-opacity:0.4",[92,176,180],{"x":177,"y":178,"fill":179,"style":126,"textAnchor":127},"120.2","173.0","#51617a","0ms",[92,182,183],{"x":100,"y":178,"fill":179,"style":126,"textAnchor":127},"50ms",[92,185,187],{"x":186,"y":178,"fill":179,"style":126,"textAnchor":127},"355.3","100ms",[92,189,191],{"x":190,"y":178,"fill":179,"style":126,"textAnchor":127},"478.9","150ms",[92,193,46],{"x":194,"y":178,"fill":179,"style":126,"textAnchor":127},"602.4",[92,196,198],{"x":197,"y":178,"fill":179,"style":126,"textAnchor":127},"713.3","250ms",[92,200,204],{"x":100,"y":201,"fill":202,"style":203,"textAnchor":127},"68.0","#8a6608","font-size:12px;font-weight:700","50ms long-frame line",[92,206,209],{"x":94,"y":207,"fill":179,"style":208},"204.0","font-size:12.5px","LoAF lists every script entry with its source URL and function, then reports renderStart and styleAndLayoutStart",[92,211,213],{"x":94,"y":212,"fill":179,"style":208},"222.0","for the tail of the frame.",[215,216,218],"h2",{"id":217},"the-metric-degradation-loaf-explains","The Metric Degradation LoAF Explains",[15,220,221,222,224],{},"The failure mode this topic addresses is an INP p75 above ",[33,223,46],{}," where the Performance panel shows a busy main thread but nothing obviously wrong in your own code. Typical shapes:",[226,227,228,240,254],"ul",{},[229,230,231,235,236,239],"li",{},[232,233,234],"strong",{},"Input delay dominates"," (",[33,237,238],{},">100ms",") because an unrelated script — a tag manager, a polling widget, a hydration chunk — owns the thread when the user taps. The Long Tasks API would report an unattributed task; LoAF names the script and the invoker that started it.",[229,241,242,245,246,249,250,253],{},[232,243,244],{},"Processing time dominates"," because several listeners fire for one interaction (a ",[33,247,248],{},"pointerdown",", a ",[33,251,252],{},"click"," and a framework's synthetic dispatcher) and each looks cheap alone. LoAF sums them inside one frame.",[229,255,256,259,260,263,264,267,268,271,272,275],{},[232,257,258],{},"Presentation delay dominates"," because the handler finished quickly but invalidated a large layout. LoAF's ",[33,261,262],{},"renderStart"," and ",[33,265,266],{},"styleAndLayoutStart"," timestamps isolate the rendering tail, and each script's ",[33,269,270],{},"forcedStyleAndLayoutDuration"," shows when layout was forced ",[25,273,274],{},"inside"," JavaScript instead.",[15,277,278,279,281,282,284,285,288],{},"A frame is considered long once it exceeds ",[33,280,183],{}," — the same budget the long-task definition uses — and a long frame overlapping an interaction is what pushes it past the ",[33,283,46],{}," boundary. The ",[33,286,287],{},"blockingDuration"," field approximates how much of that frame was unavailable for input, which is the figure that correlates with input delay in the field.",[215,290,292],{"id":291},"prerequisites","Prerequisites",[226,294,295,305,319,337],{},[229,296,297,300,301,304],{},[232,298,299],{},"Chromium 123+"," for the ",[33,302,303],{},"long-animation-frame"," entry type. Firefox and Safari do not expose it, so treat LoAF data as a Chromium-only diagnostic sample; it remains representative because the scripts are the same across engines.",[229,306,307,314,315,318],{},[232,308,309,310,313],{},"The ",[33,311,312],{},"web-vitals"," library v4 or later, attribution build."," Its INP attribution includes ",[33,316,317],{},"longAnimationFrameEntries"," for the interaction's frame, which saves you correlating timestamps by hand.",[229,320,321,324,325,328,329,332,333,336],{},[232,322,323],{},"Same-origin or CORS-enabled scripts."," A cross-origin script loaded without ",[33,326,327],{},"crossorigin"," and a matching ",[33,330,331],{},"Access-Control-Allow-Origin"," header reports its ",[33,334,335],{},"sourceURL"," but an empty function name and position — enough to blame a vendor, not to find the line.",[229,338,339,342],{},[232,340,341],{},"DevTools with the Performance panel's \"Show custom tracks\" option"," if you want to plot LoAF entries alongside the flame chart during lab reproduction.",[215,344,346],{"id":345},"_1-environment-setup-observe-long-frames-without-distorting-them","1. Environment Setup: Observe Long Frames Without Distorting Them",[15,348,349,350,353,354,357],{},"Register a single ",[33,351,352],{},"PerformanceObserver"," with ",[33,355,356],{},"buffered: true"," as early as possible so frames that happened before your observer existed are still delivered. Keep the callback cheap — it runs on the main thread you are trying to measure.",[359,360,365],"pre",{"className":361,"code":362,"language":363,"meta":364,"style":364},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F loaf-observer.js — load early, before the app bundle if you can.\nconst frames = [];\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    if (entry.duration \u003C 50) continue;            \u002F\u002F only long frames\n    frames.push(entry);\n    if (frames.length > 200) frames.shift();     \u002F\u002F bounded memory on long sessions\n  }\n}).observe({ type: 'long-animation-frame', buffered: true });\n\u002F\u002F trade-off: buffered:true replays a limited buffer of past entries, not the\n\u002F\u002F whole session. If you register late (after hydration), the slowest load-time\n\u002F\u002F frames may already have been evicted — inline the observer in \u003Chead> when\n\u002F\u002F load-phase attribution matters.\n","javascript","",[33,366,367,375,393,419,444,470,482,511,517,542,548,554,560],{"__ignoreMap":364},[368,369,371],"span",{"class":98,"line":370},1,[368,372,374],{"class":373},"sjfSM","\u002F\u002F loaf-observer.js — load early, before the app bundle if you can.\n",[368,376,378,382,386,389],{"class":98,"line":377},2,[368,379,381],{"class":380},"sPARh","const",[368,383,385],{"class":384},"sPXB4"," frames",[368,387,388],{"class":380}," =",[368,390,392],{"class":391},"saISM"," [];\n",[368,394,396,399,403,406,410,413,416],{"class":98,"line":395},3,[368,397,398],{"class":380},"new",[368,400,402],{"class":401},"smZ65"," PerformanceObserver",[368,404,405],{"class":391},"((",[368,407,409],{"class":408},"sQw3B","list",[368,411,412],{"class":391},") ",[368,414,415],{"class":380},"=>",[368,417,418],{"class":391}," {\n",[368,420,422,425,427,429,432,435,438,441],{"class":98,"line":421},4,[368,423,424],{"class":380},"  for",[368,426,235],{"class":391},[368,428,381],{"class":380},[368,430,431],{"class":384}," entry",[368,433,434],{"class":380}," of",[368,436,437],{"class":391}," list.",[368,439,440],{"class":401},"getEntries",[368,442,443],{"class":391},"()) {\n",[368,445,447,450,453,456,459,461,464,467],{"class":98,"line":446},5,[368,448,449],{"class":380},"    if",[368,451,452],{"class":391}," (entry.duration ",[368,454,455],{"class":380},"\u003C",[368,457,458],{"class":384}," 50",[368,460,412],{"class":391},[368,462,463],{"class":380},"continue",[368,465,466],{"class":391},";            ",[368,468,469],{"class":373},"\u002F\u002F only long frames\n",[368,471,473,476,479],{"class":98,"line":472},6,[368,474,475],{"class":391},"    frames.",[368,477,478],{"class":401},"push",[368,480,481],{"class":391},"(entry);\n",[368,483,485,487,490,493,496,499,502,505,508],{"class":98,"line":484},7,[368,486,449],{"class":380},[368,488,489],{"class":391}," (frames.",[368,491,492],{"class":384},"length",[368,494,495],{"class":380}," >",[368,497,498],{"class":384}," 200",[368,500,501],{"class":391},") frames.",[368,503,504],{"class":401},"shift",[368,506,507],{"class":391},"();     ",[368,509,510],{"class":373},"\u002F\u002F bounded memory on long sessions\n",[368,512,514],{"class":98,"line":513},8,[368,515,516],{"class":391},"  }\n",[368,518,520,523,526,529,533,536,539],{"class":98,"line":519},9,[368,521,522],{"class":391},"}).",[368,524,525],{"class":401},"observe",[368,527,528],{"class":391},"({ type: ",[368,530,532],{"class":531},"sZ8jY","'long-animation-frame'",[368,534,535],{"class":391},", buffered: ",[368,537,538],{"class":384},"true",[368,540,541],{"class":391}," });\n",[368,543,545],{"class":98,"line":544},10,[368,546,547],{"class":373},"\u002F\u002F trade-off: buffered:true replays a limited buffer of past entries, not the\n",[368,549,551],{"class":98,"line":550},11,[368,552,553],{"class":373},"\u002F\u002F whole session. If you register late (after hydration), the slowest load-time\n",[368,555,557],{"class":98,"line":556},12,[368,558,559],{"class":373},"\u002F\u002F frames may already have been evicted — inline the observer in \u003Chead> when\n",[368,561,563],{"class":98,"line":562},13,[368,564,565],{"class":373},"\u002F\u002F load-phase attribution matters.\n",[215,567,569],{"id":568},"_2-capture-a-baseline-which-frames-overlap-interactions","2. Capture a Baseline: Which Frames Overlap Interactions?",[15,571,572,573,576,577,580],{},"Not every long frame matters. A 300ms frame during a background data refresh with no user input is a throughput problem, not an INP problem. The baseline you want is the set of long frames that ",[25,574,575],{},"contain an interaction",": LoAF exposes ",[33,578,579],{},"firstUIEventTimestamp",", which is non-zero when the frame processed user input.",[359,582,584],{"className":361,"code":583,"language":363,"meta":364,"style":364},"const interactive = frames.filter((f) => f.firstUIEventTimestamp > 0);\nconsole.table(interactive.map((f) => ({\n  start: Math.round(f.startTime),\n  duration: Math.round(f.duration),\n  blocking: Math.round(f.blockingDuration),\n  scripts: f.scripts.length,\n  renderTail: Math.round(f.startTime + f.duration - f.renderStart),\n})));\n\u002F\u002F trade-off: firstUIEventTimestamp marks frames that *handled* input, but a\n\u002F\u002F long frame immediately BEFORE the input is what inflates input delay. Keep\n\u002F\u002F the preceding frame too when you are chasing input delay rather than processing.\n",[33,585,586,622,647,658,668,678,688,710,715,720,725],{"__ignoreMap":364},[368,587,588,590,593,595,598,601,603,606,608,610,613,616,619],{"class":98,"line":370},[368,589,381],{"class":380},[368,591,592],{"class":384}," interactive",[368,594,388],{"class":380},[368,596,597],{"class":391}," frames.",[368,599,600],{"class":401},"filter",[368,602,405],{"class":391},[368,604,605],{"class":408},"f",[368,607,412],{"class":391},[368,609,415],{"class":380},[368,611,612],{"class":391}," f.firstUIEventTimestamp ",[368,614,615],{"class":380},">",[368,617,618],{"class":384}," 0",[368,620,621],{"class":391},");\n",[368,623,624,627,630,633,636,638,640,642,644],{"class":98,"line":377},[368,625,626],{"class":391},"console.",[368,628,629],{"class":401},"table",[368,631,632],{"class":391},"(interactive.",[368,634,635],{"class":401},"map",[368,637,405],{"class":391},[368,639,605],{"class":408},[368,641,412],{"class":391},[368,643,415],{"class":380},[368,645,646],{"class":391}," ({\n",[368,648,649,652,655],{"class":98,"line":395},[368,650,651],{"class":391},"  start: Math.",[368,653,654],{"class":401},"round",[368,656,657],{"class":391},"(f.startTime),\n",[368,659,660,663,665],{"class":98,"line":421},[368,661,662],{"class":391},"  duration: Math.",[368,664,654],{"class":401},[368,666,667],{"class":391},"(f.duration),\n",[368,669,670,673,675],{"class":98,"line":446},[368,671,672],{"class":391},"  blocking: Math.",[368,674,654],{"class":401},[368,676,677],{"class":391},"(f.blockingDuration),\n",[368,679,680,683,685],{"class":98,"line":472},[368,681,682],{"class":391},"  scripts: f.scripts.",[368,684,492],{"class":384},[368,686,687],{"class":391},",\n",[368,689,690,693,695,698,701,704,707],{"class":98,"line":484},[368,691,692],{"class":391},"  renderTail: Math.",[368,694,654],{"class":401},[368,696,697],{"class":391},"(f.startTime ",[368,699,700],{"class":380},"+",[368,702,703],{"class":391}," f.duration ",[368,705,706],{"class":380},"-",[368,708,709],{"class":391}," f.renderStart),\n",[368,711,712],{"class":98,"line":513},[368,713,714],{"class":391},"})));\n",[368,716,717],{"class":98,"line":519},[368,718,719],{"class":373},"\u002F\u002F trade-off: firstUIEventTimestamp marks frames that *handled* input, but a\n",[368,721,722],{"class":98,"line":544},[368,723,724],{"class":373},"\u002F\u002F long frame immediately BEFORE the input is what inflates input delay. Keep\n",[368,726,727],{"class":98,"line":550},[368,728,729],{"class":373},"\u002F\u002F the preceding frame too when you are chasing input delay rather than processing.\n",[15,731,732],{},"Record this on a throttled lab profile (4x CPU) for the interactions your RUM flags as slowest. Two or three frames will dominate; that is your shortlist.",[215,734,736],{"id":735},"_3-isolate-the-bottleneck-read-the-script-entries","3. Isolate the Bottleneck: Read the Script Entries",[15,738,739,740,743,744,747,748,751,752,755,756,235,759,762,763,762,766,762,769,762,772,755,775,762,777,762,780,762,783,786,787,789],{},"Each entry's ",[33,741,742],{},"scripts"," array is the heart of the API. For each script you get ",[33,745,746],{},"invoker"," (for example ",[33,749,750],{},"BUTTON#save.onclick"," or ",[33,753,754],{},"Response.json.then","), ",[33,757,758],{},"invokerType",[33,760,761],{},"event-listener",", ",[33,764,765],{},"user-callback",[33,767,768],{},"resolve-promise",[33,770,771],{},"classic-script",[33,773,774],{},"module-script",[33,776,335],{},[33,778,779],{},"sourceFunctionName",[33,781,782],{},"sourceCharPosition",[33,784,785],{},"duration",", and ",[33,788,270],{},".",[15,791,792],{},[59,793,66,796,66,799,66,802,66,804,66,807,66,809,66,814,66,820,66,824,66,828,66,831,66,835,66,839,66,842,66,844,66,848,66,850,66,853,66,855,66,858,66,860,66,863,66,865,66,868,66,871,66,875,66,877,66,880,66,882,66,885,66,888,66,892,66,894,66,897,66,899,66,902,66,905,66,909,66,911,66,914,66,916,66],{"viewBox":794,"width":62,"role":63,"ariaLabel":795,"style":65},"0 0 760 258","Which LoAF script fields answer which debugging question, with the fields that point to a fix highlighted.",[68,797],{"className":798,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],[75,800,801],{},"Reading a LoAF script entry",[79,803,795],{},[68,805],{"x":84,"y":84,"width":85,"height":806,"rx":87,"fill":88,"stroke":89,"style":90},"256",[92,808,801],{"x":94,"y":95,"fill":89,"style":96},[68,810],{"x":94,"y":811,"width":812,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"56.0","150.4","30.0",[92,815,819],{"x":816,"y":817,"fill":89,"style":111,"textAnchor":818},"38.0","75.5","start","Field",[68,821],{"x":822,"y":811,"width":823,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"178.4","276.8",[92,825,827],{"x":826,"y":817,"fill":89,"style":111,"textAnchor":127},"316.8","Question it answers",[68,829],{"x":830,"y":811,"width":823,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"455.2",[92,832,834],{"x":833,"y":817,"fill":89,"style":111,"textAnchor":127},"593.6","What to do with it",[68,836],{"x":94,"y":837,"width":812,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},"86.0","stroke-opacity:0.35",[92,840,758],{"x":816,"y":841,"fill":89,"style":111,"textAnchor":818},"105.5",[68,843],{"x":822,"y":837,"width":823,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,845,847],{"x":826,"y":841,"fill":89,"style":846,"textAnchor":127},"font-size:12px","Who started this work?",[68,849],{"x":830,"y":837,"width":823,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},[92,851,852],{"x":833,"y":841,"fill":89,"style":846,"textAnchor":127},"listener, promise, timer or script eval",[68,854],{"x":94,"y":155,"width":812,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,856,746],{"x":816,"y":857,"fill":89,"style":111,"textAnchor":818},"135.5",[68,859],{"x":822,"y":155,"width":823,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,861,862],{"x":826,"y":857,"fill":89,"style":846,"textAnchor":127},"Which element or API?",[68,864],{"x":830,"y":155,"width":823,"height":813,"rx":72,"fill":120,"stroke":120,"style":121},[92,866,867],{"x":833,"y":857,"fill":89,"style":846,"textAnchor":127},"jump straight to the handler",[68,869],{"x":94,"y":870,"width":812,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},"146.0",[92,872,874],{"x":816,"y":873,"fill":89,"style":111,"textAnchor":818},"165.5","sourceURL + position",[68,876],{"x":822,"y":870,"width":823,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,878,879],{"x":826,"y":873,"fill":89,"style":846,"textAnchor":127},"Which file and line?",[68,881],{"x":830,"y":870,"width":823,"height":813,"rx":72,"fill":120,"stroke":120,"style":121},[92,883,884],{"x":833,"y":873,"fill":89,"style":846,"textAnchor":127},"open in Sources panel",[68,886],{"x":94,"y":887,"width":812,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},"176.0",[92,889,891],{"x":816,"y":890,"fill":89,"style":111,"textAnchor":818},"195.5","forcedStyleAndLayout",[68,893],{"x":822,"y":887,"width":823,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,895,896],{"x":826,"y":890,"fill":89,"style":846,"textAnchor":127},"Did JS force layout?",[68,898],{"x":830,"y":887,"width":823,"height":813,"rx":72,"fill":142,"stroke":103,"style":143},[92,900,901],{"x":833,"y":890,"fill":89,"style":846,"textAnchor":127},"batch reads before writes",[68,903],{"x":94,"y":904,"width":812,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},"206.0",[92,906,908],{"x":816,"y":907,"fill":89,"style":111,"textAnchor":818},"225.5","pauseDuration",[68,910],{"x":822,"y":904,"width":823,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,912,913],{"x":826,"y":907,"fill":89,"style":846,"textAnchor":127},"Was it blocked on sync XHR or alert?",[68,915],{"x":830,"y":904,"width":823,"height":813,"rx":72,"fill":142,"stroke":103,"style":143},[92,917,918],{"x":833,"y":907,"fill":89,"style":846,"textAnchor":127},"remove the sync API",[15,920,921,922,924,925,927,928,932],{},"Sort the scripts in the slowest interactive frames by ",[33,923,785],{}," and look at the top three. In practice they fall into a small number of recognisable patterns: one heavy listener (fix with yielding), a third-party script evaluating during the interaction (fix with deferral), or a cheap listener with a large ",[33,926,270],{}," (fix the read\u002Fwrite order). The guide on ",[19,929,931],{"href":930},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Ffinding-slow-scripts-with-loaf-script-attribution\u002F","finding slow scripts with LoAF script attribution"," walks through each pattern on a real trace.",[215,934,936],{"id":935},"_4-apply-the-fix-matched-to-the-phase","4. Apply the Fix Matched to the Phase",[15,938,939],{},"LoAF does not fix anything; it routes you to the right existing technique:",[226,941,942,952,962,974],{},[229,943,944,947,948,789],{},[232,945,946],{},"Heavy event listener"," → split it with the await-yield loop from ",[19,949,951],{"href":950},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F","optimizing INP with scheduler.yield()",[229,953,954,957,958,789],{},[232,955,956],{},"Third-party script evaluated during the frame"," → load it on interaction or idle, as in ",[19,959,961],{"href":960},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fthird-party-script-performance\u002F","third-party script performance",[229,963,964,969,970,789],{},[232,965,966,967],{},"Large ",[33,968,270],{}," → reorder DOM reads before writes; see ",[19,971,973],{"href":972},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002F","layout thrashing and forced reflow",[229,975,976,979,980,789],{},[232,977,978],{},"Short scripts, long render tail"," → reduce the size of the DOM update, the subject of ",[19,981,983],{"href":982},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002F","diagnosing presentation delay with LoAF",[15,985,986],{},[59,987,66,990,66,993,66,996,66,998,66,1015,66,1018,66,1020,66,1025,66,1030,66,1033,66,1037,66,1044,66,1050,66,1054,66,1059,66,1062,66,1066,66,1068,66,1071,66,1074,66,1077,66,1080,66,1083,66,1086,66,1090,66,1092,66,1095,66,1098,66,1101,66,1105,66,1108,66,1111,66],{"viewBox":988,"width":62,"role":63,"ariaLabel":989,"style":65},"0 0 760 325","A decision sequence that maps the dominant part of a long animation frame to the optimisation technique that addresses it.",[68,991],{"className":992,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],[75,994,995],{},"Routing a long frame to its fix",[79,997,989],{},[999,1000,1001],"defs",{},[1002,1003,1010],"marker",{"id":1004,"viewBox":1005,"refX":1006,"refY":1007,"markerWidth":1008,"markerHeight":1008,"orient":1009},"fa21d3bb9b","0 0 10 10","9","5","7","auto-start-reverse",[1011,1012],"path",{"d":1013,"fill":89,"style":1014},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[68,1016],{"x":84,"y":84,"width":85,"height":1017,"rx":87,"fill":88,"stroke":89,"style":90},"323",[92,1019,995],{"x":94,"y":95,"fill":89,"style":96},[68,1021],{"x":94,"y":811,"width":1022,"height":1023,"rx":1024,"fill":89,"stroke":89,"style":133},"320.0","35.0","6",[92,1026,1029],{"x":1027,"y":116,"fill":89,"style":1028,"textAnchor":127},"188.0","font-size:13px;font-weight:700","One script entry over 50ms?",[68,1031],{"x":1032,"y":811,"width":1022,"height":1023,"rx":1024,"fill":120,"stroke":120,"style":121},"412.0",[92,1034,1036],{"x":1035,"y":116,"fill":89,"style":208,"textAnchor":127},"572.0","Split it with scheduler.yield() or move it to a worker",[98,1038],{"x1":1039,"y1":1040,"x2":1041,"y2":1040,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"348.0","73.5","410.0","stroke-opacity:0.6","url(#fa21d3bb9b)",[92,1045,1049],{"x":1046,"y":1047,"fill":120,"style":1048,"textAnchor":127},"380.0","66.5","font-size:11.5px;font-weight:700","yes",[98,1051],{"x1":1027,"y1":1052,"x2":1027,"y2":1053,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"91.0","125.0",[92,1055,1058],{"x":1056,"y":1057,"fill":179,"style":1048},"196.0","113.0","no",[68,1060],{"x":94,"y":1061,"width":1022,"height":1023,"rx":1024,"fill":89,"stroke":89,"style":133},"127.0",[92,1063,1065],{"x":1027,"y":1064,"fill":89,"style":1028,"textAnchor":127},"149.0","Third-party sourceURL in the frame?",[68,1067],{"x":1032,"y":1061,"width":1022,"height":1023,"rx":1024,"fill":120,"stroke":120,"style":121},[92,1069,1070],{"x":1035,"y":1064,"fill":89,"style":208,"textAnchor":127},"Defer that vendor until idle or interaction",[98,1072],{"x1":1039,"y1":1073,"x2":1041,"y2":1073,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"144.5",[92,1075,1049],{"x":1046,"y":1076,"fill":120,"style":1048,"textAnchor":127},"137.5",[98,1078],{"x1":1027,"y1":1079,"x2":1027,"y2":1056,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"162.0",[92,1081,1058],{"x":1056,"y":1082,"fill":179,"style":1048},"184.0",[68,1084],{"x":94,"y":1085,"width":1022,"height":1023,"rx":1024,"fill":89,"stroke":89,"style":133},"198.0",[92,1087,1089],{"x":1027,"y":1088,"fill":89,"style":1028,"textAnchor":127},"220.0","forcedStyleAndLayout is large?",[68,1091],{"x":1032,"y":1085,"width":1022,"height":1023,"rx":1024,"fill":120,"stroke":120,"style":121},[92,1093,1094],{"x":1035,"y":1088,"fill":89,"style":208,"textAnchor":127},"Batch DOM reads before writes",[98,1096],{"x1":1039,"y1":1097,"x2":1041,"y2":1097,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"215.5",[92,1099,1049],{"x":1046,"y":1100,"fill":120,"style":1048,"textAnchor":127},"208.5",[98,1102],{"x1":1027,"y1":1103,"x2":1027,"y2":1104,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1043},"233.0","267.0",[92,1106,1058],{"x":1056,"y":1107,"fill":179,"style":1048},"255.0",[68,1109],{"x":94,"y":1110,"width":1022,"height":95,"rx":1024,"fill":142,"stroke":103,"style":143},"269.0",[92,1112,1114],{"x":1027,"y":1113,"fill":89,"style":208,"textAnchor":127},"290.5","Render tail dominates — shrink the DOM update",[215,1116,1118],{"id":1117},"deconstructing-a-long-animation-frame","Deconstructing a Long Animation Frame",[15,1120,1121,1122,1124,1125,1127],{},"A LoAF entry has enough timestamps to split its duration into four phases, and each has a practical budget if the whole frame is to stay under roughly ",[33,1123,187],{}," so the interaction lands under ",[33,1126,46],{}," once input delay is added.",[629,1129,1130,1149],{},[1131,1132,1133],"thead",{},[1134,1135,1136,1140,1143,1146],"tr",{},[1137,1138,1139],"th",{},"Phase",[1137,1141,1142],{},"Derived as",[1137,1144,1145],{},"Practical budget",[1137,1147,1148],{},"Typical culprit",[1150,1151,1152,1170,1185,1205],"tbody",{},[1134,1153,1154,1158,1164,1167],{},[1155,1156,1157],"td",{},"Script execution",[1155,1159,1160,1161],{},"sum of ",[33,1162,1163],{},"scripts[].duration",[1155,1165,1166],{},"\u003C 50ms",[1155,1168,1169],{},"listeners, framework commit",[1134,1171,1172,1175,1179,1182],{},[1155,1173,1174],{},"Forced layout inside script",[1155,1176,1160,1177],{},[33,1178,270],{},[1155,1180,1181],{},"\u003C 10ms",[1155,1183,1184],{},"read-after-write in a loop",[1134,1186,1187,1190,1195,1198],{},[1155,1188,1189],{},"Pre-render work",[1155,1191,1192],{},[33,1193,1194],{},"renderStart − (last script end)",[1155,1196,1197],{},"\u003C 5ms",[1155,1199,1200,1201,1204],{},"microtask chains, ",[33,1202,1203],{},"requestAnimationFrame"," callbacks",[1134,1206,1207,1210,1215,1218],{},[1155,1208,1209],{},"Style, layout, paint",[1155,1211,1212],{},[33,1213,1214],{},"startTime + duration − renderStart",[1155,1216,1217],{},"\u003C 30ms",[1155,1219,1220],{},"large DOM mutation, expensive selectors",[15,1222,1223],{},[59,1224,66,1227,66,1230,66,1233,66,1235,66,1238,66,1240,66,1245,66,1251,66,1257,66,1262,66,1266,66,1270,66,1274,66,1278,66,1281,66,1285,66,1289,66,1292,66,1296,66],{"viewBox":1225,"width":62,"role":63,"ariaLabel":1226,"style":65},"0 0 760 216","Bars comparing each phase of a failing 238 millisecond frame against a 50 millisecond reference line.",[68,1228],{"className":1229,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],[75,1231,1232],{},"Phase budget vs a failing frame",[79,1234,1226],{},[68,1236],{"x":84,"y":84,"width":85,"height":1237,"rx":87,"fill":88,"stroke":89,"style":90},"214",[92,1239,1232],{"x":94,"y":95,"fill":89,"style":96},[98,1241],{"x1":1242,"y1":101,"x2":1242,"y2":1243,"stroke":103,"strokeWidth":104,"strokeDashArray":1244,"style":107},"365.1","194.0",[106,106],[92,1246,1157],{"x":1247,"y":1248,"fill":89,"style":1249,"textAnchor":1250},"158.9","92.0","font-size:13px","end",[68,1252],{"x":1253,"y":116,"width":1254,"height":1255,"rx":119,"fill":142,"stroke":103,"style":1256},"170.9","497.1","19","fill-opacity:0.7;stroke-opacity:0.9",[92,1258,1261],{"x":1259,"y":1248,"fill":89,"style":1260},"674.0","font-size:12px;font-weight:600","128ms",[92,1263,1265],{"x":1247,"y":1264,"fill":89,"style":1249,"textAnchor":1250},"123.0","Forced layout in script",[68,1267],{"x":1253,"y":115,"width":1268,"height":1255,"rx":119,"fill":120,"stroke":120,"style":1269},"159.2","fill-opacity:0.55;stroke-opacity:0.9",[92,1271,1273],{"x":1272,"y":1264,"fill":89,"style":1260},"336.1","41ms",[92,1275,1277],{"x":1247,"y":1276,"fill":89,"style":1249,"textAnchor":1250},"154.0","Pre-render callbacks",[68,1279],{"x":1253,"y":1280,"width":1023,"height":1255,"rx":119,"fill":120,"stroke":120,"style":1269},"140.0",[92,1282,1284],{"x":1283,"y":1276,"fill":89,"style":1260},"211.8","9ms",[92,1286,1288],{"x":1247,"y":1287,"fill":89,"style":1249,"textAnchor":1250},"185.0","Style + layout + paint",[68,1290],{"x":1253,"y":1291,"width":1103,"height":1255,"rx":119,"fill":142,"stroke":103,"style":1256},"171.0",[92,1293,1295],{"x":1294,"y":1287,"fill":89,"style":1260},"409.9","60ms",[92,1297,1298],{"x":1242,"y":201,"fill":202,"style":203,"textAnchor":127},"50ms reference",[15,1300,309,1301,1303,1304,1306],{},[33,1302,287],{}," field is distinct from all of these: it is the sum, across the long tasks inside the frame, of time beyond the first ",[33,1305,183],{}," of each, plus rendering time. Use it as a ranking key for input-delay risk, and the phase split above to decide what to change.",[215,1308,1310],{"id":1309},"advanced-diagnostics-and-edge-cases","Advanced Diagnostics and Edge Cases",[15,1312,1313,1316,1317,1319,1320,1322,1323,1326,1327,1330,1331,1333],{},[232,1314,1315],{},"Cross-origin scripts with empty attribution."," When ",[33,1318,779],{}," is blank and ",[33,1321,782],{}," is ",[33,1324,1325],{},"-1",", the script was loaded without CORS. Adding ",[33,1328,1329],{},"crossorigin=\"anonymous\""," fixes attribution only if the vendor serves ",[33,1332,331],{},"; if they do not, the request will fail outright, so test before shipping.",[15,1335,1336,1339,1340,1342,1343,1346],{},[232,1337,1338],{},"Frames with zero scripts."," A long frame with an empty ",[33,1341,742],{}," array means the cost was entirely rendering — commonly a CSS animation on a layout property or a ",[33,1344,1345],{},"content-visibility"," region being rendered for the first time. Look at the render tail, not your JavaScript.",[15,1348,1349,1352,1353,1355,1356,1359,1360,263,1362,1364,1365,1368],{},[232,1350,1351],{},"Framework dispatchers hiding the real handler."," React and Vue attach a single root listener, so ",[33,1354,746],{}," reads something like ",[33,1357,1358],{},"DIV#root.onclick"," for every interaction. Use ",[33,1361,779],{},[33,1363,782],{}," with source maps to resolve the real component; or set ",[33,1366,1367],{},"performance.mark()"," calls in suspect handlers and match timestamps.",[15,1370,1371,1374],{},[232,1372,1373],{},"Script entries under 5ms are omitted."," The specification only lists scripts above a small duration threshold, so a frame built from hundreds of tiny callbacks may report a long duration with few scripts. That shape — death by a thousand microtasks — usually indicates a reactive store notifying too many subscribers.",[15,1376,1377,1380,1381,1384],{},[232,1378,1379],{},"Extension noise."," Browser extensions inject content scripts that appear in lab LoAF data with ",[33,1382,1383],{},"chrome-extension:\u002F\u002F"," URLs. Filter them out of RUM aggregation, but do not ignore them in support tickets: a slow extension is a real user's real INP.",[215,1386,1388],{"id":1387},"validation-and-budgeting","Validation and Budgeting",[15,1390,1391],{},"Lock the fix in at two levels. In the lab, assert that no interactive frame in a scripted interaction exceeds a budget, using Puppeteer to drive the interaction and read entries from the page:",[359,1393,1395],{"className":361,"code":1394,"language":363,"meta":364,"style":364},"\u002F\u002F ci\u002Fassert-loaf.mjs — fail the build if the scripted interaction produces a long frame.\nconst worst = await page.evaluate(() => new Promise((resolve) => {\n  const seen = [];\n  new PerformanceObserver((l) => seen.push(...l.getEntries()))\n    .observe({ type: 'long-animation-frame', buffered: true });\n  document.querySelector('#apply-filters').click();\n  setTimeout(() => resolve(Math.max(0, ...seen\n    .filter((f) => f.firstUIEventTimestamp > 0).map((f) => f.duration))), 1000);\n}));\nif (worst > 100) throw new Error(`interactive frame ${Math.round(worst)}ms > 100ms budget`);\n\u002F\u002F trade-off: CI hardware is faster than a mid-tier phone, so a 100ms lab budget\n\u002F\u002F roughly corresponds to a 200ms+ field frame. Calibrate the threshold against\n\u002F\u002F your own RUM rather than copying this number.\n",[33,1396,1397,1402,1442,1454,1489,1506,1527,1556,1596,1601,1649,1654,1659],{"__ignoreMap":364},[368,1398,1399],{"class":98,"line":370},[368,1400,1401],{"class":373},"\u002F\u002F ci\u002Fassert-loaf.mjs — fail the build if the scripted interaction produces a long frame.\n",[368,1403,1404,1406,1409,1411,1414,1417,1420,1423,1425,1428,1431,1433,1436,1438,1440],{"class":98,"line":377},[368,1405,381],{"class":380},[368,1407,1408],{"class":384}," worst",[368,1410,388],{"class":380},[368,1412,1413],{"class":380}," await",[368,1415,1416],{"class":391}," page.",[368,1418,1419],{"class":401},"evaluate",[368,1421,1422],{"class":391},"(() ",[368,1424,415],{"class":380},[368,1426,1427],{"class":380}," new",[368,1429,1430],{"class":384}," Promise",[368,1432,405],{"class":391},[368,1434,1435],{"class":408},"resolve",[368,1437,412],{"class":391},[368,1439,415],{"class":380},[368,1441,418],{"class":391},[368,1443,1444,1447,1450,1452],{"class":98,"line":395},[368,1445,1446],{"class":380},"  const",[368,1448,1449],{"class":384}," seen",[368,1451,388],{"class":380},[368,1453,392],{"class":391},[368,1455,1456,1459,1461,1463,1466,1468,1470,1473,1475,1478,1481,1484,1486],{"class":98,"line":421},[368,1457,1458],{"class":380},"  new",[368,1460,402],{"class":401},[368,1462,405],{"class":391},[368,1464,1465],{"class":408},"l",[368,1467,412],{"class":391},[368,1469,415],{"class":380},[368,1471,1472],{"class":391}," seen.",[368,1474,478],{"class":401},[368,1476,1477],{"class":391},"(",[368,1479,1480],{"class":380},"...",[368,1482,1483],{"class":391},"l.",[368,1485,440],{"class":401},[368,1487,1488],{"class":391},"()))\n",[368,1490,1491,1494,1496,1498,1500,1502,1504],{"class":98,"line":446},[368,1492,1493],{"class":391},"    .",[368,1495,525],{"class":401},[368,1497,528],{"class":391},[368,1499,532],{"class":531},[368,1501,535],{"class":391},[368,1503,538],{"class":384},[368,1505,541],{"class":391},[368,1507,1508,1511,1514,1516,1519,1522,1524],{"class":98,"line":472},[368,1509,1510],{"class":391},"  document.",[368,1512,1513],{"class":401},"querySelector",[368,1515,1477],{"class":391},[368,1517,1518],{"class":531},"'#apply-filters'",[368,1520,1521],{"class":391},").",[368,1523,252],{"class":401},[368,1525,1526],{"class":391},"();\n",[368,1528,1529,1532,1534,1536,1539,1542,1545,1547,1549,1551,1553],{"class":98,"line":484},[368,1530,1531],{"class":401},"  setTimeout",[368,1533,1422],{"class":391},[368,1535,415],{"class":380},[368,1537,1538],{"class":401}," resolve",[368,1540,1541],{"class":391},"(Math.",[368,1543,1544],{"class":401},"max",[368,1546,1477],{"class":391},[368,1548,72],{"class":384},[368,1550,762],{"class":391},[368,1552,1480],{"class":380},[368,1554,1555],{"class":391},"seen\n",[368,1557,1558,1560,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580,1582,1584,1586,1588,1591,1594],{"class":98,"line":513},[368,1559,1493],{"class":391},[368,1561,600],{"class":401},[368,1563,405],{"class":391},[368,1565,605],{"class":408},[368,1567,412],{"class":391},[368,1569,415],{"class":380},[368,1571,612],{"class":391},[368,1573,615],{"class":380},[368,1575,618],{"class":384},[368,1577,1521],{"class":391},[368,1579,635],{"class":401},[368,1581,405],{"class":391},[368,1583,605],{"class":408},[368,1585,412],{"class":391},[368,1587,415],{"class":380},[368,1589,1590],{"class":391}," f.duration))), ",[368,1592,1593],{"class":384},"1000",[368,1595,621],{"class":391},[368,1597,1598],{"class":98,"line":519},[368,1599,1600],{"class":391},"}));\n",[368,1602,1603,1606,1609,1611,1614,1616,1619,1621,1624,1626,1629,1632,1634,1636,1638,1641,1644,1647],{"class":98,"line":544},[368,1604,1605],{"class":380},"if",[368,1607,1608],{"class":391}," (worst ",[368,1610,615],{"class":380},[368,1612,1613],{"class":384}," 100",[368,1615,412],{"class":391},[368,1617,1618],{"class":380},"throw",[368,1620,1427],{"class":380},[368,1622,1623],{"class":401}," Error",[368,1625,1477],{"class":391},[368,1627,1628],{"class":531},"`interactive frame ${",[368,1630,1631],{"class":391},"Math",[368,1633,789],{"class":531},[368,1635,654],{"class":401},[368,1637,1477],{"class":531},[368,1639,1640],{"class":391},"worst",[368,1642,1643],{"class":531},")",[368,1645,1646],{"class":531},"}ms > 100ms budget`",[368,1648,621],{"class":391},[368,1650,1651],{"class":98,"line":550},[368,1652,1653],{"class":373},"\u002F\u002F trade-off: CI hardware is faster than a mid-tier phone, so a 100ms lab budget\n",[368,1655,1656],{"class":98,"line":556},[368,1657,1658],{"class":373},"\u002F\u002F roughly corresponds to a 200ms+ field frame. Calibrate the threshold against\n",[368,1660,1661],{"class":98,"line":562},[368,1662,1663],{"class":373},"\u002F\u002F your own RUM rather than copying this number.\n",[15,1665,1666,1667,1671,1672,1674],{},"In the field, ship the top script entries for the INP interaction in your RUM beacon — the guide on ",[19,1668,1670],{"href":1669},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fsending-loaf-data-in-rum-beacons\u002F","sending LoAF data in RUM beacons"," covers the payload budget — and alert when the share of INP frames attributed to a single ",[33,1673,335],{}," jumps release-over-release.",[15,1676,1677],{},[59,1678,66,1681,66,1684,66,1687,66,1689,66,1696,66,1699,66,1701,66,1705,66,1709,66,1712,66,1716,66,1719,66,1723,66,1726,66,1729,66,1732,66,1735,66,1738,66,1741,66,1744,66,1748,66,1751,66,1754,66,1758,66,1761,66,1764,66,1770,66,1774,66,1778,66],{"viewBox":1679,"width":62,"role":63,"ariaLabel":1680,"style":65},"0 0 760 149","Five-stage loop from RUM capture of a long frame through grouping, lab reproduction and fix to a CI budget assertion.",[68,1682],{"className":1683,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],[75,1685,1686],{},"Closing the loop from field frame to fixed handler",[79,1688,1680],{},[999,1690,1691],{},[1002,1692,1694],{"id":1693,"viewBox":1005,"refX":1006,"refY":1007,"markerWidth":1008,"markerHeight":1008,"orient":1009},"faa8d436cf",[1011,1695],{"d":1013,"fill":89,"style":1014},[68,1697],{"x":84,"y":84,"width":85,"height":1698,"rx":87,"fill":88,"stroke":89,"style":90},"147",[92,1700,1686],{"x":94,"y":95,"fill":89,"style":96},[68,1702],{"x":94,"y":811,"width":1703,"height":1704,"rx":1024,"fill":120,"stroke":120,"style":121},"108.8","71.0",[92,1706,1708],{"x":1707,"y":116,"fill":89,"style":1028,"textAnchor":127},"82.4","RUM beacon",[92,1710,1711],{"x":1707,"y":110,"fill":89,"style":846,"textAnchor":127},"top 3 scripts for",[92,1713,1715],{"x":1707,"y":1714,"fill":89,"style":846,"textAnchor":127},"112.0","INP",[68,1717],{"x":1718,"y":811,"width":1703,"height":1704,"rx":1024,"fill":142,"stroke":103,"style":143},"176.8",[92,1720,1722],{"x":1721,"y":116,"fill":89,"style":1028,"textAnchor":127},"231.2","Group",[92,1724,1725],{"x":1721,"y":110,"fill":89,"style":846,"textAnchor":127},"by sourceURL +",[92,1727,1728],{"x":1721,"y":1714,"fill":89,"style":846,"textAnchor":127},"function",[68,1730],{"x":1731,"y":811,"width":1703,"height":1704,"rx":1024,"fill":120,"stroke":120,"style":121},"325.6",[92,1733,1734],{"x":1046,"y":116,"fill":89,"style":1028,"textAnchor":127},"Reproduce",[92,1736,1737],{"x":1046,"y":110,"fill":89,"style":846,"textAnchor":127},"lab trace at 4x",[92,1739,1740],{"x":1046,"y":1714,"fill":89,"style":846,"textAnchor":127},"CPU",[68,1742],{"x":1743,"y":811,"width":1703,"height":1704,"rx":1024,"fill":120,"stroke":120,"style":121},"474.4",[92,1745,1747],{"x":1746,"y":116,"fill":89,"style":1028,"textAnchor":127},"528.8","Fix",[92,1749,1750],{"x":1746,"y":110,"fill":89,"style":846,"textAnchor":127},"yield",[68,1752],{"x":1753,"y":811,"width":1703,"height":1704,"rx":1024,"fill":120,"stroke":120,"style":121},"623.2",[92,1755,1757],{"x":1756,"y":116,"fill":89,"style":1028,"textAnchor":127},"677.6","CI budget",[92,1759,1760],{"x":1756,"y":110,"fill":89,"style":846,"textAnchor":127},"interactive frame",[92,1762,1763],{"x":1756,"y":1714,"fill":89,"style":846,"textAnchor":127},"cap",[98,1765],{"x1":1766,"y1":1767,"x2":1768,"y2":1767,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1769},"136.8","91.5","174.8","url(#faa8d436cf)",[98,1771],{"x1":1772,"y1":1767,"x2":1773,"y2":1767,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1769},"285.6","323.6",[98,1775],{"x1":1776,"y1":1767,"x2":1777,"y2":1767,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1769},"434.4","472.4",[98,1779],{"x1":1780,"y1":1767,"x2":1781,"y2":1767,"stroke":89,"strokeWidth":104,"style":1042,"markerEnd":1769},"583.2","621.2",[215,1783,1785],{"id":1784},"code-example-joining-loaf-to-the-inp-interaction","Code Example: Joining LoAF to the INP Interaction",[15,1787,309,1788,1790],{},[33,1789,312],{}," attribution build already does the correlation for you; this example shows the minimal useful extraction.",[359,1792,1794],{"className":361,"code":1793,"language":363,"meta":364,"style":364},"import { onINP } from 'web-vitals\u002Fattribution';\n\nonINP(({ value, attribution }) => {\n  const scripts = (attribution.longAnimationFrameEntries || [])\n    .flatMap((f) => f.scripts)\n    .sort((a, b) => b.duration - a.duration)\n    .slice(0, 3)\n    .map((s) => ({\n      src: s.sourceURL.split('?')[0],\n      fn: s.sourceFunctionName || '(anonymous)',\n      type: s.invokerType,\n      ms: Math.round(s.duration),\n      layout: Math.round(s.forcedStyleAndLayoutDuration),\n    }));\n  navigator.sendBeacon('\u002Frum', JSON.stringify({ inp: Math.round(value), scripts }));\n});\n\u002F\u002F trade-off: stripping query strings keeps cardinality manageable but merges\n\u002F\u002F distinct vendor endpoints that differ only by parameters. Keep the full URL\n\u002F\u002F for first-party scripts if your bundles are fingerprinted by query string.\n",[33,1795,1796,1813,1819,1842,1860,1878,1906,1924,1941,1962,1974,1979,1989,1999,2005,2037,2043,2049,2055],{"__ignoreMap":364},[368,1797,1798,1801,1804,1807,1810],{"class":98,"line":370},[368,1799,1800],{"class":380},"import",[368,1802,1803],{"class":391}," { onINP } ",[368,1805,1806],{"class":380},"from",[368,1808,1809],{"class":531}," 'web-vitals\u002Fattribution'",[368,1811,1812],{"class":391},";\n",[368,1814,1815],{"class":98,"line":377},[368,1816,1818],{"emptyLinePlaceholder":1817},true,"\n",[368,1820,1821,1824,1827,1830,1832,1835,1838,1840],{"class":98,"line":395},[368,1822,1823],{"class":401},"onINP",[368,1825,1826],{"class":391},"(({ ",[368,1828,1829],{"class":408},"value",[368,1831,762],{"class":391},[368,1833,1834],{"class":408},"attribution",[368,1836,1837],{"class":391}," }) ",[368,1839,415],{"class":380},[368,1841,418],{"class":391},[368,1843,1844,1846,1849,1851,1854,1857],{"class":98,"line":421},[368,1845,1446],{"class":380},[368,1847,1848],{"class":384}," scripts",[368,1850,388],{"class":380},[368,1852,1853],{"class":391}," (attribution.longAnimationFrameEntries ",[368,1855,1856],{"class":380},"||",[368,1858,1859],{"class":391}," [])\n",[368,1861,1862,1864,1867,1869,1871,1873,1875],{"class":98,"line":446},[368,1863,1493],{"class":391},[368,1865,1866],{"class":401},"flatMap",[368,1868,405],{"class":391},[368,1870,605],{"class":408},[368,1872,412],{"class":391},[368,1874,415],{"class":380},[368,1876,1877],{"class":391}," f.scripts)\n",[368,1879,1880,1882,1885,1887,1889,1891,1894,1896,1898,1901,1903],{"class":98,"line":472},[368,1881,1493],{"class":391},[368,1883,1884],{"class":401},"sort",[368,1886,405],{"class":391},[368,1888,19],{"class":408},[368,1890,762],{"class":391},[368,1892,1893],{"class":408},"b",[368,1895,412],{"class":391},[368,1897,415],{"class":380},[368,1899,1900],{"class":391}," b.duration ",[368,1902,706],{"class":380},[368,1904,1905],{"class":391}," a.duration)\n",[368,1907,1908,1910,1913,1915,1917,1919,1921],{"class":98,"line":484},[368,1909,1493],{"class":391},[368,1911,1912],{"class":401},"slice",[368,1914,1477],{"class":391},[368,1916,72],{"class":384},[368,1918,762],{"class":391},[368,1920,119],{"class":384},[368,1922,1923],{"class":391},")\n",[368,1925,1926,1928,1930,1932,1935,1937,1939],{"class":98,"line":513},[368,1927,1493],{"class":391},[368,1929,635],{"class":401},[368,1931,405],{"class":391},[368,1933,1934],{"class":408},"s",[368,1936,412],{"class":391},[368,1938,415],{"class":380},[368,1940,646],{"class":391},[368,1942,1943,1946,1949,1951,1954,1957,1959],{"class":98,"line":519},[368,1944,1945],{"class":391},"      src: s.sourceURL.",[368,1947,1948],{"class":401},"split",[368,1950,1477],{"class":391},[368,1952,1953],{"class":531},"'?'",[368,1955,1956],{"class":391},")[",[368,1958,72],{"class":384},[368,1960,1961],{"class":391},"],\n",[368,1963,1964,1967,1969,1972],{"class":98,"line":544},[368,1965,1966],{"class":391},"      fn: s.sourceFunctionName ",[368,1968,1856],{"class":380},[368,1970,1971],{"class":531}," '(anonymous)'",[368,1973,687],{"class":391},[368,1975,1976],{"class":98,"line":550},[368,1977,1978],{"class":391},"      type: s.invokerType,\n",[368,1980,1981,1984,1986],{"class":98,"line":556},[368,1982,1983],{"class":391},"      ms: Math.",[368,1985,654],{"class":401},[368,1987,1988],{"class":391},"(s.duration),\n",[368,1990,1991,1994,1996],{"class":98,"line":562},[368,1992,1993],{"class":391},"      layout: Math.",[368,1995,654],{"class":401},[368,1997,1998],{"class":391},"(s.forcedStyleAndLayoutDuration),\n",[368,2000,2002],{"class":98,"line":2001},14,[368,2003,2004],{"class":391},"    }));\n",[368,2006,2008,2011,2014,2016,2019,2021,2024,2026,2029,2032,2034],{"class":98,"line":2007},15,[368,2009,2010],{"class":391},"  navigator.",[368,2012,2013],{"class":401},"sendBeacon",[368,2015,1477],{"class":391},[368,2017,2018],{"class":531},"'\u002Frum'",[368,2020,762],{"class":391},[368,2022,2023],{"class":384},"JSON",[368,2025,789],{"class":391},[368,2027,2028],{"class":401},"stringify",[368,2030,2031],{"class":391},"({ inp: Math.",[368,2033,654],{"class":401},[368,2035,2036],{"class":391},"(value), scripts }));\n",[368,2038,2040],{"class":98,"line":2039},16,[368,2041,2042],{"class":391},"});\n",[368,2044,2046],{"class":98,"line":2045},17,[368,2047,2048],{"class":373},"\u002F\u002F trade-off: stripping query strings keeps cardinality manageable but merges\n",[368,2050,2052],{"class":98,"line":2051},18,[368,2053,2054],{"class":373},"\u002F\u002F distinct vendor endpoints that differ only by parameters. Keep the full URL\n",[368,2056,2058],{"class":98,"line":2057},19,[368,2059,2060],{"class":373},"\u002F\u002F for first-party scripts if your bundles are fingerprinted by query string.\n",[215,2062,2064],{"id":2063},"patterns-that-recur-in-production-loaf-data","Patterns That Recur in Production LoAF Data",[15,2066,2067],{},"Once LoAF attribution has been flowing for a week, the same handful of shapes account for most failing interactions. Recognising them by their signature saves a reproduction cycle.",[15,2069,2070,2073,2074,2076,2077,2079,2080,751,2082,786,2084,2086],{},[232,2071,2072],{},"The tag-manager pile-up."," The frame's slowest script has a ",[33,2075,335],{}," on a tag-manager host and an ",[33,2078,758],{}," of ",[33,2081,771],{},[33,2083,765],{},[33,2085,579],{}," falls inside it. The user tapped while a container was evaluating a dozen tags queued by a consent change or a page-view trigger. The fix is upstream: fire those tags from an idle callback rather than synchronously on the trigger, or move them server-side. Nothing in your own handler will help.",[15,2088,2089,2092,2093,2096,2097,2101],{},[232,2090,2091],{},"The hydration collision."," On the first interaction after load, the slowest script is a framework chunk with an invoker such as ",[33,2094,2095],{},"Window.requestIdleCallback"," or a module script evaluation, and input delay is large while processing is small. The interaction arrived mid-hydration. You either hydrate less (",[19,2098,2100],{"href":2099},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fselective-hydration-with-react-suspense\u002F","selective hydration with React Suspense"," and islands are the main tools) or make hydration yield so the input can slip in between chunks.",[15,2103,2104,2107,2108,2110,2111,2114],{},[232,2105,2106],{},"The reactive fan-out."," Many scripts of a few milliseconds each, all from the same store or signals library, with a total duration above ",[33,2109,187],{},". One state change notified hundreds of subscribers. Narrow the subscription (selectors, ",[33,2112,2113],{},"shallowRef",", memoised derived state) rather than speeding up any one subscriber.",[15,2116,2117,2120,2121,2123,2124,751,2127,2130,2131,789],{},[232,2118,2119],{},"The layout ping-pong."," A modest script duration with a ",[33,2122,270],{}," making up most of it — say 70ms of an 85ms listener. Some loop reads ",[33,2125,2126],{},"offsetHeight",[33,2128,2129],{},"getBoundingClientRect()"," after writing a class. The fix is mechanical: read everything first, then write, or move the measurement into a ",[33,2132,2133],{},"ResizeObserver",[15,2135,2136,2139,2140,2143,2144,2147,2148,2151],{},[232,2137,2138],{},"The silent renderer."," Script entries total under ",[33,2141,2142],{},"30ms"," but the frame is ",[33,2145,2146],{},"180ms"," because the render tail is huge. The DOM change was legitimately small in code and enormous in effect — toggling a class on ",[33,2149,2150],{},"\u003Cbody>"," that every rule in a 400KB stylesheet depends on, or inserting 2,000 table rows. Containment and virtualisation, not JavaScript, move this number.",[15,2153,2154],{},[59,2155,66,2157,66,2160,66,2163,66,2165,66,2167,66,2169,66,2172,66,2175,66,2179,66,2183,66,2186,66,2190,66,2192,66,2195,66,2197,66,2200,66,2202,66,2205,66,2207,66,2210,66,2212,66,2215,66,2217,66,2220,66,2222,66,2225,66,2227,66,2230,66,2232,66,2235,66,2237,66,2240,66,2242,66,2245,66,2247,66,2250,66,2252,66,2255,66,2257,66,2260,66,2262,66],{"viewBox":794,"width":62,"role":63,"ariaLabel":2156,"style":65},"A comparison of five common long animation frame shapes, the field that identifies each, and the fix it points to.",[68,2158],{"className":2159,"x":72,"y":72,"width":62,"height":62,"fill":73},[71],[75,2161,2162],{},"Five recurring long-frame signatures",[79,2164,2156],{},[68,2166],{"x":84,"y":84,"width":85,"height":806,"rx":87,"fill":88,"stroke":89,"style":90},[92,2168,2162],{"x":94,"y":95,"fill":89,"style":96},[68,2170],{"x":94,"y":811,"width":2171,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"139.5",[92,2173,2174],{"x":816,"y":817,"fill":89,"style":111,"textAnchor":818},"Signature",[68,2176],{"x":2177,"y":811,"width":2178,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"167.5","282.3",[92,2180,2182],{"x":2181,"y":817,"fill":89,"style":111,"textAnchor":127},"308.6","Tell-tale field",[68,2184],{"x":2185,"y":811,"width":2178,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},"449.7",[92,2187,2189],{"x":2188,"y":817,"fill":89,"style":111,"textAnchor":127},"590.9","Fix direction",[68,2191],{"x":94,"y":837,"width":2171,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2193,2194],{"x":816,"y":841,"fill":89,"style":111,"textAnchor":818},"Tag-manager pile-up",[68,2196],{"x":2177,"y":837,"width":2178,"height":813,"rx":72,"fill":142,"stroke":103,"style":143},[92,2198,2199],{"x":2181,"y":841,"fill":89,"style":846,"textAnchor":127},"vendor sourceURL at input time",[68,2201],{"x":2185,"y":837,"width":2178,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2203,2204],{"x":2188,"y":841,"fill":89,"style":846,"textAnchor":127},"Defer tags to idle",[68,2206],{"x":94,"y":155,"width":2171,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2208,2209],{"x":816,"y":857,"fill":89,"style":111,"textAnchor":818},"Hydration collision",[68,2211],{"x":2177,"y":155,"width":2178,"height":813,"rx":72,"fill":142,"stroke":103,"style":143},[92,2213,2214],{"x":2181,"y":857,"fill":89,"style":846,"textAnchor":127},"framework chunk, high input delay",[68,2216],{"x":2185,"y":155,"width":2178,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2218,2219],{"x":2188,"y":857,"fill":89,"style":846,"textAnchor":127},"Hydrate less or in chunks",[68,2221],{"x":94,"y":870,"width":2171,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2223,2224],{"x":816,"y":873,"fill":89,"style":111,"textAnchor":818},"Reactive fan-out",[68,2226],{"x":2177,"y":870,"width":2178,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},[92,2228,2229],{"x":2181,"y":873,"fill":89,"style":846,"textAnchor":127},"many tiny scripts, one library",[68,2231],{"x":2185,"y":870,"width":2178,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2233,2234],{"x":2188,"y":873,"fill":89,"style":846,"textAnchor":127},"Narrow subscriptions",[68,2236],{"x":94,"y":887,"width":2171,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2238,2239],{"x":816,"y":890,"fill":89,"style":111,"textAnchor":818},"Layout ping-pong",[68,2241],{"x":2177,"y":887,"width":2178,"height":813,"rx":72,"fill":142,"stroke":103,"style":143},[92,2243,2244],{"x":2181,"y":890,"fill":89,"style":846,"textAnchor":127},"forced layout most of duration",[68,2246],{"x":2185,"y":887,"width":2178,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2248,2249],{"x":2188,"y":890,"fill":89,"style":846,"textAnchor":127},"Read then write",[68,2251],{"x":94,"y":904,"width":2171,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2253,2254],{"x":816,"y":907,"fill":89,"style":111,"textAnchor":818},"Silent renderer",[68,2256],{"x":2177,"y":904,"width":2178,"height":813,"rx":72,"fill":89,"stroke":89,"style":133},[92,2258,2259],{"x":2181,"y":907,"fill":89,"style":846,"textAnchor":127},"short scripts, long render tail",[68,2261],{"x":2185,"y":904,"width":2178,"height":813,"rx":72,"fill":88,"stroke":89,"style":838},[92,2263,2264],{"x":2188,"y":907,"fill":89,"style":846,"textAnchor":127},"Contain or virtualise",[215,2266,2268],{"id":2267},"faq","FAQ",[2270,2271,2274,2278],"details",{"className":2272},[2273],"faq-item",[2275,2276,2277],"summary",{},"Does LoAF replace the Event Timing API for INP?",[15,2279,2280,2281,2284,2285,2287],{},"No. Event Timing still defines which interaction is the INP candidate and how long it took end to end. LoAF explains ",[25,2282,2283],{},"why"," the frame containing that interaction was slow. You need both: Event Timing to choose the interaction worth investigating, LoAF to attribute its cost. The ",[33,2286,312],{}," attribution build joins the two for you by matching the interaction's timestamps against the frames that overlap it.",[2270,2289,2291,2294],{"className":2290},[2273],[2275,2292,2293],{},"Is the overhead of observing every long frame acceptable in production?",[15,2295,2296,2297,2299],{},"Observing is cheap — the browser records the entries whether or not you listen — but what you do in the callback is not free. Filtering to frames above ",[33,2298,183],{},", keeping a bounded buffer and doing the serialisation only when you actually send a beacon keeps the cost to a fraction of a millisecond per frame. The expensive mistake is stringifying every entry eagerly or posting each one as it arrives.",[2270,2301,2303,2306],{"className":2302},[2273],[2275,2304,2305],{},"Why does my LoAF total not match the INP value?",[15,2307,2308,2309,2311],{},"They measure different spans. INP starts at the hardware input timestamp and ends at the next paint; a LoAF entry starts when the frame's first task begins and ends after rendering. If the input arrived during a frame that started earlier, LoAF duration can exceed INP; if the interaction spanned two frames (a listener that scheduled a second frame via ",[33,2310,1203],{},"), INP can exceed any single entry. Compare phases, not totals.",[215,2313,2315],{"id":2314},"guides-in-this-topic","Guides in This Topic",[226,2317,2318,2324,2331,2337],{},[229,2319,2320,2323],{},[19,2321,2322],{"href":930},"Finding slow scripts with LoAF script attribution"," — turn a list of script entries into a ranked suspect list.",[229,2325,2326,2330],{},[19,2327,2329],{"href":2328},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Floaf-vs-long-tasks-api\u002F","LoAF vs the Long Tasks API"," — what changed, and whether to keep the old observer.",[229,2332,2333,2336],{},[19,2334,2335],{"href":1669},"Sending LoAF data in RUM beacons"," — attribution without blowing the beacon size budget.",[229,2338,2339,2342],{},[19,2340,2341],{"href":982},"Diagnosing presentation delay with LoAF"," — the render tail your scripts do not show.",[215,2344,2346],{"id":2345},"related","Related",[226,2348,2349,2356,2363,2370],{},[229,2350,2351,2355],{},[19,2352,2354],{"href":2353},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F","Profiling event handlers for INP"," — the DevTools workflow LoAF data feeds into.",[229,2357,2358,2362],{},[19,2359,2361],{"href":2360},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fattributing-inp-to-a-component-with-the-attribution-build\u002F","Attributing INP to a component with the attribution build"," — mapping phases to owners in the field.",[229,2364,2365,2369],{},[19,2366,2368],{"href":2367},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F","RUM beacons and field data collection"," — the pipeline that carries LoAF attribution home.",[229,2371,2372,2376],{},[19,2373,2375],{"href":2374},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002F","Soft navigations and SPA metrics"," — assigning long frames to the route that caused them.",[2378,2379,2381],"script",{"type":2380},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Long Animation Frames (LoAF): Attributing Slow Frames to the Script That Caused Them\",\n  \"description\": \"Attribute slow frames to the script, function and rendering phase that pushed an interaction past the 200ms INP budget.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Environment Setup: Observe Long Frames Without Distorting Them\",\n      \"text\": \"Register a single PerformanceObserver with buffered: true as early as possible so frames that happened before your observer existed are still delivered.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Capture a Baseline: Which Frames Overlap Interactions?\",\n      \"text\": \"Not every long frame matters.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Isolate the Bottleneck: Read the Script Entries\",\n      \"text\": \"Each entry's scripts array is the heart of the API.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Apply the Fix Matched to the Phase\",\n      \"text\": \"LoAF does not fix anything; it routes you to the right existing technique:\"\n    }\n  ]\n}\n",[2378,2383,2384],{"type":2380},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Long Animation Frames (LoAF): Attributing Slow Frames to the Script That Caused Them\",\n  \"description\": \"Attribute slow frames to the script, function and rendering phase that pushed an interaction past the 200ms INP budget.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F\"\n  }\n}\n",[2378,2386,2387],{"type":2380},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Long Animation Frames (LoAF) API\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F\"\n    }\n  ]\n}\n",[2389,2390,2391],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--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);}",{"title":364,"searchDepth":377,"depth":377,"links":2393},[2394,2395,2396,2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407],{"id":217,"depth":377,"text":218},{"id":291,"depth":377,"text":292},{"id":345,"depth":377,"text":346},{"id":568,"depth":377,"text":569},{"id":735,"depth":377,"text":736},{"id":935,"depth":377,"text":936},{"id":1117,"depth":377,"text":1118},{"id":1309,"depth":377,"text":1310},{"id":1387,"depth":377,"text":1388},{"id":1784,"depth":377,"text":1785},{"id":2063,"depth":377,"text":2064},{"id":2267,"depth":377,"text":2268},{"id":2314,"depth":377,"text":2315},{"id":2345,"depth":377,"text":2346},"Attribute slow frames to the script, function and rendering phase that pushed an interaction past the 200ms INP budget.","md",{"slug":2411,"type":2412,"breadcrumb":2413,"datePublished":2420,"dateModified":2420},"long-animation-frames-api","topic",[2414,2417,2418],{"name":2415,"url":2416},"Home","\u002F",{"name":22,"url":21},{"name":5,"url":2419},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api",{"title":2423,"description":2424},"Long Animation Frames (LoAF) API for INP Debugging","Use the Long Animation Frames API to attribute slow frames to the exact script, function and phase that pushed INP past 200ms — in the lab and in the field.","core-web-vitals-measurement\u002Flong-animation-frames-api\u002Findex","8lSYR7ctIymkBxNWrL-CyU1uQHvdasZ8Mk9CjJs2_uM",[2428,2432],{"title":2429,"path":2430,"stem":2431,"children":-1},"Variable Fonts vs Static Font Weights","\u002Fcore-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Fvariable-fonts-vs-static-font-weights","core-web-vitals-measurement\u002Ffont-loading-and-text-rendering-performance\u002Fvariable-fonts-vs-static-font-weights\u002Findex",{"title":2433,"path":2434,"stem":2435,"children":-1},"Diagnosing Presentation Delay with LoAF","\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf","core-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002Findex",1791308070241]