[{"data":1,"prerenderedAt":1274},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers":1265},{"id":4,"title":5,"body":6,"description":1245,"extension":1246,"meta":1247,"navigation":440,"path":1259,"seo":1260,"stem":1263,"__hash__":1264},"content\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers\u002Findex.md","Fixing INP from Forced Layout in Event Handlers",{"type":7,"value":8,"toc":1231},"minimark",[9,14,29,52,165,170,214,218,224,230,243,260,351,355,360,363,555,558,562,569,663,666,670,680,809,817,821,835,927,930,1051,1055,1061,1065,1068,1133,1142,1154,1158,1167,1180,1189,1193,1216,1221,1224,1227],[10,11,13],"h1",{"id":12},"how-to-fix-inp-caused-by-forced-layout-inside-event-handlers","How to Fix INP Caused by Forced Layout Inside Event Handlers",[15,16,17,18,23,24,28],"p",{},"This guide covers a specific processing-time problem within ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F","Profiling Event Handlers for INP",", part of ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",": handlers that are slow not because of their own logic but because they force the browser to compute layout synchronously, sometimes dozens of times.",[15,30,31,32,36,37,36,40,36,43,46,47,51],{},"Browsers batch style and layout work and perform it once per frame. When JavaScript writes to the DOM (adding a class, changing a style, inserting a node) and then reads a layout-dependent property (",[33,34,35],"code",{},"offsetHeight",", ",[33,38,39],{},"getBoundingClientRect()",[33,41,42],{},"scrollTop",[33,44,45],{},"getComputedStyle()"," for layout values), the browser must perform layout ",[48,49,50],"em",{},"immediately"," to return a correct answer. Do that once and it costs a few milliseconds. Do it in a loop — write, read, write, read — and each iteration forces a fresh layout: layout thrashing. A handler that measures 50 accordion panels this way can spend 150ms in forced layout while its own logic takes 5ms.",[15,53,54],{},[55,56,62,63,62,70,62,74,62,77,62,86,62,92,62,101,62,107,62,112,62,117,62,120,62,123,62,126,62,129,62,132,62,135,62,140,62,144,62,146,62,150,62,152,62,155,62,157,62,160,62,162,62],"svg",{"viewBox":57,"width":58,"role":59,"ariaLabel":60,"style":61},"0 0 760 228","100%","img","Comparison of a loop that alternates DOM writes and layout reads, forcing layout on every iteration, with a batched version that forces layout once.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[64,65],"rect",{"className":66,"x":68,"y":68,"width":58,"height":58,"fill":69},[67],"svg-canvas","0","#ffffff",[71,72,73],"title",{},"Interleaved vs batched DOM access",[75,76,60],"desc",{},[64,78],{"x":79,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[87,88,73],"text",{"x":89,"y":90,"fill":84,"style":91},"28.0","34.0","font-size:16px;font-weight:700",[64,93],{"x":89,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":99,"style":100},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[87,102,106],{"x":103,"y":104,"fill":84,"style":105},"42.0","82.0","font-size:14px;font-weight:700","Interleaved (thrashing)",[87,108,111],{"x":103,"y":109,"fill":84,"style":110},"108.0","font-size:12.5px;font-weight:700","•",[87,113,116],{"x":94,"y":109,"fill":84,"style":114,"textAnchor":115},"font-size:12.5px","start","write class on item 1, read its height",[87,118,111],{"x":103,"y":119,"fill":84,"style":110},"132.0",[87,121,122],{"x":94,"y":119,"fill":84,"style":114,"textAnchor":115},"write class on item 2, read its height",[87,124,111],{"x":103,"y":125,"fill":84,"style":110},"156.0",[87,127,128],{"x":94,"y":125,"fill":84,"style":114,"textAnchor":115},"... 50 forced layouts",[87,130,111],{"x":103,"y":131,"fill":84,"style":110},"180.0",[87,133,134],{"x":94,"y":131,"fill":84,"style":114,"textAnchor":115},"Handler: 160ms, mostly layout",[64,136],{"x":137,"y":94,"width":95,"height":96,"rx":97,"fill":138,"stroke":138,"style":139},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[87,141,143],{"x":142,"y":104,"fill":84,"style":105},"406.0","Batched (one layout)",[87,145,111],{"x":142,"y":109,"fill":84,"style":110},[87,147,149],{"x":148,"y":109,"fill":84,"style":114,"textAnchor":115},"420.0","read all 50 heights first",[87,151,111],{"x":142,"y":119,"fill":84,"style":110},[87,153,154],{"x":148,"y":119,"fill":84,"style":114,"textAnchor":115},"then write all 50 classes",[87,156,111],{"x":142,"y":125,"fill":84,"style":110},[87,158,159],{"x":148,"y":125,"fill":84,"style":114,"textAnchor":115},"1 layout, at the next frame",[87,161,111],{"x":142,"y":131,"fill":84,"style":110},[87,163,164],{"x":148,"y":131,"fill":84,"style":114,"textAnchor":115},"Handler: 12ms",[166,167,169],"h2",{"id":168},"rapid-diagnosis","Rapid Diagnosis",[171,172,173,181,187,193,208],"ul",{},[174,175,176,180],"li",{},[177,178,179],"strong",{},"Look for purple inside yellow."," In the Performance panel, forced layouts appear as purple \"Layout\" (and \"Recalculate Style\") blocks nested inside your JavaScript call stacks, marked with a red warning triangle: \"Forced reflow is a likely performance bottleneck\".",[174,182,183,186],{},[177,184,185],{},"Click the warning."," The summary shows the JavaScript line that triggered the layout — the read — and links to the source.",[174,188,189,192],{},[177,190,191],{},"Count the layouts per interaction."," More than one or two Layout events inside a single handler indicates thrashing.",[174,194,195,198,199,203,204,207],{},[177,196,197],{},"Check LoAF attribution."," If you collect ",[19,200,202],{"href":201},"\u002Fcore-web-vitals-measurement\u002Flong-animation-frames-api\u002F","Long Animation Frames"," data, ",[33,205,206],{},"forcedStyleAndLayoutDuration"," on the handler's script entry quantifies the problem directly.",[174,209,210,213],{},[177,211,212],{},"Check third-party code."," Analytics and A\u002FB tools that read element positions on click (for heatmaps or visibility checks) can force layout inside your interaction.",[166,215,217],{"id":216},"root-cause-analysis","Root Cause Analysis",[15,219,220,223],{},[177,221,222],{},"1. Measure-then-mutate loops."," Code that iterates elements, changes each, and reads its new size — common in accordions, masonry layouts, \"equal height\" scripts and tooltip positioning.",[15,225,226,229],{},[177,227,228],{},"2. Reading layout after a framework update."," Calling a state setter and then synchronously reading the DOM (via a ref) in the same handler. In frameworks that flush synchronously, the read forces layout of the freshly mutated tree.",[15,231,232,235,236,239,240,242],{},[177,233,234],{},"3. Scroll position reads after insertion."," Inserting content and immediately reading ",[33,237,238],{},"scrollHeight"," or ",[33,241,42],{}," to auto-scroll a chat or log view.",[15,244,245,252,253,239,256,259],{},[177,246,247,248,251],{},"4. ",[33,249,250],{},"getComputedStyle"," in hot paths."," Reading computed values like ",[33,254,255],{},"width",[33,257,258],{},"height"," forces layout; even reading non-layout values forces style recalculation.",[15,261,262],{},[55,263,62,266,62,269,62,272,62,274,62,277,62,279,62,289,62,296,62,304,62,309,62,313,62,318,62,322,62,326,62,329,62,331,62,335,62,340,62,344,62],{"viewBox":264,"width":58,"role":59,"ariaLabel":265,"style":61},"0 0 760 216","Bar chart of a click handler's time split into script logic and forced layout, before and after batching reads and writes.",[64,267],{"className":268,"x":68,"y":68,"width":58,"height":58,"fill":69},[67],[71,270,271],{},"Time inside one accordion \"expand all\" handler",[75,273,265],{},[64,275],{"x":79,"y":79,"width":80,"height":276,"rx":82,"fill":83,"stroke":84,"style":85},"214",[87,278,271],{"x":89,"y":90,"fill":84,"style":91},[280,281],"line",{"x1":282,"y1":283,"x2":282,"y2":284,"stroke":99,"strokeWidth":285,"strokeDashArray":286,"style":288},"331.1","72.0","194.0","1.5",[287,287],"4","stroke-opacity:0.9",[87,290,295],{"x":291,"y":292,"fill":84,"style":293,"textAnchor":294},"157.1","92.0","font-size:13px","end","Logic (before)",[64,297],{"x":298,"y":299,"width":300,"height":301,"rx":302,"fill":84,"stroke":84,"style":303},"169.1","78.0","19.4","19","3","fill-opacity:0.7;stroke-opacity:0.4",[87,305,308],{"x":306,"y":292,"fill":84,"style":307},"194.5","font-size:12px;font-weight:600","6ms",[87,310,312],{"x":291,"y":311,"fill":84,"style":293,"textAnchor":294},"123.0","Forced layout (before)",[64,314],{"x":298,"y":315,"width":316,"height":301,"rx":302,"fill":98,"stroke":99,"style":317},"109.0","498.9","fill-opacity:0.7;stroke-opacity:0.9",[87,319,321],{"x":320,"y":311,"fill":84,"style":307},"674.0","154ms",[87,323,325],{"x":291,"y":324,"fill":84,"style":293,"textAnchor":294},"154.0","Logic (after)",[64,327],{"x":298,"y":328,"width":300,"height":301,"rx":302,"fill":84,"stroke":84,"style":303},"140.0",[87,330,308],{"x":306,"y":324,"fill":84,"style":307},[87,332,334],{"x":291,"y":333,"fill":84,"style":293,"textAnchor":294},"185.0","Forced layout (after)",[64,336],{"x":298,"y":337,"width":338,"height":301,"rx":302,"fill":138,"stroke":138,"style":339},"171.0","13.0","fill-opacity:0.55;stroke-opacity:0.9",[87,341,343],{"x":342,"y":333,"fill":84,"style":307},"188.0","4ms",[87,345,350],{"x":282,"y":346,"fill":347,"style":348,"textAnchor":349},"68.0","#8a6608","font-size:12px;font-weight:700","middle","50ms",[166,352,354],{"id":353},"step-by-step-resolution","Step-by-Step Resolution",[356,357,359],"h3",{"id":358},"_1-separate-reads-from-writes","1. Separate reads from writes",[15,361,362],{},"Gather every measurement first, then apply every mutation. One layout happens at the next frame instead of one per iteration.",[364,365,370],"pre",{"className":366,"code":367,"language":368,"meta":369,"style":369},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Before: read and write interleaved — forces layout each iteration.\npanels.forEach((p) => { p.classList.add('open'); p.style.height = p.scrollHeight + 'px'; });\n\n\u002F\u002F After: all reads, then all writes.\nconst heights = panels.map((p) => p.querySelector('.inner').scrollHeight);  \u002F\u002F reads (1 layout)\npanels.forEach((p, i) => { p.classList.add('open'); p.style.height = heights[i] + 'px'; });  \u002F\u002F writes\n\u002F\u002F trade-off: reads now happen before the 'open' class is applied, so they must\n\u002F\u002F not depend on it. Measure an inner element whose size does not change with\n\u002F\u002F the open state, or measure once on load and cache.\n","javascript","",[33,371,372,380,435,442,448,492,537,543,549],{"__ignoreMap":369},[373,374,376],"span",{"class":280,"line":375},1,[373,377,379],{"class":378},"sjfSM","\u002F\u002F Before: read and write interleaved — forces layout each iteration.\n",[373,381,383,387,391,394,397,400,404,407,410,413,417,420,423,426,429,432],{"class":280,"line":382},2,[373,384,386],{"class":385},"saISM","panels.",[373,388,390],{"class":389},"smZ65","forEach",[373,392,393],{"class":385},"((",[373,395,15],{"class":396},"sQw3B",[373,398,399],{"class":385},") ",[373,401,403],{"class":402},"sPARh","=>",[373,405,406],{"class":385}," { p.classList.",[373,408,409],{"class":389},"add",[373,411,412],{"class":385},"(",[373,414,416],{"class":415},"sZ8jY","'open'",[373,418,419],{"class":385},"); p.style.height ",[373,421,422],{"class":402},"=",[373,424,425],{"class":385}," p.scrollHeight ",[373,427,428],{"class":402},"+",[373,430,431],{"class":415}," 'px'",[373,433,434],{"class":385},"; });\n",[373,436,438],{"class":280,"line":437},3,[373,439,441],{"emptyLinePlaceholder":440},true,"\n",[373,443,445],{"class":280,"line":444},4,[373,446,447],{"class":378},"\u002F\u002F After: all reads, then all writes.\n",[373,449,451,454,458,461,464,467,469,471,473,475,478,481,483,486,489],{"class":280,"line":450},5,[373,452,453],{"class":402},"const",[373,455,457],{"class":456},"sPXB4"," heights",[373,459,460],{"class":402}," =",[373,462,463],{"class":385}," panels.",[373,465,466],{"class":389},"map",[373,468,393],{"class":385},[373,470,15],{"class":396},[373,472,399],{"class":385},[373,474,403],{"class":402},[373,476,477],{"class":385}," p.",[373,479,480],{"class":389},"querySelector",[373,482,412],{"class":385},[373,484,485],{"class":415},"'.inner'",[373,487,488],{"class":385},").scrollHeight);  ",[373,490,491],{"class":378},"\u002F\u002F reads (1 layout)\n",[373,493,495,497,499,501,503,505,508,510,512,514,516,518,520,522,524,527,529,531,534],{"class":280,"line":494},6,[373,496,386],{"class":385},[373,498,390],{"class":389},[373,500,393],{"class":385},[373,502,15],{"class":396},[373,504,36],{"class":385},[373,506,507],{"class":396},"i",[373,509,399],{"class":385},[373,511,403],{"class":402},[373,513,406],{"class":385},[373,515,409],{"class":389},[373,517,412],{"class":385},[373,519,416],{"class":415},[373,521,419],{"class":385},[373,523,422],{"class":402},[373,525,526],{"class":385}," heights[i] ",[373,528,428],{"class":402},[373,530,431],{"class":415},[373,532,533],{"class":385},"; });  ",[373,535,536],{"class":378},"\u002F\u002F writes\n",[373,538,540],{"class":280,"line":539},7,[373,541,542],{"class":378},"\u002F\u002F trade-off: reads now happen before the 'open' class is applied, so they must\n",[373,544,546],{"class":280,"line":545},8,[373,547,548],{"class":378},"\u002F\u002F not depend on it. Measure an inner element whose size does not change with\n",[373,550,552],{"class":280,"line":551},9,[373,553,554],{"class":378},"\u002F\u002F the open state, or measure once on load and cache.\n",[15,556,557],{},"Expected outcome: forced layout inside the handler drops to at most one; processing duration falls by most of the forced-layout time.",[356,559,561],{"id":560},"_2-defer-reads-to-the-next-frame-when-they-do-not-need-to-be-synchronous","2. Defer reads to the next frame when they do not need to be synchronous",[15,563,564,565,568],{},"If a measurement only drives a follow-up visual adjustment, read it in ",[33,566,567],{},"requestAnimationFrame"," — layout will already have happened for the frame, so the read is free.",[364,570,572],{"className":366,"code":571,"language":368,"meta":369,"style":369},"button.addEventListener('click', () => {\n  list.append(newItem);                                   \u002F\u002F write\n  requestAnimationFrame(() => {                           \u002F\u002F read on the next frame\n    list.scrollTop = list.scrollHeight;                   \u002F\u002F still a write, but layout is fresh\n  });\n});\n\u002F\u002F trade-off: the follow-up adjustment lands one frame later. For auto-scrolling\n\u002F\u002F that is invisible; for positioning a tooltip it can flash in the wrong place\n\u002F\u002F for a frame — hide it until positioned.\n",[33,573,574,595,609,625,638,643,648,653,658],{"__ignoreMap":369},[373,575,576,579,582,584,587,590,592],{"class":280,"line":375},[373,577,578],{"class":385},"button.",[373,580,581],{"class":389},"addEventListener",[373,583,412],{"class":385},[373,585,586],{"class":415},"'click'",[373,588,589],{"class":385},", () ",[373,591,403],{"class":402},[373,593,594],{"class":385}," {\n",[373,596,597,600,603,606],{"class":280,"line":382},[373,598,599],{"class":385},"  list.",[373,601,602],{"class":389},"append",[373,604,605],{"class":385},"(newItem);                                   ",[373,607,608],{"class":378},"\u002F\u002F write\n",[373,610,611,614,617,619,622],{"class":280,"line":437},[373,612,613],{"class":389},"  requestAnimationFrame",[373,615,616],{"class":385},"(() ",[373,618,403],{"class":402},[373,620,621],{"class":385}," {                           ",[373,623,624],{"class":378},"\u002F\u002F read on the next frame\n",[373,626,627,630,632,635],{"class":280,"line":444},[373,628,629],{"class":385},"    list.scrollTop ",[373,631,422],{"class":402},[373,633,634],{"class":385}," list.scrollHeight;                   ",[373,636,637],{"class":378},"\u002F\u002F still a write, but layout is fresh\n",[373,639,640],{"class":280,"line":450},[373,641,642],{"class":385},"  });\n",[373,644,645],{"class":280,"line":494},[373,646,647],{"class":385},"});\n",[373,649,650],{"class":280,"line":539},[373,651,652],{"class":378},"\u002F\u002F trade-off: the follow-up adjustment lands one frame later. For auto-scrolling\n",[373,654,655],{"class":280,"line":545},[373,656,657],{"class":378},"\u002F\u002F that is invisible; for positioning a tooltip it can flash in the wrong place\n",[373,659,660],{"class":280,"line":551},[373,661,662],{"class":378},"\u002F\u002F for a frame — hide it until positioned.\n",[15,664,665],{},"Expected outcome: the read no longer forces layout inside the interaction.",[356,667,669],{"id":668},"_3-use-observers-instead-of-polling-layout","3. Use observers instead of polling layout",[15,671,672,675,676,679],{},[33,673,674],{},"ResizeObserver"," and ",[33,677,678],{},"IntersectionObserver"," deliver sizes and visibility after layout, asynchronously, with no forced reflow. Replace \"measure on click\" patterns with cached values kept fresh by observers.",[364,681,683],{"className":366,"code":682,"language":368,"meta":369,"style":369},"const sizes = new WeakMap();\nconst ro = new ResizeObserver((entries) => {\n  for (const e of entries) sizes.set(e.target, e.contentBoxSize[0].blockSize);\n});\npanels.forEach((p) => ro.observe(p.querySelector('.inner')));\n\u002F\u002F In the handler: sizes.get(inner) — no layout read at all.\n\u002F\u002F trade-off: cached sizes can be one frame stale after a change. For layout\n\u002F\u002F that must be pixel-exact at the moment of the click, read once, batched.\n",[33,684,685,703,728,758,762,794,799,804],{"__ignoreMap":369},[373,686,687,689,692,694,697,700],{"class":280,"line":375},[373,688,453],{"class":402},[373,690,691],{"class":456}," sizes",[373,693,460],{"class":402},[373,695,696],{"class":402}," new",[373,698,699],{"class":389}," WeakMap",[373,701,702],{"class":385},"();\n",[373,704,705,707,710,712,714,717,719,722,724,726],{"class":280,"line":382},[373,706,453],{"class":402},[373,708,709],{"class":456}," ro",[373,711,460],{"class":402},[373,713,696],{"class":402},[373,715,716],{"class":389}," ResizeObserver",[373,718,393],{"class":385},[373,720,721],{"class":396},"entries",[373,723,399],{"class":385},[373,725,403],{"class":402},[373,727,594],{"class":385},[373,729,730,733,736,738,741,744,747,750,753,755],{"class":280,"line":437},[373,731,732],{"class":402},"  for",[373,734,735],{"class":385}," (",[373,737,453],{"class":402},[373,739,740],{"class":456}," e",[373,742,743],{"class":402}," of",[373,745,746],{"class":385}," entries) sizes.",[373,748,749],{"class":389},"set",[373,751,752],{"class":385},"(e.target, e.contentBoxSize[",[373,754,68],{"class":456},[373,756,757],{"class":385},"].blockSize);\n",[373,759,760],{"class":280,"line":444},[373,761,647],{"class":385},[373,763,764,766,768,770,772,774,776,779,782,785,787,789,791],{"class":280,"line":450},[373,765,386],{"class":385},[373,767,390],{"class":389},[373,769,393],{"class":385},[373,771,15],{"class":396},[373,773,399],{"class":385},[373,775,403],{"class":402},[373,777,778],{"class":385}," ro.",[373,780,781],{"class":389},"observe",[373,783,784],{"class":385},"(p.",[373,786,480],{"class":389},[373,788,412],{"class":385},[373,790,485],{"class":415},[373,792,793],{"class":385},")));\n",[373,795,796],{"class":280,"line":494},[373,797,798],{"class":378},"\u002F\u002F In the handler: sizes.get(inner) — no layout read at all.\n",[373,800,801],{"class":280,"line":539},[373,802,803],{"class":378},"\u002F\u002F trade-off: cached sizes can be one frame stale after a change. For layout\n",[373,805,806],{"class":280,"line":545},[373,807,808],{"class":378},"\u002F\u002F that must be pixel-exact at the moment of the click, read once, batched.\n",[15,810,811,812,816],{},"Expected outcome: handlers do no layout reads; see ",[19,813,815],{"href":814},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002F","ResizeObserver vs resize and scroll handlers",".",[356,818,820],{"id":819},"_4-prefer-css-over-measured-javascript-animation","4. Prefer CSS over measured JavaScript animation",[15,822,823,824,239,827,830,831,834],{},"Many measure-then-animate patterns exist only to animate height. CSS ",[33,825,826],{},"grid-template-rows: 0fr → 1fr",[33,828,829],{},"interpolate-size: allow-keywords"," with ",[33,832,833],{},"height: auto"," animate to intrinsic size without any JavaScript measurement.",[364,836,840],{"className":837,"code":838,"language":839,"meta":369,"style":369},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast",":root { interpolate-size: allow-keywords; }        \u002F* Chromium 129+ *\u002F\n.panel { height: 0; overflow: clip; transition: height 200ms; }\n.panel.open { height: auto; }\n\u002F* trade-off: interpolate-size is not yet supported in every engine; elsewhere\n   the panel snaps open without animation, which is an acceptable fallback. *\u002F\n","css",[33,841,842,859,901,917,922],{"__ignoreMap":369},[373,843,844,847,850,853,856],{"class":280,"line":375},[373,845,846],{"class":456},":root",[373,848,849],{"class":385}," { ",[373,851,852],{"class":456},"interpolate-size",[373,854,855],{"class":385},": allow-keywords; }        ",[373,857,858],{"class":378},"\u002F* Chromium 129+ *\u002F\n",[373,860,861,864,866,868,871,873,876,879,881,884,886,889,892,895,898],{"class":280,"line":382},[373,862,863],{"class":456},".panel",[373,865,849],{"class":385},[373,867,258],{"class":456},[373,869,870],{"class":385},": ",[373,872,68],{"class":456},[373,874,875],{"class":385},"; ",[373,877,878],{"class":456},"overflow",[373,880,870],{"class":385},[373,882,883],{"class":456},"clip",[373,885,875],{"class":385},[373,887,888],{"class":456},"transition",[373,890,891],{"class":385},": height ",[373,893,894],{"class":456},"200",[373,896,897],{"class":402},"ms",[373,899,900],{"class":385},"; }\n",[373,902,903,906,908,910,912,915],{"class":280,"line":437},[373,904,905],{"class":456},".panel.open",[373,907,849],{"class":385},[373,909,258],{"class":456},[373,911,870],{"class":385},[373,913,914],{"class":456},"auto",[373,916,900],{"class":385},[373,918,919],{"class":280,"line":444},[373,920,921],{"class":378},"\u002F* trade-off: interpolate-size is not yet supported in every engine; elsewhere\n",[373,923,924],{"class":280,"line":450},[373,925,926],{"class":378},"   the panel snaps open without animation, which is an acceptable fallback. *\u002F\n",[15,928,929],{},"Expected outcome: the handler only toggles a class; all layout happens once, in the browser's normal frame.",[15,931,932],{},[55,933,62,936,62,939,62,942,62,944,62,947,62,949,62,953,62,959,62,963,62,969,62,973,62,976,62,979,62,982,62,985,62,988,62,992,62,996,62,1000,62,1004,62,1007,62,1013,62,1019,62,1023,62,1027,62,1031,62,1035,62,1039,62,1043,62,1047,62],{"viewBox":934,"width":58,"role":59,"ariaLabel":935,"style":61},"0 0 760 178","Timeline of a click handler with repeated forced layout blocks interleaved with short script segments, compared with the batched version.",[64,937],{"className":938,"x":68,"y":68,"width":58,"height":58,"fill":69},[67],[71,940,941],{},"Forced layouts inside a single click handler",[75,943,935],{},[64,945],{"x":79,"y":79,"width":80,"height":946,"rx":82,"fill":83,"stroke":84,"style":85},"176",[87,948,941],{"x":89,"y":90,"fill":84,"style":91},[87,950,952],{"x":89,"y":951,"fill":84,"style":110},"74.0","Thrashing",[64,954],{"x":955,"y":94,"width":956,"height":957,"rx":302,"fill":84,"stroke":84,"style":958},"102.1","13.2","26.0","fill-opacity:0.06;stroke-opacity:0.4",[64,960],{"x":961,"y":94,"width":962,"height":957,"rx":302,"fill":98,"stroke":99,"style":100},"116.8","108.7",[87,964,968],{"x":965,"y":966,"fill":84,"style":967,"textAnchor":349},"171.2","73.5","font-size:11.5px","layout",[64,970],{"x":971,"y":94,"width":972,"height":957,"rx":302,"fill":84,"stroke":84,"style":958},"227.0","9.5",[64,974],{"x":975,"y":94,"width":962,"height":957,"rx":302,"fill":98,"stroke":99,"style":100},"238.1",[87,977,968],{"x":978,"y":966,"fill":84,"style":967,"textAnchor":349},"292.4",[64,980],{"x":981,"y":94,"width":972,"height":957,"rx":302,"fill":84,"stroke":84,"style":958},"348.3",[64,983],{"x":984,"y":94,"width":962,"height":957,"rx":302,"fill":98,"stroke":99,"style":100},"359.3",[87,986,968],{"x":987,"y":966,"fill":84,"style":967,"textAnchor":349},"413.7",[64,989],{"x":990,"y":94,"width":991,"height":957,"rx":302,"fill":98,"stroke":99,"style":100},"469.5","219.0",[87,993,995],{"x":994,"y":966,"fill":84,"style":967,"textAnchor":349},"579.0","... x50",[87,997,999],{"x":89,"y":998,"fill":84,"style":110},"112.0","Batched",[64,1001],{"x":955,"y":1002,"width":1003,"height":957,"rx":302,"fill":84,"stroke":84,"style":958},"94.0","20.5",[64,1005],{"x":1006,"y":1002,"width":956,"height":957,"rx":302,"fill":138,"stroke":138,"style":139},"124.2",[280,1008],{"x1":1009,"y1":1010,"x2":1011,"y2":1010,"stroke":84,"style":1012},"101.4","134.0","726.0","stroke-opacity:0.4",[87,1014,1018],{"x":1015,"y":1016,"fill":1017,"style":967,"textAnchor":349},"112.1","151.0","#51617a","0ms",[87,1020,1022],{"x":1021,"y":1016,"fill":1017,"style":967,"textAnchor":349},"174.8","20ms",[87,1024,1026],{"x":1025,"y":1016,"fill":1017,"style":967,"textAnchor":349},"248.3","40ms",[87,1028,1030],{"x":1029,"y":1016,"fill":1017,"style":967,"textAnchor":349},"321.8","60ms",[87,1032,1034],{"x":1033,"y":1016,"fill":1017,"style":967,"textAnchor":349},"395.3","80ms",[87,1036,1038],{"x":1037,"y":1016,"fill":1017,"style":967,"textAnchor":349},"468.8","100ms",[87,1040,1042],{"x":1041,"y":1016,"fill":1017,"style":967,"textAnchor":349},"542.3","120ms",[87,1044,1046],{"x":1045,"y":1016,"fill":1017,"style":967,"textAnchor":349},"615.8","140ms",[87,1048,1050],{"x":1049,"y":1016,"fill":1017,"style":967,"textAnchor":349},"689.3","160ms",[166,1052,1054],{"id":1053},"verification","Verification",[15,1056,1057,1058,1060],{},"Record the interaction again: the red forced-reflow warnings should be gone, and the handler's flame chart should contain at most one small Layout block. Processing duration in the Interactions track should fall correspondingly. Add a lint rule or code review checklist item for reads after writes in handlers; there is no reliable automated detection, but traces in CI (with LoAF's ",[33,1059,206],{}," asserted below a few milliseconds for scripted interactions) catch regressions.",[166,1062,1064],{"id":1063},"properties-that-force-layout","Properties That Force Layout",[15,1066,1067],{},"A practical list of reads to watch for inside handlers, all of which force style and\u002For layout if the DOM has pending changes:",[171,1069,1070,1087,1103,1109,1122],{},[174,1071,1072,1073,36,1076,36,1079,36,1082,36,1085,816],{},"Element geometry: ",[33,1074,1075],{},"offsetTop\u002FLeft\u002FWidth\u002FHeight",[33,1077,1078],{},"clientTop\u002FLeft\u002FWidth\u002FHeight",[33,1080,1081],{},"scrollTop\u002FLeft\u002FWidth\u002FHeight",[33,1083,1084],{},"getClientRects()",[33,1086,39],{},[174,1088,1089,1090,36,1093,36,1096,36,1099,1102],{},"Scrolling: ",[33,1091,1092],{},"scrollBy()",[33,1094,1095],{},"scrollTo()",[33,1097,1098],{},"scrollIntoView()",[33,1100,1101],{},"focus()"," (which may scroll).",[174,1104,1105,1106,1108],{},"Computed style: ",[33,1107,45],{}," and reading its layout-dependent properties.",[174,1110,1111,1112,36,1115,36,1118,1121],{},"Window: ",[33,1113,1114],{},"innerWidth\u002FinnerHeight",[33,1116,1117],{},"scrollX\u002FscrollY",[33,1119,1120],{},"getSelection()"," in some cases.",[174,1123,1124,1125,1128,1129,1132],{},"Text: ",[33,1126,1127],{},"innerText"," (which depends on layout; ",[33,1130,1131],{},"textContent"," does not).",[15,1134,1135,1136,1138,1139,1141],{},"The last one is a frequent surprise: reading ",[33,1137,1127],{}," to get an element's text forces layout, while ",[33,1140,1131],{}," does not. Swapping them in a hot path is often a free win.",[1143,1144,1147,1151],"details",{"className":1145},[1146],"faq-item",[1148,1149,1150],"summary",{},"How do I find forced layouts caused by third-party scripts?",[15,1152,1153],{},"In the trace, expand the call stack above the purple Layout block; the top frames show which script's function read layout. Vendor scripts that measure elements on click — heatmaps, scroll-depth trackers, visibility checks for ad viewability — are common culprits. You cannot change their code, but you can delay their initialisation, configure them to sample fewer elements, or load them only on a subset of sessions.",[166,1155,1157],{"id":1156},"faq","FAQ",[1143,1159,1161,1164],{"className":1160},[1146],[1148,1162,1163],{},"Is one forced layout per interaction acceptable?",[15,1165,1166],{},"Usually. A single forced layout costs roughly what the next frame's layout would have cost anyway — the work moves earlier rather than doubling. The problem is repetition: every additional write-then-read cycle adds another full layout.",[1143,1168,1170,1173],{"className":1169},[1146],[1148,1171,1172],{},"Do frameworks prevent layout thrashing?",[15,1174,1175,1176,1179],{},"Frameworks batch their own DOM writes into a commit, which avoids thrashing between framework updates. They cannot prevent your code — or a library's — from reading layout in an effect or handler between writes. ",[33,1177,1178],{},"useLayoutEffect"," in React is a common source, because it runs synchronously after the DOM is mutated: any measurement there forces layout.",[1143,1181,1183,1186],{"className":1182},[1146],[1148,1184,1185],{},"Does reading layout in a requestAnimationFrame callback ever force layout?",[15,1187,1188],{},"It can, if you write first. Inside a rAF callback, style and layout for the coming frame have not run yet; a read before any write in that frame uses the previous frame's layout and is cheap, but a write followed by a read in the same callback forces layout exactly as it would in an event handler. Keep the same discipline everywhere: reads first, then writes.",[166,1190,1192],{"id":1191},"related","Related",[171,1194,1195,1202,1209],{},[174,1196,1197,1201],{},[19,1198,1200],{"href":1199},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Ffinding-forced-reflows-in-the-performance-panel\u002F","Finding forced reflows in the Performance panel"," — a deeper DevTools walkthrough.",[174,1203,1204,1208],{},[19,1205,1207],{"href":1206},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fbatching-dom-reads-and-writes\u002F","Batching DOM reads and writes"," — patterns and libraries for read\u002Fwrite scheduling.",[174,1210,1211,1215],{},[19,1212,1214],{"href":1213},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Freducing-presentation-delay-from-large-dom-updates\u002F","Reducing presentation delay from large DOM updates"," — when layout cost lands after the handler instead.",[1217,1218,1220],"script",{"type":1219},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Fix INP Caused by Forced Layout Inside Event Handlers\",\n  \"description\": \"How read-after-write DOM access inside event handlers forces layout, how to find it in a trace, and how to restructure handlers to avoid it.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Separate reads from writes\",\n      \"text\": \"Gather every measurement first, then apply every mutation.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Defer reads to the next frame when they do not need to be synchronous\",\n      \"text\": \"If a measurement only drives a follow-up visual adjustment, read it in requestAnimationFrame — layout will already have happened for the frame, so the read is free.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Use observers instead of polling layout\",\n      \"text\": \"ResizeObserver and IntersectionObserver deliver sizes and visibility after layout, asynchronously, with no forced reflow.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Prefer CSS over measured JavaScript animation\",\n      \"text\": \"Many measure-then-animate patterns exist only to animate height.\"\n    }\n  ]\n}\n",[1217,1222,1223],{"type":1219},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix INP Caused by Forced Layout Inside Event Handlers\",\n  \"description\": \"How read-after-write DOM access inside event handlers forces layout, how to find it in a trace, and how to restructure handlers to avoid it.\",\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\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers\u002F\"\n  }\n}\n",[1217,1225,1226],{"type":1219},"\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\": \"Profiling Event Handlers for INP\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Fixing INP from Forced Layout in Event Handlers\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers\u002F\"\n    }\n  ]\n}\n",[1228,1229,1230],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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":369,"searchDepth":382,"depth":382,"links":1232},[1233,1234,1235,1241,1242,1243,1244],{"id":168,"depth":382,"text":169},{"id":216,"depth":382,"text":217},{"id":353,"depth":382,"text":354,"children":1236},[1237,1238,1239,1240],{"id":358,"depth":437,"text":359},{"id":560,"depth":437,"text":561},{"id":668,"depth":437,"text":669},{"id":819,"depth":437,"text":820},{"id":1053,"depth":382,"text":1054},{"id":1063,"depth":382,"text":1064},{"id":1156,"depth":382,"text":1157},{"id":1191,"depth":382,"text":1192},"How read-after-write DOM access inside event handlers forces layout, how to find it in a trace, and how to restructure handlers to avoid it.","md",{"slug":1248,"type":1249,"breadcrumb":1250,"datePublished":1258,"dateModified":1258},"fixing-inp-from-forced-layout-in-event-handlers","article",[1251,1254,1255,1256],{"name":1252,"url":1253},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1257},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers",{"title":1261,"description":1262},"Fixing INP from Forced Synchronous Layout in Handlers","Reading offsetHeight or getBoundingClientRect after a DOM write forces synchronous layout inside your handler. Find the reads and batch them to cut INP.","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-inp-from-forced-layout-in-event-handlers\u002Findex","WxVtUmRM5KW455q7OI_MoHJOpf9NhUyAwER-rvmLOzk",[1266,1270],{"title":1267,"path":1268,"stem":1269},"Finding the Slowest Interaction in the Performance Panel","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffinding-the-slowest-interaction-in-the-performance-panel","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffinding-the-slowest-interaction-in-the-performance-panel\u002Findex",{"title":1271,"path":1272,"stem":1273},"Fixing Slow INP on Text Inputs and Typeahead","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-slow-inp-on-text-inputs-and-typeahead","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-slow-inp-on-text-inputs-and-typeahead\u002Findex",1791308078575]