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.offsetHeight there)

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.

  1. Find the red triangle forced-reflow warning. Read the stack it blames.
  2. 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."
  3. 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).