Interleaving DOM reads and writes in a loop forces the browser to recalculate layout after every write before it can serve the next read — called "layout thrashing" (ISO-25010 time-behaviour). Reading element.offsetHeight after setting element.style.height on 100 elements triggers 100 synchronous layout recalculations instead of one. On a 60Hz display, each recalculation costs approximately 16ms of budget. One thrashing loop can consume the entire frame budget, causing the browser to drop frames and producing visible jank.
Low because layout thrashing causes visible jank and dropped frames during the specific interaction that triggers it, but does not affect baseline performance outside those code paths.
Batch all DOM reads before any DOM writes. Read from the DOM, store the values, then write back in a separate pass:
// Bad: interleaved read-write causes layout thrashing
for (const el of elements) {
el.style.height = el.scrollHeight + 'px' // read then write on every iteration
}
// Good: batch reads first, then batch writes
const heights = Array.from(elements).map(el => el.scrollHeight) // all reads
elements.forEach((el, i) => { el.style.height = heights[i] + 'px' }) // all writes
For animation-related mutations, wrap batched writes in requestAnimationFrame:
requestAnimationFrame(() => {
const updates = Array.from(elements).map(el => ({ el, height: el.scrollHeight }))
updates.forEach(({ el, height }) => { el.style.height = `${height}px` })
})
Use the FastDOM library or Chrome DevTools Performance tab → Layout to identify thrashing code paths.
ID: performance-core.script-style-efficiency.layout-thrashing
Severity: low
What to look for: Count all relevant instances and enumerate each. Review JavaScript that reads and writes to the DOM. Look for patterns like element.offsetHeight followed immediately by element.style.height = .... Check for loops that interleave reads and writes. Examine event handlers for synchronized read-write operations.
Pass criteria: DOM read operations are batched before DOM writes. requestAnimationFrame is used for coordinating multiple DOM mutations. Loops batch reads, then batch writes separately.
Fail criteria: DOM reads and writes are interleaved in loops or event handlers, causing the browser to recalculate layout repeatedly (layout thrashing). Code like for (let el of elements) { read offsetHeight; write style; }.
Skip (N/A) when: The project has minimal DOM manipulation or no dynamic updates.
Detail on fail: Provide an example of thrashing. Example: "Loop over 100 items: element.offsetHeight read, then style.height written for each. Layout recalculated 100 times in 300ms instead of once" or "Scroll event handler reads clientHeight (triggers layout) then writes transform (triggers paint) multiple times".
Remediation: Batch DOM reads and writes:
Bad (layout thrashing):
for (let el of elements) {
el.style.height = el.scrollHeight + 'px' // read then write, repeated
}
Good (batched):
const heights = elements.map(el => el.scrollHeight) // batch all reads
elements.forEach((el, i) => el.style.height = heights[i] + 'px') // batch all writes
Or with requestAnimationFrame:
function updateElements() {
requestAnimationFrame(() => {
const updates = elements.map(el => ({ el, height: el.scrollHeight }))
updates.forEach(({ el, height }) => el.style.height = height + 'px')
})
}