[{"data":1,"prerenderedAt":1303},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf":1295},{"id":4,"title":5,"body":6,"description":1274,"extension":1275,"meta":1276,"navigation":732,"path":1289,"seo":1290,"stem":1293,"__hash__":1294},"content\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002Findex.md","Diagnosing Presentation Delay with LoAF",{"type":7,"value":8,"toc":1260},"minimark",[9,14,29,50,201,206,266,270,284,290,300,312,426,430,435,438,667,674,678,681,788,791,795,882,897,901,904,1021,1024,1106,1110,1125,1129,1135,1149,1159,1180,1190,1194,1206,1218,1222,1245,1250,1253,1256],[10,11,13],"h1",{"id":12},"how-to-fix-inp-when-presentation-delay-not-your-handler-is-the-problem","How to Fix INP When Presentation Delay, Not Your Handler, Is the Problem",[15,16,17,18,23,24,28],"p",{},"This guide uses the rendering timestamps of the ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F","Long Animation Frames API"," to diagnose the third and least understood phase of INP, within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",".",[15,30,31,32,36,37,40,41,44,45,49],{},"The scenario: your INP breakdown says input delay is ",[33,34,35],"code",{},"12ms",", processing is ",[33,38,39],{},"35ms",", and yet the interaction totals ",[33,42,43],{},"240ms",". Almost 200ms is ",[46,47,48],"em",{},"presentation delay"," — the time from your last listener returning to the browser painting the result. Yielding inside the handler cannot help because the handler is already done. The cost is in style recalculation, layout, paint and compositing for whatever your handler changed, and LoAF is the only field API that splits that tail apart.",[15,51,52],{},[53,54,60,61,60,68,60,72,60,75,60,84,60,90,60,101,60,106,60,114,60,120,60,127,60,133,60,136,60,140,60,144,60,149,60,153,60,157,60,161,60,165,60,170,60,176,60,180,60,184,60,188,60,191,60,195,60],"svg",{"viewBox":55,"width":56,"role":57,"ariaLabel":58,"style":59},"0 0 760 200","100%","img","Timeline of an interaction where the handler finishes at 47 milliseconds but style, layout and paint push the next frame to 240 milliseconds.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[62,63],"rect",{"className":64,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],"svg-canvas","0","#ffffff",[69,70,71],"title",{},"A fast handler with a slow render tail",[73,74,58],"desc",{},[62,76],{"x":77,"y":77,"width":78,"height":79,"rx":80,"fill":81,"stroke":82,"style":83},"1","758","198","10","none","currentColor","stroke-opacity:0.18",[85,86,71],"text",{"x":87,"y":88,"fill":82,"style":89},"28.0","34.0","font-size:16px;font-weight:700",[91,92],"line",{"x1":93,"y1":94,"x2":93,"y2":95,"stroke":96,"strokeWidth":97,"strokeDashArray":98,"style":100},"601.8","72.0","156.0","#b8860b","1.5",[99,99],"4","stroke-opacity:0.9",[85,102,105],{"x":87,"y":103,"fill":82,"style":104},"96.0","font-size:12.5px;font-weight:700","Interaction",[62,107],{"x":108,"y":109,"width":110,"height":111,"rx":112,"fill":82,"stroke":82,"style":113},"105.8","78.0","28.3","26.0","3","fill-opacity:0.06;stroke-opacity:0.4",[62,115],{"x":116,"y":109,"width":117,"height":111,"rx":112,"fill":118,"stroke":118,"style":119},"135.6","85.4","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[85,121,126],{"x":122,"y":123,"fill":82,"style":124,"textAnchor":125},"178.3","95.5","font-size:11.5px","middle","handler",[62,128],{"x":129,"y":109,"width":130,"height":111,"rx":112,"fill":131,"stroke":96,"style":132},"222.5","477.9","#ffc300","fill-opacity:0.24;stroke-opacity:0.9",[85,134,48],{"x":135,"y":123,"fill":82,"style":124,"textAnchor":125},"461.5",[85,137,139],{"x":87,"y":138,"fill":82,"style":104},"134.0","LoAF tail",[62,141],{"x":129,"y":142,"width":143,"height":111,"rx":112,"fill":82,"stroke":82,"style":113},"116.0","30.8",[85,145,148],{"x":146,"y":147,"fill":82,"style":124,"textAnchor":125},"237.9","133.5","rAF",[62,150],{"x":151,"y":142,"width":152,"height":111,"rx":112,"fill":118,"stroke":118,"style":119},"254.8","271.7",[85,154,156],{"x":155,"y":147,"fill":82,"style":124,"textAnchor":125},"390.7","style + layout",[62,158],{"x":159,"y":142,"width":160,"height":111,"rx":112,"fill":82,"stroke":82,"style":113},"528.0","172.4",[85,162,164],{"x":163,"y":147,"fill":82,"style":124,"textAnchor":125},"614.2","paint + composite",[91,166],{"x1":167,"y1":95,"x2":168,"y2":95,"stroke":82,"style":169},"105.0","726.0","stroke-opacity:0.4",[85,171,175],{"x":172,"y":173,"fill":174,"style":124,"textAnchor":125},"115.7","173.0","#51617a","0ms",[85,177,179],{"x":178,"y":173,"fill":174,"style":124,"textAnchor":125},"229.2","50ms",[85,181,183],{"x":182,"y":173,"fill":174,"style":124,"textAnchor":125},"353.4","100ms",[85,185,187],{"x":186,"y":173,"fill":174,"style":124,"textAnchor":125},"477.6","150ms",[85,189,190],{"x":93,"y":173,"fill":174,"style":124,"textAnchor":125},"200ms",[85,192,194],{"x":193,"y":173,"fill":174,"style":124,"textAnchor":125},"715.3","250ms",[85,196,200],{"x":93,"y":197,"fill":198,"style":199,"textAnchor":125},"68.0","#8a6608","font-size:12px;font-weight:700","INP 200ms",[202,203,205],"h2",{"id":204},"rapid-diagnosis","Rapid Diagnosis",[207,208,209,227,248,254,260],"ul",{},[210,211,212,216,217,220,221,223,224,226],"li",{},[213,214,215],"strong",{},"Compute the render tail."," For the INP frame, ",[33,218,219],{},"startTime + duration − renderStart"," is time spent rendering after scripts. Over ",[33,222,179],{}," is a problem; over ",[33,225,183],{}," is the problem.",[210,228,229,60,232,235,236,239,240,243,244,247],{},[213,230,231],{},"Split style\u002Flayout from the rest.",[33,233,234],{},"renderStart"," marks the start of the rendering phase and ",[33,237,238],{},"styleAndLayoutStart"," comes after ",[33,241,242],{},"requestAnimationFrame"," callbacks, so ",[33,245,246],{},"styleAndLayoutStart − renderStart"," is rAF work and the remainder of the frame is style, layout and paint.",[210,249,250,253],{},[213,251,252],{},"Check the Performance panel's \"Recalculate Style\" event."," Its \"Elements affected\" count tells you whether the change touched 40 nodes or 40,000.",[210,255,256,259],{},[213,257,258],{},"Look for \"Layout\" with a large node count"," and \"Paint\" events covering the whole viewport after a small UI change.",[210,261,262,265],{},[213,263,264],{},"Toggle Rendering → Paint flashing"," in DevTools and repeat the interaction. Whole-screen green flashes mean the paint invalidation was far larger than the visual change.",[202,267,269],{"id":268},"root-cause-analysis","Root Cause Analysis",[15,271,272,275,276,279,280,283],{},[213,273,274],{},"1. Broad style invalidation."," Toggling a class on ",[33,277,278],{},"\u003Chtml>"," or ",[33,281,282],{},"\u003Cbody>"," (a theme switch, a \"menu open\" state) forces the engine to re-match selectors for every element under it. With descendant selectors depending on that class, style recalculation scales with DOM size rather than the size of the change.",[15,285,286,289],{},[213,287,288],{},"2. Unbounded layout."," Inserting content into a flow without containment can force layout for the whole document — every following sibling moves. A filter that swaps 300 product cards lays out the page, the footer, and anything positioned relative to them.",[15,291,292,295,296,299],{},[213,293,294],{},"3. Large paint and raster areas."," A change that triggers repaint of a full-viewport element with box-shadows, gradients, or ",[33,297,298],{},"filter: blur()"," is expensive on low-end GPUs. The script is cheap; the pixels are not.",[15,301,302,308,309,311],{},[213,303,304,305,307],{},"4. Synchronous ",[33,306,242],{}," work."," Code scheduled with ",[33,310,242],{}," runs inside the rendering phase of the same frame, before style. Analytics or measurement code that \"politely\" waits for rAF still delays the paint.",[15,313,314],{},[53,315,60,318,60,321,60,324,60,326,60,329,60,331,60,336,60,342,60,346,60,350,60,353,60,357,60,361,60,365,60,367,60,371,60,373,60,376,60,378,60,382,60,384,60,387,60,389,60,392,60,395,60,399,60,401,60,404,60,406,60,409,60,412,60,416,60,418,60,421,60,423,60],{"viewBox":316,"width":56,"role":57,"ariaLabel":317,"style":59},"0 0 760 228","Four causes of presentation delay with the DevTools signal that identifies each and the CSS or JavaScript fix.",[62,319],{"className":320,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],[69,322,323],{},"Render-tail cause to signal and fix",[73,325,317],{},[62,327],{"x":77,"y":77,"width":78,"height":328,"rx":80,"fill":81,"stroke":82,"style":83},"226",[85,330,323],{"x":87,"y":88,"fill":82,"style":89},[62,332],{"x":87,"y":333,"width":334,"height":335,"rx":66,"fill":82,"stroke":82,"style":113},"56.0","155.2","30.0",[85,337,341],{"x":338,"y":339,"fill":82,"style":104,"textAnchor":340},"38.0","75.5","start","Cause",[62,343],{"x":344,"y":333,"width":345,"height":335,"rx":66,"fill":82,"stroke":82,"style":113},"183.2","274.4",[85,347,349],{"x":348,"y":339,"fill":82,"style":104,"textAnchor":125},"320.4","DevTools signal",[62,351],{"x":352,"y":333,"width":345,"height":335,"rx":66,"fill":82,"stroke":82,"style":113},"457.6",[85,354,356],{"x":355,"y":339,"fill":82,"style":104,"textAnchor":125},"594.8","Fix",[62,358],{"x":87,"y":359,"width":334,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},"86.0","stroke-opacity:0.35",[85,362,364],{"x":338,"y":363,"fill":82,"style":104,"textAnchor":340},"105.5","Broad style invalidation",[62,366],{"x":344,"y":359,"width":345,"height":335,"rx":66,"fill":131,"stroke":96,"style":132},[85,368,370],{"x":348,"y":363,"fill":82,"style":369,"textAnchor":125},"font-size:12px","Recalculate Style: 10k+ elements",[62,372],{"x":352,"y":359,"width":345,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},[85,374,375],{"x":355,"y":363,"fill":82,"style":369,"textAnchor":125},"Scope the class lower in the tree",[62,377],{"x":87,"y":142,"width":334,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},[85,379,381],{"x":338,"y":380,"fill":82,"style":104,"textAnchor":340},"135.5","Unbounded layout",[62,383],{"x":344,"y":142,"width":345,"height":335,"rx":66,"fill":131,"stroke":96,"style":132},[85,385,386],{"x":348,"y":380,"fill":82,"style":369,"textAnchor":125},"Layout root is #document",[62,388],{"x":352,"y":142,"width":345,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},[85,390,391],{"x":355,"y":380,"fill":82,"style":369,"textAnchor":125},"contain: layout or content-visibility",[62,393],{"x":87,"y":394,"width":334,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},"146.0",[85,396,398],{"x":338,"y":397,"fill":82,"style":104,"textAnchor":340},"165.5","Large paint area",[62,400],{"x":344,"y":394,"width":345,"height":335,"rx":66,"fill":82,"stroke":82,"style":113},[85,402,403],{"x":348,"y":397,"fill":82,"style":369,"textAnchor":125},"full-screen paint flashing",[62,405],{"x":352,"y":394,"width":345,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},[85,407,408],{"x":355,"y":397,"fill":82,"style":369,"textAnchor":125},"Promote or simplify the effect",[62,410],{"x":87,"y":411,"width":334,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},"176.0",[85,413,415],{"x":338,"y":414,"fill":82,"style":104,"textAnchor":340},"195.5","rAF work in the frame",[62,417],{"x":344,"y":411,"width":345,"height":335,"rx":66,"fill":82,"stroke":82,"style":113},[85,419,420],{"x":348,"y":414,"fill":82,"style":369,"textAnchor":125},"Animation Frame Fired before style",[62,422],{"x":352,"y":411,"width":345,"height":335,"rx":66,"fill":81,"stroke":82,"style":360},[85,424,425],{"x":355,"y":414,"fill":82,"style":369,"textAnchor":125},"Move to post-paint callback",[202,427,429],{"id":428},"step-by-step-resolution","Step-by-Step Resolution",[431,432,434],"h3",{"id":433},"_1-measure-the-tail-per-interaction-target-in-the-field","1. Measure the tail per interaction target in the field",[15,436,437],{},"Extend your INP beacon with the render-tail split so you can find which targets suffer from rendering cost at scale.",[439,440,445],"pre",{"className":441,"code":442,"language":443,"meta":444,"style":444},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { onINP } from 'web-vitals\u002Fattribution';\nonINP(({ value, attribution }) => {\n  const f = attribution.longAnimationFrameEntries?.at(-1);\n  if (!f) return;\n  const end = f.startTime + f.duration;\n  report({\n    inp: Math.round(value),\n    target: attribution.interactionTarget,\n    rafMs: Math.round(Math.max(0, f.styleAndLayoutStart - f.renderStart)),\n    renderMs: Math.round(end - f.styleAndLayoutStart),\n  });\n});\n\u002F\u002F trade-off: using the LAST overlapping frame assumes the interaction's paint\n\u002F\u002F happens there. Interactions that schedule a second frame (rAF → rAF) need\n\u002F\u002F the frame containing the presentation timestamp instead.\n","javascript","",[33,446,447,469,498,528,548,567,576,588,594,620,636,642,648,655,661],{"__ignoreMap":444},[448,449,451,455,459,462,466],"span",{"class":91,"line":450},1,[448,452,454],{"class":453},"sPARh","import",[448,456,458],{"class":457},"saISM"," { onINP } ",[448,460,461],{"class":453},"from",[448,463,465],{"class":464},"sZ8jY"," 'web-vitals\u002Fattribution'",[448,467,468],{"class":457},";\n",[448,470,472,476,479,483,486,489,492,495],{"class":91,"line":471},2,[448,473,475],{"class":474},"smZ65","onINP",[448,477,478],{"class":457},"(({ ",[448,480,482],{"class":481},"sQw3B","value",[448,484,485],{"class":457},", ",[448,487,488],{"class":481},"attribution",[448,490,491],{"class":457}," }) ",[448,493,494],{"class":453},"=>",[448,496,497],{"class":457}," {\n",[448,499,501,504,508,511,514,517,520,523,525],{"class":91,"line":500},3,[448,502,503],{"class":453},"  const",[448,505,507],{"class":506},"sPXB4"," f",[448,509,510],{"class":453}," =",[448,512,513],{"class":457}," attribution.longAnimationFrameEntries?.",[448,515,516],{"class":474},"at",[448,518,519],{"class":457},"(",[448,521,522],{"class":453},"-",[448,524,77],{"class":506},[448,526,527],{"class":457},");\n",[448,529,531,534,537,540,543,546],{"class":91,"line":530},4,[448,532,533],{"class":453},"  if",[448,535,536],{"class":457}," (",[448,538,539],{"class":453},"!",[448,541,542],{"class":457},"f) ",[448,544,545],{"class":453},"return",[448,547,468],{"class":457},[448,549,551,553,556,558,561,564],{"class":91,"line":550},5,[448,552,503],{"class":453},[448,554,555],{"class":506}," end",[448,557,510],{"class":453},[448,559,560],{"class":457}," f.startTime ",[448,562,563],{"class":453},"+",[448,565,566],{"class":457}," f.duration;\n",[448,568,570,573],{"class":91,"line":569},6,[448,571,572],{"class":474},"  report",[448,574,575],{"class":457},"({\n",[448,577,579,582,585],{"class":91,"line":578},7,[448,580,581],{"class":457},"    inp: Math.",[448,583,584],{"class":474},"round",[448,586,587],{"class":457},"(value),\n",[448,589,591],{"class":91,"line":590},8,[448,592,593],{"class":457},"    target: attribution.interactionTarget,\n",[448,595,597,600,602,605,608,610,612,615,617],{"class":91,"line":596},9,[448,598,599],{"class":457},"    rafMs: Math.",[448,601,584],{"class":474},[448,603,604],{"class":457},"(Math.",[448,606,607],{"class":474},"max",[448,609,519],{"class":457},[448,611,66],{"class":506},[448,613,614],{"class":457},", f.styleAndLayoutStart ",[448,616,522],{"class":453},[448,618,619],{"class":457}," f.renderStart)),\n",[448,621,623,626,628,631,633],{"class":91,"line":622},10,[448,624,625],{"class":457},"    renderMs: Math.",[448,627,584],{"class":474},[448,629,630],{"class":457},"(end ",[448,632,522],{"class":453},[448,634,635],{"class":457}," f.styleAndLayoutStart),\n",[448,637,639],{"class":91,"line":638},11,[448,640,641],{"class":457},"  });\n",[448,643,645],{"class":91,"line":644},12,[448,646,647],{"class":457},"});\n",[448,649,651],{"class":91,"line":650},13,[448,652,654],{"class":653},"sjfSM","\u002F\u002F trade-off: using the LAST overlapping frame assumes the interaction's paint\n",[448,656,658],{"class":91,"line":657},14,[448,659,660],{"class":653},"\u002F\u002F happens there. Interactions that schedule a second frame (rAF → rAF) need\n",[448,662,664],{"class":91,"line":663},15,[448,665,666],{"class":653},"\u002F\u002F the frame containing the presentation timestamp instead.\n",[15,668,669,670,673],{},"Expected outcome: a ranked list of interaction targets where ",[33,671,672],{},"renderMs"," dominates — typically two or three components.",[431,675,677],{"id":676},"_2-narrow-the-style-invalidation-scope","2. Narrow the style invalidation scope",[15,679,680],{},"Move state classes from the document root to the smallest subtree that needs them, and avoid selectors that make large parts of the tree depend on a root class.",[439,682,686],{"className":683,"code":684,"language":685,"meta":444,"style":444},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F* Before: every rule under .menu-open must be re-matched across the page. *\u002F\n.menu-open .site-header nav a { color: var(--accent); }\n\n\u002F* After: the state lives on the component; invalidation stays inside it. *\u002F\n.site-header[data-menu=\"open\"] nav a { color: var(--accent); }\n\u002F* trade-off: component-scoped state makes global \"page dimmed while menu open\"\n   effects harder; use a single overlay element for those instead of a root class. *\u002F\n","css",[33,687,688,693,728,734,739,778,783],{"__ignoreMap":444},[448,689,690],{"class":91,"line":450},[448,691,692],{"class":653},"\u002F* Before: every rule under .menu-open must be re-matched across the page. *\u002F\n",[448,694,695,698,701,705,708,711,714,717,720,722,725],{"class":91,"line":471},[448,696,697],{"class":506},".menu-open",[448,699,700],{"class":506}," .site-header",[448,702,704],{"class":703},"sZBmE"," nav",[448,706,707],{"class":703}," a",[448,709,710],{"class":457}," { ",[448,712,713],{"class":506},"color",[448,715,716],{"class":457},": ",[448,718,719],{"class":506},"var",[448,721,519],{"class":457},[448,723,724],{"class":481},"--accent",[448,726,727],{"class":457},"); }\n",[448,729,730],{"class":91,"line":500},[448,731,733],{"emptyLinePlaceholder":732},true,"\n",[448,735,736],{"class":91,"line":530},[448,737,738],{"class":653},"\u002F* After: the state lives on the component; invalidation stays inside it. *\u002F\n",[448,740,741,744,747,750,753,756,759,762,764,766,768,770,772,774,776],{"class":91,"line":550},[448,742,743],{"class":506},".site-header",[448,745,746],{"class":457},"[",[448,748,749],{"class":506},"data-menu",[448,751,752],{"class":453},"=",[448,754,755],{"class":464},"\"open\"",[448,757,758],{"class":457},"] ",[448,760,761],{"class":703},"nav",[448,763,707],{"class":703},[448,765,710],{"class":457},[448,767,713],{"class":506},[448,769,716],{"class":457},[448,771,719],{"class":506},[448,773,519],{"class":457},[448,775,724],{"class":481},[448,777,727],{"class":457},[448,779,780],{"class":91,"line":569},[448,781,782],{"class":653},"\u002F* trade-off: component-scoped state makes global \"page dimmed while menu open\"\n",[448,784,785],{"class":91,"line":578},[448,786,787],{"class":653},"   effects harder; use a single overlay element for those instead of a root class. *\u002F\n",[15,789,790],{},"Expected outcome: \"Elements affected\" in Recalculate Style drops from thousands to tens; render tail typically falls 30–90ms.",[431,792,794],{"id":793},"_3-contain-layout-for-the-region-that-changes","3. Contain layout for the region that changes",[439,796,798],{"className":683,"code":797,"language":685,"meta":444,"style":444},".results-grid {\n  contain: layout paint;        \u002F* changes inside cannot move content outside *\u002F\n}\n.results-grid > .card {\n  content-visibility: auto;     \u002F* off-screen cards skip rendering entirely *\u002F\n  contain-intrinsic-size: auto 320px;\n}\n\u002F* trade-off: contain: paint clips overflow (dropdowns, tooltips escaping the\n   grid). Portal those overlays to the body or skip paint containment. *\u002F\n",[33,799,800,807,818,823,835,851,868,872,877],{"__ignoreMap":444},[448,801,802,805],{"class":91,"line":450},[448,803,804],{"class":506},".results-grid",[448,806,497],{"class":457},[448,808,809,812,815],{"class":91,"line":471},[448,810,811],{"class":506},"  contain",[448,813,814],{"class":457},": layout paint;        ",[448,816,817],{"class":653},"\u002F* changes inside cannot move content outside *\u002F\n",[448,819,820],{"class":91,"line":500},[448,821,822],{"class":457},"}\n",[448,824,825,827,830,833],{"class":91,"line":530},[448,826,804],{"class":506},[448,828,829],{"class":453}," >",[448,831,832],{"class":506}," .card",[448,834,497],{"class":457},[448,836,837,840,842,845,848],{"class":91,"line":550},[448,838,839],{"class":506},"  content-visibility",[448,841,716],{"class":457},[448,843,844],{"class":506},"auto",[448,846,847],{"class":457},";     ",[448,849,850],{"class":653},"\u002F* off-screen cards skip rendering entirely *\u002F\n",[448,852,853,856,858,860,863,866],{"class":91,"line":569},[448,854,855],{"class":506},"  contain-intrinsic-size",[448,857,716],{"class":457},[448,859,844],{"class":506},[448,861,862],{"class":506}," 320",[448,864,865],{"class":453},"px",[448,867,468],{"class":457},[448,869,870],{"class":91,"line":578},[448,871,822],{"class":457},[448,873,874],{"class":91,"line":590},[448,875,876],{"class":653},"\u002F* trade-off: contain: paint clips overflow (dropdowns, tooltips escaping the\n",[448,878,879],{"class":91,"line":596},[448,880,881],{"class":653},"   grid). Portal those overlays to the body or skip paint containment. *\u002F\n",[15,883,884,885,888,889,891,892,896],{},"Expected outcome: the Layout event's root changes from ",[33,886,887],{},"#document"," to ",[33,890,804],{},", and layout time scales with visible cards only. See ",[19,893,895],{"href":894},"\u002Frendering-css-performance\u002Fcss-containment-and-content-visibility\u002F","CSS containment and content-visibility"," for the full model.",[431,898,900],{"id":899},"_4-move-post-interaction-work-after-the-paint","4. Move post-interaction work after the paint",[15,902,903],{},"Anything that does not change what the user sees — analytics, logging, prefetching — should run after the next paint, not in a rAF callback before it.",[439,905,907],{"className":441,"code":906,"language":443,"meta":444,"style":444},"function afterNextPaint(fn) {\n  requestAnimationFrame(() => setTimeout(fn, 0));   \u002F\u002F rAF → task after the frame\n}\nbutton.addEventListener('click', () => {\n  applyFilter();                                 \u002F\u002F visible change: do it now\n  afterNextPaint(() => track('filter_applied'));  \u002F\u002F invisible work: after paint\n});\n\u002F\u002F trade-off: work deferred past the paint can be lost if the page unloads in\n\u002F\u002F the same instant. For must-send analytics, queue a beacon instead.\n",[33,908,909,925,949,953,973,984,1007,1011,1016],{"__ignoreMap":444},[448,910,911,914,917,919,922],{"class":91,"line":450},[448,912,913],{"class":453},"function",[448,915,916],{"class":474}," afterNextPaint",[448,918,519],{"class":457},[448,920,921],{"class":481},"fn",[448,923,924],{"class":457},") {\n",[448,926,927,930,933,935,938,941,943,946],{"class":91,"line":471},[448,928,929],{"class":474},"  requestAnimationFrame",[448,931,932],{"class":457},"(() ",[448,934,494],{"class":453},[448,936,937],{"class":474}," setTimeout",[448,939,940],{"class":457},"(fn, ",[448,942,66],{"class":506},[448,944,945],{"class":457},"));   ",[448,947,948],{"class":653},"\u002F\u002F rAF → task after the frame\n",[448,950,951],{"class":91,"line":500},[448,952,822],{"class":457},[448,954,955,958,961,963,966,969,971],{"class":91,"line":530},[448,956,957],{"class":457},"button.",[448,959,960],{"class":474},"addEventListener",[448,962,519],{"class":457},[448,964,965],{"class":464},"'click'",[448,967,968],{"class":457},", () ",[448,970,494],{"class":453},[448,972,497],{"class":457},[448,974,975,978,981],{"class":91,"line":550},[448,976,977],{"class":474},"  applyFilter",[448,979,980],{"class":457},"();                                 ",[448,982,983],{"class":653},"\u002F\u002F visible change: do it now\n",[448,985,986,989,991,993,996,998,1001,1004],{"class":91,"line":569},[448,987,988],{"class":474},"  afterNextPaint",[448,990,932],{"class":457},[448,992,494],{"class":453},[448,994,995],{"class":474}," track",[448,997,519],{"class":457},[448,999,1000],{"class":464},"'filter_applied'",[448,1002,1003],{"class":457},"));  ",[448,1005,1006],{"class":653},"\u002F\u002F invisible work: after paint\n",[448,1008,1009],{"class":91,"line":578},[448,1010,647],{"class":457},[448,1012,1013],{"class":91,"line":590},[448,1014,1015],{"class":653},"\u002F\u002F trade-off: work deferred past the paint can be lost if the page unloads in\n",[448,1017,1018],{"class":91,"line":596},[448,1019,1020],{"class":653},"\u002F\u002F the same instant. For must-send analytics, queue a beacon instead.\n",[15,1022,1023],{},"Expected outcome: the rAF portion of the tail disappears from LoAF entries for the affected interaction.",[15,1025,1026],{},[53,1027,60,1030,60,1033,60,1036,60,1038,60,1041,60,1043,60,1048,60,1055,60,1061,60,1066,60,1070,60,1074,60,1078,60,1082,60,1086,60,1090,60,1094,60,1099,60,1103,60],{"viewBox":1028,"width":56,"role":57,"ariaLabel":1029,"style":59},"0 0 760 216","Bar chart of render tail duration after cumulatively applying three fixes, ending under 50 milliseconds.",[62,1031],{"className":1032,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],[69,1034,1035],{},"Render tail before and after each fix (mid-tier phone)",[73,1037,1029],{},[62,1039],{"x":77,"y":77,"width":78,"height":1040,"rx":80,"fill":81,"stroke":82,"style":83},"214",[85,1042,1035],{"x":87,"y":88,"fill":82,"style":89},[91,1044],{"x1":1045,"y1":94,"x2":1045,"y2":1046,"stroke":96,"strokeWidth":97,"strokeDashArray":1047,"style":100},"293.1","194.0",[99,99],[85,1049,1054],{"x":1050,"y":1051,"fill":82,"style":1052,"textAnchor":1053},"150.0","92.0","font-size:13px","end","Baseline",[62,1056],{"x":1057,"y":109,"width":1058,"height":1059,"rx":112,"fill":131,"stroke":96,"style":1060},"162.0","506.0","19","fill-opacity:0.7;stroke-opacity:0.9",[85,1062,1065],{"x":1063,"y":1051,"fill":82,"style":1064},"674.0","font-size:12px;font-weight:600","193ms",[85,1067,1069],{"x":1050,"y":1068,"fill":82,"style":1052,"textAnchor":1053},"123.0","Scoped state class",[62,1071],{"x":1057,"y":1072,"width":1073,"height":1059,"rx":112,"fill":131,"stroke":96,"style":1060},"109.0","317.2",[85,1075,1077],{"x":1076,"y":1068,"fill":82,"style":1064},"485.2","121ms",[85,1079,1081],{"x":1050,"y":1080,"fill":82,"style":1052,"textAnchor":1053},"154.0","+ layout containment",[62,1083],{"x":1057,"y":1084,"width":1085,"height":1059,"rx":112,"fill":131,"stroke":96,"style":1060},"140.0","167.8",[85,1087,1089],{"x":1088,"y":1080,"fill":82,"style":1064},"335.8","64ms",[85,1091,1093],{"x":1050,"y":1092,"fill":82,"style":1052,"textAnchor":1053},"185.0","+ work after paint",[62,1095],{"x":1057,"y":1096,"width":1097,"height":1059,"rx":112,"fill":118,"stroke":118,"style":1098},"171.0","107.5","fill-opacity:0.55;stroke-opacity:0.9",[85,1100,1102],{"x":1101,"y":1092,"fill":82,"style":1064},"275.5","41ms",[85,1104,1105],{"x":1045,"y":197,"fill":198,"style":199,"textAnchor":125},"50ms tail target",[202,1107,1109],{"id":1108},"verification","Verification",[15,1111,1112,1113,1115,1116,1119,1120,1124],{},"In the lab, record the interaction with paint flashing on and confirm the flashed area matches the visual change. In the trace, Recalculate Style should report a small element count and Layout should be rooted at the contained element. In the field, the ",[33,1114,672],{}," series for the target should drop and INP's presentation-delay share should fall below a third. If presentation delay is still high but the trace looks lean, check for a heavy ",[33,1117,1118],{},"backdrop-filter"," or large composited layers — the guide on ",[19,1121,1123],{"href":1122},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fwill-change-overuse-and-layer-explosion\u002F","will-change overuse and layer explosion"," covers that case.",[202,1126,1128],{"id":1127},"edge-cases-that-hide-in-the-render-tail","Edge Cases That Hide in the Render Tail",[15,1130,1131,1134],{},[213,1132,1133],{},"Font swaps landing in the interaction frame."," If an interaction reveals text in a weight or family that has not loaded yet — a bold label in an opened accordion, an icon font in a menu — the font request starts on that frame and the swap triggers a second layout later. The first frame is not slow, but the visible result is not final either. Preload the weights used by interactive UI, or render those states with already-loaded faces.",[15,1136,1137,1140,1141,1144,1145,1148],{},[213,1138,1139],{},"Image decode on reveal."," Expanding a panel that contains large images can push synchronous decode into the paint step. Large images revealed by interaction should carry ",[33,1142,1143],{},"decoding=\"async\""," and, ideally, be decoded ahead of time with ",[33,1146,1147],{},"img.decode()"," when the user hovers the trigger.",[15,1150,1151,1154,1155,1158],{},[213,1152,1153],{},"Sticky and fixed elements during scroll-linked updates."," A handler that updates something inside a ",[33,1156,1157],{},"position: sticky"," header can force the browser to repaint the header on every frame of an ongoing scroll. LoAF shows a modest paint each time, but on low-end GPUs these add up to a slow render tail for any interaction that happens mid-scroll.",[15,1160,1161,1164,1165,279,1168,1171,1172,1175,1176,1179],{},[213,1162,1163],{},"Transitions started by the same interaction."," A CSS transition on ",[33,1166,1167],{},"height",[33,1169,1170],{},"top"," that the click starts will not lengthen the interaction frame itself, but each subsequent frame does layout. The next interaction may land on one of those frames and inherit its cost as input delay. Transition ",[33,1173,1174],{},"transform"," and ",[33,1177,1178],{},"opacity"," instead, and the follow-on frames stay on the compositor.",[15,1181,1182,1183,279,1186,1189],{},"When none of these apply and the tail is still long, take a trace with the \"Enable advanced paint instrumentation\" setting on. It is slower to record, but the paint profiler lists draw operations per layer, which is the only way to see that a single large ",[33,1184,1185],{},"box-shadow",[33,1187,1188],{},"filter"," is what costs 60ms on a mid-range phone.",[202,1191,1193],{"id":1192},"faq","FAQ",[1195,1196,1199,1203],"details",{"className":1197},[1198],"faq-item",[1200,1201,1202],"summary",{},"Why does presentation delay vary so much between devices?",[15,1204,1205],{},"Style and layout scale with CPU speed, but paint and raster also scale with GPU capability and screen resolution. A high-DPR phone with a weak GPU can spend three times longer rasterising the same change than a laptop. That is why lab traces on desktop routinely under-report presentation delay; throttle the CPU and test on a real mid-tier Android device.",[1195,1207,1209,1212],{"className":1208},[1198],[1200,1210,1211],{},"Can I yield inside rendering?",[15,1213,1214,1215,28],{},"No — rendering is the browser's work, not a task you control. What you can do is make the change smaller (fewer nodes, narrower invalidation), split a large visual update across frames deliberately (render the first screenful now, the rest after paint), or skip rendering for off-screen content with ",[33,1216,1217],{},"content-visibility",[202,1219,1221],{"id":1220},"related","Related",[207,1223,1224,1231,1238],{},[210,1225,1226,1230],{},[19,1227,1229],{"href":1228},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Freducing-presentation-delay-from-large-dom-updates\u002F","Reducing presentation delay from large DOM updates"," — techniques for shrinking the update itself.",[210,1232,1233,1237],{},[19,1234,1236],{"href":1235},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Freducing-style-recalculation-cost\u002F","Reducing style recalculation cost"," — selector and invalidation mechanics in depth.",[210,1239,1240,1244],{},[19,1241,1243],{"href":1242},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Ffinding-slow-scripts-with-loaf-script-attribution\u002F","Finding slow scripts with LoAF script attribution"," — the companion workflow when scripts, not rendering, dominate.",[1246,1247,1249],"script",{"type":1248},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Fix INP When Presentation Delay, Not Your Handler, Is the Problem\",\n  \"description\": \"When handlers are fast but INP is slow, the cost is in rendering. Use LoAF timestamps to measure and shrink the render tail.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Measure the tail per interaction target in the field\",\n      \"text\": \"Extend your INP beacon with the render-tail split so you can find which targets suffer from rendering cost at scale.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Narrow the style invalidation scope\",\n      \"text\": \"Move state classes from the document root to the smallest subtree that needs them, and avoid selectors that make large parts of the tree depend on a root class.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Contain layout for the region that changes\",\n      \"text\": \"Expected outcome: the Layout event's root changes from #document to .results-grid, and layout time scales with visible cards only.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Move post-interaction work after the paint\",\n      \"text\": \"Anything that does not change what the user sees — analytics, logging, prefetching — should run after the next paint, not in a rAF callback before it.\"\n    }\n  ]\n}\n",[1246,1251,1252],{"type":1248},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix INP When Presentation Delay, Not Your Handler, Is the Problem\",\n  \"description\": \"When handlers are fast but INP is slow, the cost is in rendering. Use LoAF timestamps to measure and shrink the render tail.\",\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\u002Fdiagnosing-presentation-delay-with-loaf\u002F\"\n  }\n}\n",[1246,1254,1255],{"type":1248},"\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      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Diagnosing Presentation Delay with LoAF\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002F\"\n    }\n  ]\n}\n",[1257,1258,1259],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":444,"searchDepth":471,"depth":471,"links":1261},[1262,1263,1264,1270,1271,1272,1273],{"id":204,"depth":471,"text":205},{"id":268,"depth":471,"text":269},{"id":428,"depth":471,"text":429,"children":1265},[1266,1267,1268,1269],{"id":433,"depth":500,"text":434},{"id":676,"depth":500,"text":677},{"id":793,"depth":500,"text":794},{"id":899,"depth":500,"text":900},{"id":1108,"depth":471,"text":1109},{"id":1127,"depth":471,"text":1128},{"id":1192,"depth":471,"text":1193},{"id":1220,"depth":471,"text":1221},"When handlers are fast but INP is slow, the cost is in rendering. Use LoAF timestamps to measure and shrink the render tail.","md",{"slug":1277,"type":1278,"breadcrumb":1279,"datePublished":1288,"dateModified":1288},"diagnosing-presentation-delay-with-loaf","article",[1280,1283,1284,1286],{"name":1281,"url":1282},"Home","\u002F",{"name":27,"url":26},{"name":1285,"url":21},"Long Animation Frames (LoAF) API",{"name":5,"url":1287},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf",{"title":1291,"description":1292},"Diagnosing INP Presentation Delay with LoAF","Use LoAF renderStart and styleAndLayoutStart to isolate the rendering tail of slow interactions, then cut style, layout and paint cost under 200ms.","core-web-vitals-measurement\u002Flong-animation-frames-api\u002Fdiagnosing-presentation-delay-with-loaf\u002Findex","mOwb-EwhbYgZLmKfkSG4xFHtLlU81JP_HxdY5Bs-NWQ",[1296,1299],{"title":1285,"path":1297,"stem":1298,"children":-1},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api","core-web-vitals-measurement\u002Flong-animation-frames-api\u002Findex",{"title":1300,"path":1301,"stem":1302,"children":-1},"Finding Slow Scripts with LoAF Script Attribution","\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002Ffinding-slow-scripts-with-loaf-script-attribution","core-web-vitals-measurement\u002Flong-animation-frames-api\u002Ffinding-slow-scripts-with-loaf-script-attribution\u002Findex",1791308074171]