Step 3 — Forced Synchronous Layout
Goal
Quantify the cost of interleaving layout reads and writes, and learn to recognise the pattern in code that looks entirely ordinary.
Prerequisites
- Step 1 complete (you met
b.offsetHeightthere)
Predict first
Two loops over 800 elements. Identical work, identical final DOM:
// A
for (const e of els) e.style.width = (e.offsetWidth + 1) + 'px';
// B
const widths = els.map(e => e.offsetWidth);
els.forEach((e, i) => { e.style.width = (widths[i] + 1) + 'px'; });
Write down: how much slower is A than B? Commit to a number, not "somewhat".
Run
cd src
npm run layout
Expected output:
=== E. FORCED SYNCHRONOUS LAYOUT (800 elements, median of 5) ===
pattern | time
-----------------------------------------|--------
interleaved read -> write, per element | 86.2ms
batched: read all, then write all | 0.3ms
287x difference. Identical work, identical final DOM.
The ratio varies by machine and element count; on the reference machine it ranged from 98× to 287× across runs. Anything above ~50× is the same phenomenon.
What just happened
Each style.width write invalidates layout. Each offsetWidth read demands a geometrically
correct answer right now, so the browser must run style recalculation and layout synchronously
before returning — inside your loop, outside the rendering steps.
Interleaved, you pay N layouts. Batched, you pay one. You wrote code that looks O(n) and executes O(n) layouts, each of which is itself proportional to the document.
Why this matters more than the number suggests: the bug fits on one line, and the line reads as completely normal code.
e.style.width = e.offsetWidth + 1 + 'px';
There is no await, no loop-in-a-loop, no obviously expensive call. Code review does not catch
this by inspection — it is caught by knowing which properties force layout, or by DevTools'
forced-reflow warning.
Layout-forcing properties (partial): offsetWidth/Height/Top/Left, clientWidth/Height,
scrollWidth/Height/Top, getBoundingClientRect(), getComputedStyle() (for most properties),
focus(), scrollIntoView(), innerText. Paul Irish's list in references.md is the complete
one and is worth bookmarking rather than memorising.
The general rule: batch reads, then batch writes. If a library forces you to interleave (many measurement-driven layout libraries do), that is a real architectural cost of the library and belongs in the evaluation.
Debugging exercise
npm run serve # http://localhost:8080/lab.html
DevTools → Performance, CPU 6× slowdown, record while the thrash path runs.
- Find the red triangle forced-reflow warning. Read the stack it blames.
- Confirm the total time is dominated by Layout, not Scripting — this is the case where the flame chart contradicts the intuition that "JS is the slow part."
- Now record the batched version. The Layout blocks collapse to one.
Induce this deliberately now, while you have a working example. You want to recognise the marker before you need to find one under production pressure.
Checkpoint
docs/verification.md Checkpoint 5.
Going deeper
COUNT=3000 npm run layout
Is the ratio stable as N grows, or does it worsen? Explain the answer in terms of what each forced layout costs relative to document size — and what that predicts for a 100k-row table (fe-30).