[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-long-animation-frames-api":44,"related-long-animation-frames-api":259,"code:plain:true:1eeffec":403,"code:js:true:1pa94tj":404,"code:js:true:l81nxc":405,"code:plain:true:yt41uo":406,"code:js:true:nta1xi":407,"code:js:true:93szkc":408,"search-suggestions":409,"comments-01a10fd5-be9a-719b-8acf-e0e930c602f8":435},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":242,"seo":243,"translationGroupId":246,"series":48,"podcastUrl":48,"verticalId":5,"thread":247,"assessments":249,"translations":255,"quiz":257},"01a10fd5-be9a-719b-8acf-e0e930c602f8","long-animation-frames-api","Your Lighthouse Score Is 100. Long Animation Frames Explain the Jank.",null,"Lighthouse says 100 and CI is green, but a user still says the filter button feels sluggish. The Long Animation Frames API names the exact script and phase that blocked the frame — here's how to read it instead of squinting at a flame chart.","\u002Fmedia\u002Fcovers\u002Flong-animation-frames-api.png",6,"2026-10-11T12:25:55.553Z",37,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"performance","Performance",{"slug":70,"name":71,"color":48},"webdev","Webdev",{"slug":73,"name":74,"color":48},"programming","Programming",{"assessments":76},1,{"slug":46,"title":78},"Long Animation Frames — interactive playground",{"blocks":80,"version":76},[81,85,88,93,96,99,105,108,111,116,119,122,125,128,132,135,138,142,145,149,152,156,159,162,165,168,171,175,178,181,184,187,190,193,196,199,202,205,208,211,214,217,224,227,230,233,236],{"id":82,"html":83,"type":84},"b1","\u003Cp>Lighthouse says 100. CI is green. Then a support ticket comes in: &quot;the filter button on the dashboard feels stuck for a second.&quot; You open the page yourself, click the same filter, and feel it too — a tiny freeze between the click and the list updating. Lighthouse never saw this, because Lighthouse scores page \u003Cem>load\u003C\u002Fem>. Nobody clicked anything during the audit.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>This is the gap between &quot;fast&quot; and &quot;doesn&#39;t feel fast&quot;: a single slow response to a single interaction, days after the page finished loading. The browser has had an API for this since 2017 — Long Tasks — and it has one deep flaw that makes it almost useless for exactly this kind of ticket.\u003C\u002Fp>",{"id":89,"html":90,"text":91,"type":92,"level":31},"b3","The wrong way: squinting at a flame chart for a name that isn&#39;t there","The wrong way: squinting at a flame chart for a name that isn't there","heading",{"id":94,"html":95,"type":84},"b4","\u003Cp>Here&#39;s the instinctive move. Open DevTools, hit record, click the filter button, stop recording, and scrub through the \u003Cstrong>Performance\u003C\u002Fstrong> panel looking for the red-flagged block over 50ms.\u003C\u002Fp>",{"id":97,"html":98,"type":84},"b5","\u003Cp>You find it. It&#39;s purple, it&#39;s wide, and when you click it, the summary says:\u003C\u002Fp>",{"id":100,"code":101,"type":102,"language":103,"highlight":104},"b6","Task\nDuration: 187.00 ms","code","plain",[],{"id":106,"html":107,"type":84},"b7","\u003Cp>That&#39;s it. No function name you recognize — just &quot;Task,&quot; sometimes attributed to a minified vendor chunk as \u003Ccode>(anonymous)\u003C\u002Fcode>, sometimes split across three or four stacked calls that all blend into the same purple block. If the slow code lives in a third-party script (an analytics tag, a UI library, a chat widget), the browser won&#39;t even give you a stack — cross-origin scripts get stripped from the trace for privacy reasons, so the one task you actually care about is the one with the least information attached to it.\u003C\u002Fp>",{"id":109,"html":110,"type":84},"b8","\u003Cp>This is also exactly what the underlying \u003Ccode>longtask\u003C\u002Fcode> entry type gives you programmatically. It&#39;s not a DevTools limitation — the API itself reports a duration and almost nothing else:\u003C\u002Fp>",{"id":112,"code":113,"type":102,"language":114,"highlight":115},"b9","new PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(entry.duration); \u002F\u002F 187\n    \u002F\u002F ...and basically nothing else usable.\n  }\n}).observe({ type: \"longtask\", buffered: true });","js",[],{"id":117,"html":118,"type":84},"b10","\u003Cp>You know \u003Cem>that\u003C\u002Fem> something took 187ms. You still don&#39;t know \u003Cem>what\u003C\u002Fem>. So the next move is usually wrapping suspiciously-large chunks of code in \u003Ccode>console.time\u003C\u002Fcode>\u002F\u003Ccode>console.timeEnd\u003C\u002Fcode> and reloading, hoping you guessed the right function on the first try. On a codebase with more than one engineer, you rarely do.\u003C\u002Fp>",{"id":120,"html":121,"text":121,"type":92,"level":31},"b11","The real diagnosis: the frame, not just the task",{"id":123,"html":124,"type":84},"b12","\u003Cp>A dropped frame isn&#39;t only JavaScript. By the time the browser paints a frame, it ran your event handler, then recalculated style, then ran layout, then painted — and any one of those steps, or the hand-off between them, can be where the 187ms actually went. \u003Ccode>longtask\u003C\u002Fcode> only ever measured the &quot;ran your event handler&quot; part, as one undifferentiated blob.\u003C\u002Fp>",{"id":126,"html":127,"type":84},"b13","\u003Cp>The \u003Cstrong>Long Animation Frames API\u003C\u002Fstrong> (LoAF) reports on the whole frame instead, and names what happened inside it. Where it&#39;s supported, you observe \u003Ccode>&quot;long-animation-frame&quot;\u003C\u002Fcode> instead of \u003Ccode>&quot;longtask&quot;\u003C\u002Fcode>:\u003C\u002Fp>",{"id":129,"code":130,"type":102,"language":114,"highlight":131},"b14","new PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(\"frame duration:\", entry.duration);\n    console.log(\"blocked input for:\", entry.blockingDuration);\n\n    for (const script of entry.scripts) {\n      console.log({\n        invoker: script.invoker,       \u002F\u002F e.g. \"BUTTON#filter-btn.onclick\"\n        source: script.sourceURL,\n        duration: script.duration,\n        forcedLayout: script.forcedStyleAndLayoutDuration,\n      });\n    }\n  }\n}).observe({ type: \"long-animation-frame\", buffered: true });",[],{"id":133,"html":134,"type":84},"b15","\u003Cp>Instead of one opaque &quot;Task,&quot; each \u003Ccode>PerformanceLongAnimationFrameTiming\u003C\u002Fcode> entry hands you an array of \u003Ccode>PerformanceScriptTiming\u003C\u002Fcode> objects — one per script that ran during that frame, each with a \u003Ccode>duration\u003C\u002Fcode>, a \u003Ccode>sourceURL\u003C\u002Fcode>, and an \u003Ccode>invoker\u003C\u002Fcode> that names the actual handler (&quot;\u003Ccode>BUTTON#filter-btn.onclick\u003C\u002Fcode>&quot;, not &quot;anonymous&quot;). It also exposes \u003Ccode>renderStart\u003C\u002Fcode> and \u003Ccode>styleAndLayoutStart\u003C\u002Fcode>, so you can see where the frame&#39;s time actually went: script execution, or the browser&#39;s own style\u002Flayout\u002Fpaint work after your code handed off.\u003C\u002Fp>",{"id":136,"html":137,"type":84},"b16","\u003Cp>Run that observer against the filter-button ticket and the blob stops being a blob:\u003C\u002Fp>",{"id":139,"code":140,"type":102,"language":103,"highlight":141},"b17","frame duration: 187\nblocked input for: 134\n{\n  invoker: \"BUTTON#filter-btn.onclick\",\n  source: \"https:\u002F\u002Fapp.example.com\u002Fdashboard.js\",\n  duration: 162,\n  forcedLayout: 71\n}",[],{"id":143,"html":144,"type":84},"b18","\u003Cp>\u003Ccode>forcedStyleAndLayoutDuration: 71\u003C\u002Fcode> is the tell. That&#39;s time spent on \u003Cem>forced\u003C\u002Fem> layout — reading a layout-dependent property like \u003Ccode>offsetHeight\u003C\u002Fcode> right after writing to the DOM, which makes the browser stop and recalculate synchronously instead of waiting for its normal paint schedule. The click handler wasn&#39;t slow because the filter logic was heavy; it was slow because it wrote to the DOM, then immediately read a size back from it, in a loop, once per filtered row.\u003C\u002Fp>",{"id":146,"html":147,"text":148,"type":92,"level":31},"b19","The fix, once you know what you&#39;re fixing","The fix, once you know what you're fixing",{"id":150,"html":151,"type":84},"b20","\u003Cp>Knowing the handler&#39;s name and that 71 of its 162ms went to forced layout turns a guessing game into a code review. The fix here is the standard one for layout thrash: stop interleaving reads and writes. Batch every DOM write first, then read sizes once, after:\u003C\u002Fp>",{"id":153,"code":154,"type":102,"language":114,"highlight":155},"b21","\u002F\u002F before: read-write-read-write, once per row — forces layout every iteration\nrows.forEach((row) => {\n  row.style.height = computeHeight(row);\n  totalHeight += row.offsetHeight; \u002F\u002F forces a synchronous layout recalculation\n});\n\n\u002F\u002F after: write everything, then read everything, once\nrows.forEach((row) => {\n  row.style.height = computeHeight(row);\n});\ntotalHeight = rows.reduce((sum, row) => sum + row.offsetHeight, 0);",[],{"id":157,"html":158,"type":84},"b22","\u003Cp>Re-run the observer and \u003Ccode>forcedStyleAndLayoutDuration\u003C\u002Fcode> on that entry drops to near zero, because there&#39;s no longer a read sandwiched between writes forcing the browser to recompute layout mid-loop. The frame still does the same amount of work — it just stops forcing the browser to do that work twice.\u003C\u002Fp>",{"id":160,"html":161,"text":161,"type":92,"level":31},"b23","Why this matters beyond one button",{"id":163,"html":164,"type":84},"b24","\u003Cp>This isn&#39;t a one-off diagnostic trick. \u003Cstrong>Interaction to Next Paint (INP)\u003C\u002Fstrong> — the Core Web Vital that replaced First Input Delay — measures exactly the gap between a user&#39;s interaction and the frame that responds to it. A long animation frame that overlaps a click, keypress, or tap is, by definition, the thing making your INP score worse. \u003Ccode>blockingDuration\u003C\u002Fcode> on a LoAF entry is close kin to what&#39;s slowing that metric down, which is why the Chrome team built this API specifically to make INP regressions debuggable in production, not just in a local DevTools session you happened to be recording at the right moment.\u003C\u002Fp>",{"id":166,"html":167,"text":167,"type":92,"level":31},"b25","The honest caveat",{"id":169,"html":170,"type":84},"b26","\u003Cp>The Long Animation Frames API shipped in Chrome and Edge 123 (March 2024), after an origin trial — Firefox and Safari don&#39;t implement it yet, and there&#39;s no public commitment on when or whether they will. If your users are meaningfully split across browsers, you&#39;re diagnosing the Chromium slice of your traffic, not all of it. Feature-detect before you rely on it:\u003C\u002Fp>",{"id":172,"code":173,"type":102,"language":114,"highlight":174},"b27","if (PerformanceObserver.supportedEntryTypes.includes(\"long-animation-frame\")) {\n  \u002F\u002F use LoAF\n} else if (PerformanceObserver.supportedEntryTypes.includes(\"longtask\")) {\n  \u002F\u002F fall back to duration-only longtask — better than nothing\n}",[],{"id":176,"html":177,"type":84},"b28","\u003Cp>That&#39;s a real gap, not a footnote to skip past. But for the browser where it does exist, it turns &quot;something took 187ms&quot; into &quot;this handler forced layout 71 of those milliseconds, here&#39;s the file&quot; — which is the difference between a ticket you can act on and one you close as &quot;couldn&#39;t reproduce.&quot;\u003C\u002Fp>",{"id":179,"html":180,"type":84},"b29","\u003C!-- playground:start -->",{"id":182,"html":183,"text":183,"type":92,"level":31},"b30","🎮 Try it yourself",{"id":185,"html":186,"type":84},"b31","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":188,"html":189,"type":84},"b32","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":191,"html":192,"type":84},"b33","\u003C!-- playground:end -->",{"id":194,"html":195,"type":84},"b34","\u003Cp>Next time a user says a page &quot;just feels slow&quot; and your Lighthouse score says otherwise, open the console and observe \u003Ccode>long-animation-frame\u003C\u002Fcode> before you touch the Performance panel. What&#39;s the slowest interaction on your own site right now — and do you already know, without looking, which handler it&#39;s going to name?\u003C\u002Fp>",{"id":197,"html":198,"type":84},"b35","\u003C!-- quiz:start -->",{"id":200,"html":201,"text":201,"type":92,"level":31},"b36","🧠 Test yourself",{"id":203,"html":204,"type":84},"b37","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":206,"html":207,"type":84},"b38","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":209,"html":210,"type":84},"b39","\u003C!-- quiz:end -->",{"id":212,"html":213,"type":84},"b40","\u003C!-- related:start -->",{"id":215,"html":216,"text":216,"type":92,"level":31},"b41","📚 Read next",{"id":218,"type":219,"items":220,"ordered":18},"b42","list",[221,222,223],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety\">My .catch() Never Ran. The Bug Was Two Lines Above It\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftemporal-api-date-mutation-trap\">addDays() Mutated a Date Three Components Away From Where I Called It\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\">localStorage Isn&#39;t Free — It&#39;s Blocking Your Main Thread\u003C\u002Fa>",{"id":225,"html":226,"type":84},"b43","\u003C!-- related:end -->",{"id":228,"type":229},"b44","divider",{"id":231,"html":232,"type":84},"b45","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":234,"html":235,"type":84},"b46","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":237,"type":219,"items":238,"ordered":18},"b47",[239,240,241],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Lighthouse says 100. CI is green. Then a support ticket comes in: \"the filter button on the dashboard feels stuck for a second.\" You open the page yourself, click the same filter, and feel it too — a tiny freeze between the click and the list updating. Lighthouse never saw this, because Lighthouse scores page *load*. Nobody clicked anything during the audit.\n\nThis is the gap between \"fast\" and \"doesn't feel fast\": a single slow response to a single interaction, days after the page finished loading. The browser has had an API for this since 2017 — Long Tasks — and it has one deep flaw that makes it almost useless for exactly this kind of ticket.\n\n## The wrong way: squinting at a flame chart for a name that isn't there\n\nHere's the instinctive move. Open DevTools, hit record, click the filter button, stop recording, and scrub through the **Performance** panel looking for the red-flagged block over 50ms.\n\nYou find it. It's purple, it's wide, and when you click it, the summary says:\n\n```\nTask\nDuration: 187.00 ms\n```\n\nThat's it. No function name you recognize — just \"Task,\" sometimes attributed to a minified vendor chunk as `(anonymous)`, sometimes split across three or four stacked calls that all blend into the same purple block. If the slow code lives in a third-party script (an analytics tag, a UI library, a chat widget), the browser won't even give you a stack — cross-origin scripts get stripped from the trace for privacy reasons, so the one task you actually care about is the one with the least information attached to it.\n\nThis is also exactly what the underlying `longtask` entry type gives you programmatically. It's not a DevTools limitation — the API itself reports a duration and almost nothing else:\n\n```js\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(entry.duration); \u002F\u002F 187\n    \u002F\u002F ...and basically nothing else usable.\n  }\n}).observe({ type: \"longtask\", buffered: true });\n```\n\nYou know *that* something took 187ms. You still don't know *what*. So the next move is usually wrapping suspiciously-large chunks of code in `console.time`\u002F`console.timeEnd` and reloading, hoping you guessed the right function on the first try. On a codebase with more than one engineer, you rarely do.\n\n## The real diagnosis: the frame, not just the task\n\nA dropped frame isn't only JavaScript. By the time the browser paints a frame, it ran your event handler, then recalculated style, then ran layout, then painted — and any one of those steps, or the hand-off between them, can be where the 187ms actually went. `longtask` only ever measured the \"ran your event handler\" part, as one undifferentiated blob.\n\nThe **Long Animation Frames API** (LoAF) reports on the whole frame instead, and names what happened inside it. Where it's supported, you observe `\"long-animation-frame\"` instead of `\"longtask\"`:\n\n```js\nnew PerformanceObserver((list) => {\n  for (const entry of list.getEntries()) {\n    console.log(\"frame duration:\", entry.duration);\n    console.log(\"blocked input for:\", entry.blockingDuration);\n\n    for (const script of entry.scripts) {\n      console.log({\n        invoker: script.invoker,       \u002F\u002F e.g. \"BUTTON#filter-btn.onclick\"\n        source: script.sourceURL,\n        duration: script.duration,\n        forcedLayout: script.forcedStyleAndLayoutDuration,\n      });\n    }\n  }\n}).observe({ type: \"long-animation-frame\", buffered: true });\n```\n\nInstead of one opaque \"Task,\" each `PerformanceLongAnimationFrameTiming` entry hands you an array of `PerformanceScriptTiming` objects — one per script that ran during that frame, each with a `duration`, a `sourceURL`, and an `invoker` that names the actual handler (\"`BUTTON#filter-btn.onclick`\", not \"anonymous\"). It also exposes `renderStart` and `styleAndLayoutStart`, so you can see where the frame's time actually went: script execution, or the browser's own style\u002Flayout\u002Fpaint work after your code handed off.\n\nRun that observer against the filter-button ticket and the blob stops being a blob:\n\n```\nframe duration: 187\nblocked input for: 134\n{\n  invoker: \"BUTTON#filter-btn.onclick\",\n  source: \"https:\u002F\u002Fapp.example.com\u002Fdashboard.js\",\n  duration: 162,\n  forcedLayout: 71\n}\n```\n\n`forcedStyleAndLayoutDuration: 71` is the tell. That's time spent on *forced* layout — reading a layout-dependent property like `offsetHeight` right after writing to the DOM, which makes the browser stop and recalculate synchronously instead of waiting for its normal paint schedule. The click handler wasn't slow because the filter logic was heavy; it was slow because it wrote to the DOM, then immediately read a size back from it, in a loop, once per filtered row.\n\n## The fix, once you know what you're fixing\n\nKnowing the handler's name and that 71 of its 162ms went to forced layout turns a guessing game into a code review. The fix here is the standard one for layout thrash: stop interleaving reads and writes. Batch every DOM write first, then read sizes once, after:\n\n```js\n\u002F\u002F before: read-write-read-write, once per row — forces layout every iteration\nrows.forEach((row) => {\n  row.style.height = computeHeight(row);\n  totalHeight += row.offsetHeight; \u002F\u002F forces a synchronous layout recalculation\n});\n\n\u002F\u002F after: write everything, then read everything, once\nrows.forEach((row) => {\n  row.style.height = computeHeight(row);\n});\ntotalHeight = rows.reduce((sum, row) => sum + row.offsetHeight, 0);\n```\n\nRe-run the observer and `forcedStyleAndLayoutDuration` on that entry drops to near zero, because there's no longer a read sandwiched between writes forcing the browser to recompute layout mid-loop. The frame still does the same amount of work — it just stops forcing the browser to do that work twice.\n\n## Why this matters beyond one button\n\nThis isn't a one-off diagnostic trick. **Interaction to Next Paint (INP)** — the Core Web Vital that replaced First Input Delay — measures exactly the gap between a user's interaction and the frame that responds to it. A long animation frame that overlaps a click, keypress, or tap is, by definition, the thing making your INP score worse. `blockingDuration` on a LoAF entry is close kin to what's slowing that metric down, which is why the Chrome team built this API specifically to make INP regressions debuggable in production, not just in a local DevTools session you happened to be recording at the right moment.\n\n## The honest caveat\n\nThe Long Animation Frames API shipped in Chrome and Edge 123 (March 2024), after an origin trial — Firefox and Safari don't implement it yet, and there's no public commitment on when or whether they will. If your users are meaningfully split across browsers, you're diagnosing the Chromium slice of your traffic, not all of it. Feature-detect before you rely on it:\n\n```js\nif (PerformanceObserver.supportedEntryTypes.includes(\"long-animation-frame\")) {\n  \u002F\u002F use LoAF\n} else if (PerformanceObserver.supportedEntryTypes.includes(\"longtask\")) {\n  \u002F\u002F fall back to duration-only longtask — better than nothing\n}\n```\n\nThat's a real gap, not a footnote to skip past. But for the browser where it does exist, it turns \"something took 187ms\" into \"this handler forced layout 71 of those milliseconds, here's the file\" — which is the difference between a ticket you can act on and one you close as \"couldn't reproduce.\"\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nNext time a user says a page \"just feels slow\" and your Lighthouse score says otherwise, open the console and observe `long-animation-frame` before you touch the Performance panel. What's the slowest interaction on your own site right now — and do you already know, without looking, which handler it's going to name?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [My .catch() Never Ran. The Bug Was Two Lines Above It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety)\n- [addDays() Mutated a Date Three Components Away From Where I Called It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ftemporal-api-date-mutation-trap)\n- [localStorage Isn't Free — It's Blocking Your Main Thread](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":244,"description":245},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flong-animation-frames-api","Lighthouse says 100 and CI is green, but a user still says the filter button feels sluggish. The Long Animation Frames API names the exact script and phase that blocked the frame —","01a10fd5-be9a-719b-8acf-e494c61e3a7d",{"id":248,"locked":18},"01a10fd5-bebf-7726-8e3a-1745bfb51377",[250],{"id":251,"slug":46,"title":252,"_count":253},"01a10fd5-bedc-76cd-9e6f-8ece4514ec69","Long Animation Frames — test yourself",{"questions":254},8,[256],{"locale":13,"slug":46},{"id":251,"slug":46,"title":252,"_count":258,"questionCount":254},{"questions":254},{"items":260,"meta":400},[261,284,296,317,338,358,380],{"id":262,"slug":263,"title":264,"subtitle":48,"excerpt":265,"coverUrl":266,"locale":13,"readingMinutes":267,"publishedAt":268,"viewCount":269,"likeCount":19,"commentCount":19,"author":270,"vertical":271,"topic":272,"tags":275,"_count":282,"playground":283,"hasQuiz":17,"hasPlayground":17},"01a11d02-8b28-774b-9f36-fb66bece8e50","nuxt-weekly-4-migration-breaking-changes","Nuxt 4 Migration: The Breaking Changes That Actually Bite","Nuxt 3 reached end-of-life on July 31, 2026. Here's exactly what breaks moving to Nuxt 4 — directories, data fetching, TypeScript — plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-4-migration-breaking-changes.png",14,"2026-10-11T12:26:26.492Z",33,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":273,"name":274},"nuxt","Nuxt",[276,277,278,281],{"slug":273,"name":274,"color":48},{"slug":61,"name":65,"color":48},{"slug":279,"name":280,"color":48},"tutorial","Tutorial",{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":263},{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":285,"likeCount":19,"commentCount":19,"author":286,"vertical":287,"topic":288,"tags":289,"_count":294,"playground":295,"hasQuiz":17,"hasPlayground":17},38,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[290,291,292,293],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":297,"slug":298,"title":299,"subtitle":48,"excerpt":300,"coverUrl":301,"locale":13,"readingMinutes":302,"publishedAt":303,"viewCount":304,"likeCount":19,"commentCount":19,"author":305,"vertical":306,"topic":307,"tags":310,"_count":315,"playground":316,"hasQuiz":17,"hasPlayground":17},"01a117e0-acb2-740f-9391-db919729cf21","react-weekly-fragment-refs","React 19.3 Fragment Refs: Skip the Wrapper Div for a Ref","React 19.3 Fragment Refs let you ref a sibling group with no wrapper div. Learn the FragmentInstance API, its limits, and get a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Freact-weekly-fragment-refs.png",13,"2026-10-10T12:12:38.989Z",109,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":308,"name":309},"react","React",[311,312,313,314],{"slug":308,"name":309,"color":48},{"slug":61,"name":65,"color":48},{"slug":279,"name":280,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":298},{"id":318,"slug":319,"title":320,"subtitle":48,"excerpt":321,"coverUrl":322,"locale":13,"readingMinutes":323,"publishedAt":324,"viewCount":325,"likeCount":19,"commentCount":19,"author":326,"vertical":327,"topic":328,"tags":329,"_count":336,"playground":337,"hasQuiz":17,"hasPlayground":17},"01a1017f-c2ac-70af-a013-25321448f53a","unhandledrejection-swallowed-promise-errors","Stop Losing Promise Errors. `unhandledrejection` Catches Them.","A rejected promise with no .catch doesn't crash and doesn't show up in your error tracker — it just disappears. The unhandledrejection event is the global safety net most apps never wire up.","\u002Fmedia\u002Fcovers\u002Funhandledrejection-swallowed-promise-errors.png",5,"2026-10-08T06:10:23.452Z",232,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[330,331,332,333],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"slug":334,"name":335,"color":48},"debugging","Debugging",{"assessments":76},{"slug":319},{"id":339,"slug":340,"title":341,"subtitle":48,"excerpt":342,"coverUrl":343,"locale":13,"readingMinutes":51,"publishedAt":344,"viewCount":345,"likeCount":19,"commentCount":19,"author":346,"vertical":347,"topic":348,"tags":349,"_count":356,"playground":357,"hasQuiz":17,"hasPlayground":17},"01a0fb3c-4e87-7604-9b6e-d9f518d232be","fetch-retry-exponential-backoff","Stop Retrying Immediately. Exponential Backoff Fixes It.","Retrying a failed fetch() right away feels responsible — until the server that failed was already overloaded, and five instant retries per client turn a blip into an outage. Exponential backoff with jitter is the fix.","\u002Fmedia\u002Fcovers\u002Ffetch-retry-exponential-backoff.png","2026-10-07T06:10:20.335Z",410,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[350,351,352,353],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"slug":354,"name":355,"color":48},"node","Node",{"assessments":76},{"slug":340},{"id":359,"slug":360,"title":361,"subtitle":48,"excerpt":362,"coverUrl":363,"locale":13,"readingMinutes":51,"publishedAt":364,"viewCount":365,"likeCount":19,"commentCount":19,"author":366,"vertical":367,"topic":368,"tags":369,"_count":378,"playground":379,"hasQuiz":17,"hasPlayground":17},"01a0f615-e1c2-729c-b09a-6e35ad0eba80","storage-event-cross-tab-sync","I Built Cross-Tab Logout. The storage Event Skipped One Tab.","A cross-tab 'log out everywhere' feature built on the storage event worked in every tab except the one the user actually clicked in. The storage event never fires in the tab that made the change — and that's spec, not a bug.","\u002Fmedia\u002Fcovers\u002Fstorage-event-cross-tab-sync.png","2026-10-06T06:11:02.312Z",416,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[370,371,372,375],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":373,"name":374,"color":48},"frontend","Frontend",{"slug":376,"name":377,"color":48},"browser","Browser",{"assessments":76},{"slug":360},{"id":381,"slug":382,"title":383,"subtitle":48,"excerpt":384,"coverUrl":385,"locale":13,"readingMinutes":302,"publishedAt":386,"viewCount":387,"likeCount":19,"commentCount":19,"author":388,"vertical":389,"topic":390,"tags":393,"_count":398,"playground":399,"hasQuiz":17,"hasPlayground":17},"01a0af18-2d15-7347-b2f4-6cdfa2ce56fa","vue-weekly-definemodel-v-model-contract","Vue defineModel: The v-model Contract, Explained","Vue's defineModel macro replaces the modelValue prop and update:modelValue event with one ref. The exact v-model contract behind it, verified against Vue 3.5.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-definemodel-v-model-contract.png","2026-10-05T06:11:02.623Z",409,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":391,"name":392},"vue","Vue",[394,395,396,397],{"slug":391,"name":392,"color":48},{"slug":61,"name":65,"color":48},{"slug":279,"name":280,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":382},{"page":76,"perPage":401,"total":304,"totalPages":402},7,16,"\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">Task\nDuration: 187.00 ms\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>duration\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F 187\n\u003C\u002Fspan>    \u003Cspan class=\"shj-cmnt\">\u002F\u002F ...and basically nothing else usable.\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"longtask\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>list\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> entry \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> list\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getEntries\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"frame duration:\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>duration\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"blocked input for:\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>blockingDuration\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n    \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> script \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scripts\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n        invoker\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> script\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>invoker\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>       \u003Cspan class=\"shj-cmnt\">\u002F\u002F e.g. \"BUTTON#filter-btn.onclick\"\n\u003C\u002Fspan>        source\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> script\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>sourceURL\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n        duration\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> script\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>duration\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n        forcedLayout\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> script\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>forcedStyleAndLayoutDuration\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> type\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"long-animation-frame\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> buffered\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">frame duration: 187\nblocked input for: 134\n{\n  invoker: \u003Cspan class=\"shj-str\">\"BUTTON#filter-btn.onclick\"\u003C\u002Fspan>,\n  source: \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fapp.example.com\u002Fdashboard.js\"\u003C\u002Fspan>,\n  duration: 162,\n  forcedLayout: 71\n}\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F before: read-write-read-write, once per row — forces layout every iteration\n\u003C\u002Fspan>rows\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>row\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  row\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>height \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">computeHeight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>row\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  totalHeight \u003Cspan class=\"shj-oper\">+=\u003C\u002Fspan> row\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>offsetHeight; \u003Cspan class=\"shj-cmnt\">\u002F\u002F forces a synchronous layout recalculation\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F after: write everything, then read everything, once\n\u003C\u002Fspan>rows\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>row\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  row\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>height \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">computeHeight\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>row\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\ntotalHeight \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> rows\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">reduce\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>sum\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> row\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> sum \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> row\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>offsetHeight\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>supportedEntryTypes\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">includes\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"long-animation-frame\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F use LoAF\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">else\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-class\">PerformanceObserver\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>supportedEntryTypes\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">includes\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"longtask\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F fall back to duration-only longtask — better than nothing\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",[410,412,413,415,417,421,423,426,427,429,430,434],{"slug":70,"name":71,"articles":411},133,{"slug":61,"name":65,"articles":304},{"slug":373,"name":374,"articles":414},82,{"slug":279,"name":280,"articles":416},51,{"slug":418,"name":419,"articles":420},"css","Css",42,{"slug":67,"name":68,"articles":422},18,{"slug":424,"name":425,"articles":422},"typescript","Typescript",{"slug":308,"name":309,"articles":402},{"slug":376,"name":377,"articles":428},12,{"slug":354,"name":355,"articles":428},{"slug":431,"name":432,"articles":433},"accessibility","Accessibility",9,{"slug":73,"name":74,"articles":433},{"locked":18,"total":19,"comments":436},[]]