The Loop That Read and Wrote the DOM a Thousand Times

Each iteration measured an element and then changed a style, so every read invalidated the layout and forced a synchronous reflow, and the frame budget went…

Share
The Loop That Read and Wrote the DOM a Thousand Times. Abstract performance illustration in orange and dark grey on debugly.dev

The stuttered behaved. The stutter behaved. The chain between them is where working as designed stopped meaning working.

Here is the shape of the forced synchronous layout, and the one property that makes it expensive: a style write invalidates the layout, and a subsequent geometry read must recompute it immediately, so alternating reads and writes in a loop turns one layout into a thousand.

This was a list positioning routine in vanilla JavaScript, and the mechanics are the same in any framework that touches the DOM directly.

Why the read forces the layout

The write is the style change, and the change is the dirty, and the dirty is the layout's invalidation, and the invalidation is the pending, and the pending is the browser's optimisation, and the optimisation is the batch at the frame's end, and the end is the efficient, and the efficient is the one layout.

The read is the geometry query, and the query is the offsetHeight or the getBoundingClientRect, and the two are the current layout's requirement, and the requirement is the pending's flush, and the flush is the synchronous, and the synchronous is the immediate, and the immediate is the forced, and the forced is the layout now rather than at the frame's end.

The alternation is the loop, and the loop is the write then the read then the write then the read, and the four are the thousand layouts, and the thousand is the budget's consumption, and the consumption is the dropped frame, and the dropped is the stutter, and the stutter is the incident.

Why the batching fixes it

The reads first is the all measurements, and the measurements are the one layout, and the one layout is the flush, and the flush is the single, and the single is the cached values, and the cached is the writes, and the writes are the all changes, and the changes are the one layout at the frame's end, and the end is the two layouts total, and the two is the fix.

The two against the thousand is the five hundred times, and the times is the budget's return, and the return is the frame's completion, and the completion is the smooth, and the smooth is the fix, and the fix is the ordering, and the ordering is the discipline, because the same operations in a different order cost five hundred times less.

This is the same batching principle as the context churn that made every consumer re-render, same mechanism, different costume.

How to find it

The performance panel shows the purple. The purple is the layout, and the layout's bars are the forced, and the forced is the label, and the label is the diagnosis, and the diagnosis is the fix, and the fix is the recording, and the recording is the interaction, and the interaction is the reproduction, and the reproduction is the discipline.

The call stack shows the read. The forced layout's stack names the line, and the line is the geometry query, and the query is the fix's target, and the target is the reorder, and the reorder is the fix, and the fix is the one line's move, and the move is the discipline.

The count is the tell. The layout count per frame is the number, and the number should be the one or the two, and the two is the fine, and the fine is the not the thousand, and the thousand is the loop, and the loop is the diagnosis, and the diagnosis is the fix.

The fixes

Separate the reads from the writes. The loop becomes the two loops, and the two are the measure and the mutate, and the mutate is the after, and the after is the no interleaving, and the no interleaving is the fix, and the fix is the structure, and the structure is the discipline.

Cache the geometry. The measurement is the once, and the once is the variable, and the variable is the loop's read, and the read is the no layout, and the no layout is the fix, and the fix is the cache, and the cache is the discipline, because the geometry rarely changes inside the loop.

Use requestAnimationFrame for the visual updates. The frame callback is the batched, and the batched is the browser's schedule, and the schedule is the one layout, and the one layout is the fix, and the fix is the API, and the API is the discipline, because the writes inside the frame callback are batched by design.

Prefer the transform over the layout properties. The transform is the compositor's, and the compositor's is the no layout, and the no layout is the cheap, and the cheap is the fix, and the fix is the property choice, and the choice is the discipline, because the transform and the opacity skip the layout and the paint.

Use the fast DOM lookup tools. The library is the read-write separation, and the separation is the automatic, and the automatic is the fix, and the fix is the fastdom or the own wrapper, and the wrapper is the discipline, because the manual separation is the easy thing to forget in the next loop.

What I now do

A style write invalidates the layout and a geometry read forces it to recompute immediately, so alternating them in a loop costs one layout per iteration. Do all the reads first, cache the geometry, batch the writes inside requestAnimationFrame, and prefer transform over layout-triggering properties.

The animation dropped frames and the profiler showed a thousand forced layouts in one second, because the loop measured each element and then restyled it, and every measurement flushed the layout the previous restyle had invalidated. Name that one property in review and this class of bug gets hard to ship.


Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud