[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-layout-thrashing-forced-reflow":44,"search-suggestions":60,"quiz-article-layout-thrashing-forced-reflow":109,"path-membership-layout-thrashing-forced-reflow":296},[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,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a13908-8be8-71e8-82e2-49165fd79356","layout-thrashing-forced-reflow","PRACTICE_QUIZ","Layout thrashing: forced reflow quiz","Check what actually forces a synchronous layout recalculation, why the read-then-write fix works, and a couple of the trickier edge cases around it.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",146,{"slug":66,"name":67,"articles":68},"javascript","Javascript",119,{"slug":70,"name":71,"articles":72},"frontend","Frontend",87,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",56,{"slug":78,"name":79,"articles":80},"css","Css",44,{"slug":82,"name":83,"articles":84},"performance","Performance",24,{"slug":86,"name":87,"articles":88},"typescript","Typescript",19,{"slug":90,"name":91,"articles":92},"react","React",18,{"slug":94,"name":95,"articles":96},"browser","Browser",14,{"slug":98,"name":99,"articles":100},"node","Node",12,{"slug":102,"name":103,"articles":104},"programming","Programming",11,{"slug":106,"name":107,"articles":108},"accessibility","Accessibility",10,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":114,"publishedAt":115,"viewCount":116,"likeCount":19,"commentCount":19,"author":117,"vertical":122,"topic":123,"tags":125,"_count":130,"playground":132,"body":134,"bodyMd":282,"seo":283,"translationGroupId":286,"series":52,"podcastUrl":52,"verticalId":5,"thread":287,"assessments":289,"translations":292,"quiz":294},"01a13908-8b6a-71f8-9fbc-606a126441c0","Your 'Expand All' Button Is Quietly Causing Layout Thrashing","An 'expand all' click that stutters isn't a slow CPU — it's layout thrashing, a loop that forces the browser to recalculate layout once per row instead of once per click. Here's how to spot it and the two-line fix.","\u002Fmedia\u002Fcovers\u002Flayout-thrashing-forced-reflow.png",5,"2026-10-19T06:10:27.840Z",27,{"id":118,"name":119,"username":120,"avatarUrl":52,"headline":121},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":124},"JavaScript",[126,127,128,129],{"slug":66,"name":67,"color":52},{"slug":82,"name":83,"color":52},{"slug":62,"name":63,"color":52},{"slug":94,"name":95,"color":52},{"assessments":131},1,{"slug":46,"title":133},"Layout thrashing — interactive playground",{"blocks":135,"version":131},[136,140,143,147,150,156,159,162,165,168,171,174,177,180,184,187,190,193,196,199,202,205,208,211,218,221,224,228,231,234,237,240,243,246,249,252,255,258,264,267,270,273,276],{"id":137,"html":138,"type":139},"b1","\u003Cp>You click &quot;Expand all&quot; on a settings page or an FAQ list, and for a moment, nothing happens. Not nothing-nothing — the click registered, the network tab is quiet, there&#39;s no spinner. The page is just... stuck, for a beat, before every row snaps open at once.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>Nobody wrote a slow function on purpose. The code probably looks completely reasonable. That&#39;s what makes this one worth knowing: the bug isn&#39;t in what the loop does, it&#39;s in the \u003Cem>order\u003C\u002Fem> it does it in.\u003C\u002Fp>",{"id":144,"html":145,"text":145,"type":146,"level":31},"b3","The loop that looks fine","heading",{"id":148,"html":149,"type":139},"b4","\u003Cp>Here&#39;s a version of the handler almost everyone writes the first time:\u003C\u002Fp>",{"id":151,"code":152,"type":153,"language":154,"highlight":155},"b5","function expandAll(rows) {\n  rows.forEach((row) => {\n    const content = row.querySelector(\".row-content\");\n    const height = content.scrollHeight; \u002F\u002F read\n    content.style.height = height + \"px\"; \u002F\u002F write\n  });\n}","code","js",[],{"id":157,"html":158,"type":139},"b6","\u003Cp>Read the current content height, set the element to that height so the \u003Ccode>height: 0 → height: auto\u003C\u002Fcode>-style expand animation has something to animate toward. Repeat for every row. It&#39;s clean, it&#39;s obviously correct, and it ships.\u003C\u002Fp>",{"id":160,"html":161,"type":139},"b7","\u003Cp>Open your browser&#39;s performance profiler, click &quot;Expand all&quot; on a list of forty or fifty rows, and record. You&#39;ll see a stack of narrow purple bars, one per row, each labeled something like \u003Cstrong>&quot;Forced reflow&quot;\u003C\u002Fstrong> or \u003Cstrong>&quot;Forced synchronous layout&quot;\u003C\u002Fstrong>. That label is the browser telling you exactly what just happened, in its own words.\u003C\u002Fp>",{"id":163,"html":164,"text":164,"type":146,"level":31},"b8","Why reading a height can be slow",{"id":166,"html":167,"type":139},"b9","\u003Cp>Browsers don&#39;t recompute layout after every style change. They \u003Cem>batch\u003C\u002Fem> it — style and layout writes pile up in a queue, and the engine runs one layout pass right before the next paint. That&#39;s why setting ten CSS properties in a row usually costs about the same as setting one.\u003C\u002Fp>",{"id":169,"html":170,"type":139},"b10","\u003Cp>But some reads break the batch. \u003Ccode>scrollHeight\u003C\u002Fcode>, \u003Ccode>offsetHeight\u003C\u002Fcode>, \u003Ccode>offsetWidth\u003C\u002Fcode>, \u003Ccode>clientWidth\u003C\u002Fcode>, \u003Ccode>getBoundingClientRect()\u003C\u002Fcode>, \u003Ccode>getComputedStyle()\u003C\u002Fcode> — anything that hands you back a real number has to be \u003Cem>correct right now\u003C\u002Fem>, not &quot;correct as of the last paint.&quot; If there&#39;s a pending style write the browser hasn&#39;t applied to layout yet, it can&#39;t answer honestly without doing the work first. So it does the work first: it flushes every queued change and recalculates layout, synchronously, on the spot, then hands you the number.\u003C\u002Fp>",{"id":172,"html":173,"type":139},"b11","\u003Cp>Now look at the loop again. Each iteration writes \u003Ccode>content.style.height\u003C\u002Fcode>, which invalidates layout. The \u003Cem>next\u003C\u002Fem> iteration reads \u003Ccode>scrollHeight\u003C\u002Fcode> on a different element — but the browser doesn&#39;t know your next read doesn&#39;t care about the previous write, so it has to flush and recalculate to be sure. Forty rows means forty forced layouts, each one touching the whole document, not just the row you&#39;re looking at. That&#39;s layout thrashing: alternating reads and writes that force the same work the browser was trying to batch away, over and over, inside a single click handler, on the main thread, blocking everything else — including the next frame.\u003C\u002Fp>",{"id":175,"html":176,"text":176,"type":146,"level":31},"b12","The fix is reordering, not rewriting",{"id":178,"html":179,"type":139},"b13","\u003Cp>You don&#39;t need a library for this. You need two loops instead of one:\u003C\u002Fp>",{"id":181,"code":182,"type":153,"language":154,"highlight":183},"b14","function expandAll(rows) {\n  const contents = rows.map((row) => row.querySelector(\".row-content\"));\n\n  \u002F\u002F 1. read every height first — one flush, if any, covers all of them\n  const heights = contents.map((c) => c.scrollHeight);\n\n  \u002F\u002F 2. write every height second — no reads in between to force a recalculation\n  contents.forEach((c, i) => {\n    c.style.height = heights[i] + \"px\";\n  });\n}",[],{"id":185,"html":186,"type":139},"b15","\u003Cp>Same inputs, same visual result, same number of DOM nodes touched. The only change is that every read happens before every write. The browser still has to recalculate layout once, to answer the first \u003Ccode>scrollHeight\u003C\u002Fcode> honestly — but after that, it&#39;s just queuing style changes, which it was always good at batching. One forced layout instead of N.\u003C\u002Fp>",{"id":188,"html":189,"type":139},"b16","\u003C!-- playground:start -->",{"id":191,"html":192,"text":192,"type":146,"level":31},"b17","🎮 Try it yourself",{"id":194,"html":195,"type":139},"b18","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flayout-thrashing-forced-reflow\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":197,"html":198,"type":139},"b19","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":200,"html":201,"type":139},"b20","\u003C!-- playground:end -->",{"id":203,"html":204,"type":139},"b21","\u003Cp>This is the whole idea behind the old &quot;FastDOM&quot; pattern, and it&#39;s worth internalizing as a rule rather than a library: \u003Cstrong>never interleave a layout read with a layout write inside a loop.\u003C\u002Fstrong> Batch the reads, batch the writes, in that order.\u003C\u002Fp>",{"id":206,"html":207,"text":207,"type":146,"level":31},"b22","Where this hides in real code",{"id":209,"html":210,"type":139},"b23","\u003Cp>It&#39;s rarely written as nakedly as the example above. It shows up as:\u003C\u002Fp>",{"id":212,"type":213,"items":214,"ordered":18},"b24","list",[215,216,217],"A \u003Ccode>ResizeObserver\u003C\u002Fcode> or scroll handler that reads \u003Ccode>getBoundingClientRect()\u003C\u002Fcode> on one element, then immediately updates another element&#39;s size based on it, inside a loop over a list.","A virtualized list or masonry layout that measures each item&#39;s height to position the next one — read, position, read, position.","An animation library (or a hand-rolled one) that reads the current transform of an element to compute the next frame&#39;s value, for every animated element, every frame.",{"id":219,"html":220,"type":139},"b25","\u003Cp>The common shape is always &quot;measure, then move, then measure the next thing&quot; — and the fix is always the same two passes: measure everything, then move everything.\u003C\u002Fp>",{"id":222,"html":223,"type":139},"b26","\u003Cp>If you&#39;re not sure whether your own code does this, the profiler will tell you faster than reading the source will. Record a trace around the interaction you suspect, and search for &quot;Forced reflow&quot; in the summary. If it&#39;s there, it names the exact call stack that triggered it — you don&#39;t have to guess.\u003C\u002Fp>",{"id":225,"html":226,"text":227,"type":146,"level":31},"b27","What this costs you, and what it doesn&#39;t","What this costs you, and what it doesn't",{"id":229,"html":230,"type":139},"b28","\u003Cp>This isn&#39;t a feature flag or a browser quirk you need to check support tables for — every layout engine works this way, because batching writes and flushing on-demand reads is the only way to be both fast in the common case and correct when a script actually needs a real number. There&#39;s nothing to polyfill and nothing to feature-detect. The fix costs you an array and a second loop, which is a rounding error next to the forced layouts it removes.\u003C\u002Fp>",{"id":232,"html":233,"type":139},"b29","\u003Cp>What it does cost you is a bit of discipline: every time you write a loop that both measures and changes the DOM, ask which one happens first, and whether it happens for every item before the loop moves to the next. One extra array, two passes instead of one — that&#39;s the entire price of not blocking your own main thread on your own click handler.\u003C\u002Fp>",{"id":235,"html":236,"type":139},"b30","\u003C!-- quiz:start -->",{"id":238,"html":239,"text":239,"type":146,"level":31},"b31","🧠 Test yourself",{"id":241,"html":242,"type":139},"b32","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flayout-thrashing-forced-reflow\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":244,"html":245,"type":139},"b33","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":247,"html":248,"type":139},"b34","\u003C!-- quiz:end -->",{"id":250,"html":251,"type":139},"b35","\u003Cp>Next time a click handler stutters and the network tab is empty, open the performance panel before you reach for a spinner or a \u003Ccode>setTimeout\u003C\u002Fcode>. What&#39;s the slowest &quot;obviously fine&quot; loop you&#39;ve found hiding a forced reflow? I&#39;d genuinely like to know — drop it in the comments.\u003C\u002Fp>",{"id":253,"html":254,"type":139},"b36","\u003C!-- related:start -->",{"id":256,"html":257,"text":257,"type":146,"level":31},"b37","📚 Read next",{"id":259,"type":213,"items":260,"ordered":18},"b38",[261,262,263],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpage-visibility-api-tab-aware-timers\">Your Timer Doesn&#39;t Know You Tabbed Away\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fservice-worker-stale-cache-deploy\">I Shipped a Fix. My Service Worker Kept Serving the Bug.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-trigonometric-functions-radial-layout\">Stop Writing Math.cos() in a Resize Listener\u003C\u002Fa>",{"id":265,"html":266,"type":139},"b39","\u003C!-- related:end -->",{"id":268,"type":269},"b40","divider",{"id":271,"html":272,"type":139},"b41","\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":274,"html":275,"type":139},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":277,"type":213,"items":278,"ordered":18},"b43",[279,280,281],"⭐ \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>","You click \"Expand all\" on a settings page or an FAQ list, and for a moment, nothing happens. Not nothing-nothing — the click registered, the network tab is quiet, there's no spinner. The page is just... stuck, for a beat, before every row snaps open at once.\n\nNobody wrote a slow function on purpose. The code probably looks completely reasonable. That's what makes this one worth knowing: the bug isn't in what the loop does, it's in the *order* it does it in.\n\n## The loop that looks fine\n\nHere's a version of the handler almost everyone writes the first time:\n\n```js\nfunction expandAll(rows) {\n  rows.forEach((row) => {\n    const content = row.querySelector(\".row-content\");\n    const height = content.scrollHeight; \u002F\u002F read\n    content.style.height = height + \"px\"; \u002F\u002F write\n  });\n}\n```\n\nRead the current content height, set the element to that height so the `height: 0 → height: auto`-style expand animation has something to animate toward. Repeat for every row. It's clean, it's obviously correct, and it ships.\n\nOpen your browser's performance profiler, click \"Expand all\" on a list of forty or fifty rows, and record. You'll see a stack of narrow purple bars, one per row, each labeled something like **\"Forced reflow\"** or **\"Forced synchronous layout\"**. That label is the browser telling you exactly what just happened, in its own words.\n\n## Why reading a height can be slow\n\nBrowsers don't recompute layout after every style change. They *batch* it — style and layout writes pile up in a queue, and the engine runs one layout pass right before the next paint. That's why setting ten CSS properties in a row usually costs about the same as setting one.\n\nBut some reads break the batch. `scrollHeight`, `offsetHeight`, `offsetWidth`, `clientWidth`, `getBoundingClientRect()`, `getComputedStyle()` — anything that hands you back a real number has to be *correct right now*, not \"correct as of the last paint.\" If there's a pending style write the browser hasn't applied to layout yet, it can't answer honestly without doing the work first. So it does the work first: it flushes every queued change and recalculates layout, synchronously, on the spot, then hands you the number.\n\nNow look at the loop again. Each iteration writes `content.style.height`, which invalidates layout. The *next* iteration reads `scrollHeight` on a different element — but the browser doesn't know your next read doesn't care about the previous write, so it has to flush and recalculate to be sure. Forty rows means forty forced layouts, each one touching the whole document, not just the row you're looking at. That's layout thrashing: alternating reads and writes that force the same work the browser was trying to batch away, over and over, inside a single click handler, on the main thread, blocking everything else — including the next frame.\n\n## The fix is reordering, not rewriting\n\nYou don't need a library for this. You need two loops instead of one:\n\n```js\nfunction expandAll(rows) {\n  const contents = rows.map((row) => row.querySelector(\".row-content\"));\n\n  \u002F\u002F 1. read every height first — one flush, if any, covers all of them\n  const heights = contents.map((c) => c.scrollHeight);\n\n  \u002F\u002F 2. write every height second — no reads in between to force a recalculation\n  contents.forEach((c, i) => {\n    c.style.height = heights[i] + \"px\";\n  });\n}\n```\n\nSame inputs, same visual result, same number of DOM nodes touched. The only change is that every read happens before every write. The browser still has to recalculate layout once, to answer the first `scrollHeight` honestly — but after that, it's just queuing style changes, which it was always good at batching. One forced layout instead of N.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flayout-thrashing-forced-reflow\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nThis is the whole idea behind the old \"FastDOM\" pattern, and it's worth internalizing as a rule rather than a library: **never interleave a layout read with a layout write inside a loop.** Batch the reads, batch the writes, in that order.\n\n## Where this hides in real code\n\nIt's rarely written as nakedly as the example above. It shows up as:\n\n- A `ResizeObserver` or scroll handler that reads `getBoundingClientRect()` on one element, then immediately updates another element's size based on it, inside a loop over a list.\n- A virtualized list or masonry layout that measures each item's height to position the next one — read, position, read, position.\n- An animation library (or a hand-rolled one) that reads the current transform of an element to compute the next frame's value, for every animated element, every frame.\n\nThe common shape is always \"measure, then move, then measure the next thing\" — and the fix is always the same two passes: measure everything, then move everything.\n\nIf you're not sure whether your own code does this, the profiler will tell you faster than reading the source will. Record a trace around the interaction you suspect, and search for \"Forced reflow\" in the summary. If it's there, it names the exact call stack that triggered it — you don't have to guess.\n\n## What this costs you, and what it doesn't\n\nThis isn't a feature flag or a browser quirk you need to check support tables for — every layout engine works this way, because batching writes and flushing on-demand reads is the only way to be both fast in the common case and correct when a script actually needs a real number. There's nothing to polyfill and nothing to feature-detect. The fix costs you an array and a second loop, which is a rounding error next to the forced layouts it removes.\n\nWhat it does cost you is a bit of discipline: every time you write a loop that both measures and changes the DOM, ask which one happens first, and whether it happens for every item before the loop moves to the next. One extra array, two passes instead of one — that's the entire price of not blocking your own main thread on your own click handler.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flayout-thrashing-forced-reflow\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\nNext time a click handler stutters and the network tab is empty, open the performance panel before you reach for a spinner or a `setTimeout`. What's the slowest \"obviously fine\" loop you've found hiding a forced reflow? I'd genuinely like to know — drop it in the comments.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Your Timer Doesn't Know You Tabbed Away](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpage-visibility-api-tab-aware-timers)\n- [I Shipped a Fix. My Service Worker Kept Serving the Bug.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fservice-worker-stale-cache-deploy)\n- [Stop Writing Math.cos() in a Resize Listener](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-trigonometric-functions-radial-layout)\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":111,"canonical":284,"description":285},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Flayout-thrashing-forced-reflow","An 'expand all' click that stutters isn't a slow CPU — it's layout thrashing, a loop that forces the browser to recalculate layout once per row instead of once per click. Here's ho","01a13908-8b6a-71f8-9fbc-679532c31111",{"id":288,"locked":18},"01a13908-8bc5-77e4-b521-220f23b1e21d",[290],{"id":45,"slug":46,"title":48,"_count":291},{"questions":51},[293],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":295,"questionCount":51},{"questions":51},[]]