skip to content

A React component positions a tooltip by writing to the DOM node inside a useEffect callback, and users see the tooltip flash at the wrong position for one frame. Explain the timing that causes the flash, and what changes if the identical code runs in useLayoutEffect instead.

level: seniorimportance: should knowfreq 50%

answer

  1. a flash means the browser painted it
  2. which side of the paint does it run on
  3. commit returns, then pixels
  4. atomic with the update, at a cost
  5. on the critical path of the frame

basics

~20 s

React commits the DOM, the browser paints the un-positioned tooltip, and only then does the useEffect callback move it, so one bad frame is visible. A layout effect runs inside the commit before paint, so the user only ever sees the corrected position.

solid answer

~50 s

The flash is a frame boundary. React's commit pass writes the new DOM and returns; the browser then paints whatever is there, which is the tooltip at its default position. The `useEffect` callback is a passive effect scheduled to run after that paint, so its correction lands one frame late and the user sees both states. Moving the same write into `useLayoutEffect` puts it inside the commit pass: it runs after React has updated the DOM but before control returns to the browser, so the corrected position is part of the first painted frame. If that callback calls `setState`, React re-renders and re-commits synchronously before paint too. The cost is that a layout effect sits on the critical path of the frame — anything slow there delays the pixels for every user, so it should only hold work that genuinely must be visually atomic.

code

jsx · 13 lines
jsx
import { useLayoutEffect, useRef } from 'react';

export default function Marker({ offset }) {
  const nodeRef = useRef(null);

  // Runs after React updates the DOM but BEFORE the browser paints,
  // so the un-offset position is never shown.
  useLayoutEffect(() => {
    nodeRef.current.style.transform = `translateY(${offset}px)`;
  }, [offset]);

  return <span ref={nodeRef}>marker</span>;
}

go deeper

for a junior

Know that useEffect callbacks run after React has updated the screen, so DOM changes made there can be visible as a brief flicker.

for a middle

Explain the mechanism in terms of the commit pass and the paint that follows it, and say which hook runs on which side of that boundary.

for a senior

Diagnose the flash from a profile rather than by guessing, choose the pre-paint hook only for visually load-bearing work, and state the frame-latency cost when you recommend it.

for a principal

Set the team-wide rule for what may run before paint, since every layout effect is a permanent tax on interaction latency; prefer designs that render the correct output directly over measure-and-correct loops.

## What the user actually sees A visual flash is always evidence that the browser painted an intermediate state. To find it, place every piece of code on the frame timeline: 1. React's render phase computes the new tree — nothing visible. 2. React's commit pass mutates the DOM, attaches refs, runs layout effects — still nothing visible. 3. React returns; the browser runs style, layout and paint. **Pixels appear here.** 4. React's scheduled callback runs the passive `useEffect` bodies. A write in step 4 is by definition a correction applied to something already on screen. With the tooltip at, say, `top: 0` in its initial markup and repositioned in an effect, step 3 paints it at the top of the viewport and step 4 moves it, producing exactly one wrong frame. On a fast machine that is 16 milliseconds and reads as a flicker; under load it can be several frames and reads as a jump. ## Why useLayoutEffect removes it `useLayoutEffect` bodies run in the layout sub-phase of the commit — step 2 above. The DOM already matches the new tree, so measurements are valid, but the browser has not been given control back, so nothing has been painted. Any style or attribute you write there is simply part of the state the browser will paint when React finally returns. The guarantee extends to state updates. If a layout effect calls `setState`, React does not wait for the next frame: it performs another render and another commit synchronously before yielding. That is what makes the measure-then-adjust pattern viable at all — you can read a real geometry, feed it back into state, and still ship a single frame. ```jsx useLayoutEffect(() => { const { bottom } = anchorRef.current.getBoundingClientRect(); setTop(bottom + 8); // re-render and re-commit happen before paint }, []); ``` ## The cost you must state Saying "use `useLayoutEffect`" without the cost is a weak answer. Everything in the layout sub-phase is synchronous work between the DOM update and the paint, so it is added directly to the frame's latency. A layout effect that walks a large subtree, does an expensive computation, or triggers a chain of state updates delays the pixels for every user on every commit where it runs. A passive effect that does the same work costs nothing visually, because the frame is already on screen. So the decision rule is narrow: **use a layout effect only when the user would otherwise see a wrong state.** Positioning and sizing that depends on measured geometry, restoring a scroll position, and reading a value from the pre-paint DOM qualify. Fetching data, logging analytics, starting subscriptions, and setting up timers do not — none of those change what the current frame looks like. ## Fixes that are often better than either hook A senior answer usually points out that the flicker is sometimes a signal to avoid the measure-and-correct loop entirely: - **Render the correct position in the first place.** If the position is derivable from props or state, compute it during render and pass it as a style; no post-commit correction is needed and no hook is involved. - **Do not render the element until you can place it.** Rendering the tooltip only once the anchor geometry is known means there is no intermediate state to paint. - **Let CSS do it.** Transforms and layout primitives often express the relationship declaratively, which is cheaper than any JavaScript measurement and survives resizes without a hook. - **Keep the layout effect tiny when you do need one.** Read, write, return. Push everything that is not visually load-bearing into a passive effect. ## How to diagnose it in the wild When someone reports a flash you cannot reproduce, the tooling matches the model. Recording a performance profile shows one frame with the element in position A and the next in position B, with a script block between them — that pattern is a post-paint correction. Throttling the CPU widens the gap until it is obvious. If instead both frames are identical and the element merely appears late, the problem is elsewhere: a suspended boundary, an image without dimensions, or a slow data dependency, none of which the commit/paint boundary explains. ## The sentence that answers the question "`useEffect` runs after the browser paints, so any DOM correction it makes is visible as a wrong frame; `useLayoutEffect` runs inside the commit before paint, so the correction is atomic with the update — at the price of putting that work on the critical path of the frame."

  • Would wrapping the same positioning code in requestAnimationFrame inside the effect fix the flash?
    No. The passive effect already runs after the paint that showed the wrong position, so scheduling the write for the next animation frame moves the correction later, not earlier. It can make the flash worse by guaranteeing at least one extra frame in the wrong state.
  • When is it right to accept the extra frame and keep the work in useEffect?
    Whenever the work does not change what the current frame looks like — analytics, logging, data fetching, subscriptions, focus the user is not racing. Also when the correction is expensive and the intermediate state is visually acceptable, because a delayed paint is a worse experience than a brief settle.
  • A layout effect in a deep list makes typing feel sluggish. What is the mechanism?
    Every commit runs that layout effect synchronously before paint, for every affected item, so its cost is added to the latency of each keystroke's frame. The fixes are to shrink the effect, hoist it to a single ancestor that measures once, or move the non-visual part of it into a passive effect.

saying these in an interview costs you the question

  • Says useEffect runs before the browser paints
  • Recommends useLayoutEffect everywhere to be safe
  • Blames React's rendering speed rather than the paint boundary
  • Thinks requestAnimationFrame inside an effect runs before paint
  • Ignores that layout effects block the frame

context