In the browser, reading `el.offsetWidth` looks like a plain property access, yet it can be one of the most expensive lines in a frame. What does the browser actually do when that read happens, and when is the same read almost free?
answer
- writes are queued, reads are not
- the value has to be current
- dirty flags on style and layout
- flush is dragged into your task
- cheap again when layout is clean
basics
~20 sGeometry reads such as offsetWidth and getBoundingClientRect must return current values, so the browser first flushes any pending style recalculation and layout synchronously, inside your task. When nothing has been invalidated since the last layout, the same read is nearly free.
solid answer
~50 sDOM writes are lazy: setting `el.style.height`, toggling a class or inserting a node only marks style and layout dirty, and the browser clears those flags once, in its rendering steps, after the current task. A geometry read cannot be deferred that way — `offsetWidth`, `clientHeight`, `scrollTop` and `getBoundingClientRect()` are defined to return the element's current box, so if anything is dirty the browser must recalculate style and run layout **right there**, synchronously, before the read returns. That is a forced synchronous layout. The cost is not about the one element you asked for: it scales with how much of the document was invalidated and how expensive that subtree is to lay out. If nothing has changed since the last layout, layout is clean and the read is a cached lookup — which is why the second identical read in a row costs nothing.
code
javascript · 17 linesconst el = document.body.appendChild(document.createElement('div'));
el.textContent = 'measure me';
// 1. cheap - only marks style and layout dirty
el.style.padding = '20px';
// 2. expensive - must flush style + layout before it can answer
console.time('first read');
const first = el.offsetHeight;
console.timeEnd('first read');
// 3. cheap - layout is clean, nothing was invalidated in between
console.time('second read');
const second = el.offsetHeight;
console.timeEnd('second read');
console.log(first === second); // truego deeper
Know that offsetWidth and getBoundingClientRect are not plain stored fields — the browser may have to do real work before it can answer them.
Explain the invalidate-then-flush model: writes set dirty flags and are batched until the frame's rendering steps, while a geometry read forces style and layout to run immediately.
Show that the cost scales with how much of the document is dirty rather than with the element you asked about, and describe how you would keep a hot path from paying that flush repeatedly.
Be ready to discuss what this implies for API design: a synchronous measure() on a shared component forces the flush on every caller, so measurement points are an architectural decision, not a local one.
## A property read that is not really a property read Most JavaScript property reads are a lookup on an object that already holds the value. `el.offsetWidth` looks identical, but the number does not exist until the browser has computed where the element sits on the page. If anything changed since the last time it computed that, producing the answer is real work — and the work happens synchronously, inside your task, before the read returns. ## The browser queues your writes When you write `el.style.height = '40px'`, add a class, insert a node or change text, the browser does not immediately recompute anything. It marks the affected work dirty: style needs recalculating, layout needs redoing. Those flags accumulate. Normally they are cleared exactly once — in the rendering steps the browser runs after the current task, before it paints the frame. That laziness is why a hundred consecutive writes cost roughly what one costs. Nobody has to see the intermediate states, so nobody computes them. ## Why a read cannot be queued The geometry APIs are specified to return the element's current box. There is no version of `getBoundingClientRect()` that returns "whatever we last computed, possibly stale" — that would be a correctness bug for any code positioning a tooltip or deciding whether something fits. So when you ask, the browser has only one option: recalculate style for whatever is dirty, run layout, then answer. This is what people mean by *forced synchronous layout*, *forced reflow*, or the read "flushing" layout. ```js el.style.width = '200px'; // cheap: just marks layout dirty const a = el.offsetWidth; // expensive: flushes style + layout now const b = el.offsetWidth; // cheap again: layout is already clean ``` ## What the flush actually costs The cost is not proportional to the single element you named. It is proportional to how much of the document is invalidated and how expensive that content is to lay out — deep trees, large tables, long text runs and complex flex or grid containers are all slower than a handful of absolutely positioned boxes. On a big page a single flush can run into milliseconds. Worth stressing in an interview: this is the same work the browser was going to do anyway before painting. One forced layout in a task is not a bug. The damage comes from doing it **repeatedly** — every forced flush after the first is work the browser would otherwise have done once. ## Which reads force it The layout-dependent surface is larger than most people expect, and it includes both reads and some methods: - box metrics: `offsetTop`, `offsetLeft`, `offsetWidth`, `offsetHeight` - client metrics: `clientTop`, `clientLeft`, `clientWidth`, `clientHeight` - scroll metrics: `scrollTop`, `scrollLeft`, `scrollWidth`, `scrollHeight` - rectangles: `getBoundingClientRect()`, `getClientRects()` - `getComputedStyle()` — always needs current style, and needs layout too when you ask for a property whose resolved value depends on layout, such as `width`, `height` or `top` - layout-aware text: `innerText` (unlike `textContent`, which is not layout-aware) - hit testing and scrolling: `document.elementFromPoint()`, `scrollIntoView()`, `window.scrollX` / `window.scrollY` You do not need to memorise the list. The useful heuristic is: *if the answer depends on where things ended up on screen, layout has to be current.* ## When the read is almost free Layout is either dirty or clean. If nothing has invalidated it since the last time it ran, the browser answers from what it already computed. That is why measuring ten elements back to back is one flush, not ten — the first read pays, the rest ride along. It is also why the fix for the expensive case is about **ordering**, not about avoiding the APIs: keep the reads together, ahead of the writes, so exactly one flush happens. ## The mental model to state out loud "Writes are queued and settled once per frame. Reads of anything layout-derived cannot be queued, so they drag that settlement forward into my task. One flush is fine; alternating reads and writes turns one flush into many."
- If you append an element and immediately read its offsetHeight, is the value guaranteed to reflect the new element?Yes, and that guarantee is exactly what costs you. The read forces the browser to recalculate style and run layout for the pending insertion before answering, so you get a correct height without waiting for a frame. The caveat is the element must actually be rendered — measuring something still `display: none` or detached returns `0`.
- Does every getComputedStyle() call force layout?No. It always needs current style, so it forces a style recalculation when styles are dirty. It forces layout only when the property you read has a resolved value that depends on layout — `width`, `height`, `top`, percentage-derived values. Reading something like `color` or `font-weight` does not need the boxes to have been laid out.
- If one forced layout per task is unavoidable, when is it actually a problem?When it happens more than once for the same set of changes. The browser was going to lay out anyway before painting, so a single flush is just early work. The pathology is a loop or a set of independent components that each write and then read, so the same layout is recomputed again and again inside one task, turning one unit of work into dozens.
saying these in an interview costs you the question
- Says reads are cheap and only writes are expensive
- Claims every geometry read triggers a layout regardless of state
- Thinks the browser lays out after every style assignment
- Confuses a layout flush with a repaint
- Assumes the cost depends only on the element being read