skip to content

Your React codebase keeps accumulating components that measure DOM nodes in useLayoutEffect to size or position themselves. As the lead, how do you decide which of those measurements stay in JavaScript and which have to be solved another way?

level: principalimportance: should knowfreq 24%

answer

  1. ask what CSS could do instead
  2. the server cannot measure anything
  3. one-shot reads go stale silently
  4. cost multiplies with instance count
  5. centralise the escape hatch

basics

~20 s

Keep a JavaScript measurement only when the geometry genuinely cannot be expressed declaratively and the answer must be true before paint. Everything CSS can express, everything above the fold, and anything that invalidates constantly should move to CSS or an observer.

solid answer

~50 s

I apply four filters. Can CSS already express it — flex, grid, `position: sticky`, container queries? If yes, that wins, because it costs nothing on the frame path and works before hydration. Is it above the fold? Measurement cannot run during server rendering, so any measured layout in the initial viewport buys a guaranteed post-hydration shift. How often does the answer change — fonts loading, zoom, translated text, resizes? A one-shot read is a bug factory there, so it becomes a `ResizeObserver` subscription or nothing. And what does it cost at scale: a layout effect blocks paint, so a per-item measurement across a long list is an architectural problem, not a tuning problem. What survives all four goes into one or two reviewed hooks that own attach, resize and teardown, rather than being hand-rolled per component — that also gives us one place to require browser-based tests, since a headless DOM reports zero for every dimension.

go deeper

for a junior

Know that CSS can express far more layout than people assume — flex, grid, sticky positioning — and that reaching for a DOM measurement should feel like an exception, not the default way to size something.

for a middle

Be able to name what makes a measurement go stale (fonts loading, zoom, resizes, translated text) and explain why that turns a one-shot read into a subscription with an observer and a cleanup.

for a senior

Argue the cost concretely: layout effects sit before paint, measurement cannot run on the server so above-the-fold layout shifts after hydration, and per-row measurement scales with list length. Then propose the structural fix rather than tuning the symptom.

for a principal

Own it as policy. Define the narrow allow-list where measurement is legitimate, centralise it in reviewed hooks that handle attach, resize and teardown, make the exception explicit in review, and tie the whole thing to a field metric so regressions surface without anyone filing a bug.

## Why this needs a policy rather than case-by-case review Measure-then-apply is locally reasonable and globally expensive. Each component that does it looks fine in isolation: a few lines, a ref, a `getBoundingClientRect()`, a state write. The costs are all emergent — frame time that only shows up when many instances mount together, layout shift that only shows up in field metrics, test gaps that only show up when a headless run passes on code that is broken in a browser. Reviewers approving one component at a time systematically under-price it, which is why the accumulation happens. ## Filter 1: can the platform express it declaratively? A large share of measured geometry is re-implementing something CSS does natively: equal-height columns, filling remaining space, aligning to a container edge, staying visible while scrolling, adapting to the space an element was given. Flexbox and grid handle the first three, `position: sticky` the fourth, and container queries let a component respond to its own container's size without any JavaScript reading that size. A declarative solution is not merely tidier. It runs in the browser's own layout pass rather than in your commit, it is correct during server-rendered HTML before any JavaScript executes, it survives zoom and font-size changes automatically, and it needs no teardown. When the CSS answer exists, the JavaScript one is strictly worse. The honest exception is geometry that depends on rendered content in a way CSS cannot see — text that has to be truncated to a measured line count, a canvas that needs device pixels, a popover that must flip because it would leave the viewport, a virtualized list computing offsets. Those are real; treat them as the allow-list. ## Filter 2: does it affect the first paint? Measurement requires a layout engine, so it cannot happen during server rendering — React does not run layout effects there. Any component whose *initial* layout is measured therefore ships one geometry in the server HTML and a different one after hydration. For content below the fold or behind an interaction that is invisible and harmless. For anything in the initial viewport it is a guaranteed visual shift that shows up in field performance data and, worse, in perceived quality. So the rule is not "measurement is bad" but "measured initial layout above the fold is bad". Reserving space with CSS and refining after hydration is usually the compromise: the shift becomes a refinement rather than a jump. ## Filter 3: how often does the answer go stale? Ask what invalidates the measurement. Web fonts finishing load, the user zooming, a translation making a label 40% longer, a splitter dragged, a sibling growing, an ancestor collapsing, an orientation change. A component whose size is measured once at mount is correct only until the first of those happens, and in a long-lived app all of them eventually do. If the answer is "often", the component does not need a measurement, it needs a subscription — a `ResizeObserver` (or `IntersectionObserver` for visibility) created against the node and disconnected in cleanup, reading sizes the browser already computed. If the answer is "often and the consequence is only cosmetic", the right decision is frequently to delete the feature rather than maintain the subscription. ## Filter 4: what does it cost at the scale it will actually run? A layout effect runs between DOM mutation and paint, so its cost is added to the frame the user is waiting on. One measurement in a dialog is invisible. The same code inside a list row is multiplied by the row count, and interleaved reads and writes force the browser to recompute layout repeatedly. Per-item measurement therefore needs a structural answer — hoist it to the container so reads can be batched, or virtualize so the cost is proportional to what is visible — rather than micro-optimisation. ## Turning the filters into something a team can follow - **Centralise.** One or two hooks (element size, element position) that own the callback-ref attachment, the observer, the change guard and the teardown. Feature code consumes a value; it never hand-rolls the lifecycle, and every known pitfall is fixed once. - **Make the exception explicit.** New measurement outside those hooks is a design decision that gets stated in the PR — what CSS could not do, whether it is above the fold, what invalidates it. - **Test where layout exists.** A headless DOM implementation performs no layout and reports zero for every dimension, so measurement code is untested by default in unit tests. Either the shared hooks carry browser-based coverage, or the team knowingly accepts that this code is verified by hand. - **Watch the field signal.** Layout shift and interaction latency are where this class of code actually fails. Regressions there are the trigger to revisit which components are still measuring. ## The judgment to articulate Measurement is an escape hatch from declarative layout. It is legitimate when the browser cannot know what you know, and it is a liability everywhere else — because it runs on the critical path, cannot run on the server, goes stale silently, and hides from tests. Leading this well means keeping the escape hatch narrow, shared, and deliberately entered.

  • A component measures its container to pick a layout variant. What would you propose instead?
    Container queries: the element responds to the size of its containment context in CSS, with no JavaScript reading a width and no state write. That is correct in server-rendered HTML before hydration, costs nothing on the frame path, and cannot go stale. I would keep the measured variant only where the decision depends on something CSS cannot observe, such as the rendered length of dynamic text.
  • How do you keep this from silently regressing once the policy is written?
    Policy alone does not hold. I make the sanctioned hooks the path of least resistance so hand-rolling is more work than complying, require any measurement outside them to be justified in the PR description, and watch layout shift and interaction latency in field metrics as the regression signal. When those move, we audit which components measure, rather than waiting for someone to report a flicker.
  • Why is measurement code disproportionately likely to be untested?
    A headless DOM implementation does not perform layout, so every dimension reads as zero and the branches that depend on real geometry are never exercised — the tests pass while the behaviour is unverified. That pushes measurement coverage into browser-based tests, which are slower and rarer. Concentrating the logic in a few shared hooks makes that cost payable once instead of per feature.

saying these in an interview costs you the question

  • Measuring in JavaScript is always more flexible than CSS
  • Layout shift only matters for images
  • Measure once on mount and it stays correct
  • One extra layout effect per component is negligible
  • Unit tests cover measurement code fine

context