Have you ever built a tooltip that snaps from the top-left corner to its correct spot? Or a modal that flashes at the wrong size before settling into place? That split-second glitch is a layout flicker. It happens when React reads the DOM, calculates a correction, and updates state, but the browser has already started pushing pixels to the screen. The usual prescription is to swap useEffect for useLayoutEffect. The swap works, but only if you understand exactly when each hook fires inside the browser pipeline.

The Browser Pipeline: Render, Commit, Paint

React updates a component in three distinct stages. In the render phase, React builds—or rebuilds—the Virtual DOM and computes the diff. No actual pixel changes yet; this is pure calculation happening in memory. Next comes the commit phase, where React applies those changes to the real DOM nodes. Styles update, nodes are inserted or removed, and text changes.

Then the browser takes over. In the paint phase, the browser’s rendering engine calculates layout geometry and draws pixels onto the screen. This sequence is rigid. The browser must complete layout before it can paint, and it must finish painting before the user sees anything new. The gap between commit and paint is measured in milliseconds, but it is real, and it is the window where useEffect and useLayoutEffect diverge.

Why useEffect Causes the Flicker

useEffect runs asynchronously, scheduled to fire after the browser has already painted the screen. The DOM is updated, the pixels are drawn, and then React steps back in to run your effect.

Imagine you render a dropdown menu under a button. Inside useEffect, you call buttonRef.current.getBoundingClientRect(), compute the correct top and left coordinates, and store them in state. Because useEffect runs after paint, the browser has already drawn the dropdown at its default position, perhaps at top: 0, left: 0. Only after that paint does your effect update state. React commits the corrected coordinates, and the browser paints again. The user sees two frames: the wrong position, then the right one. That visual snap is the flicker everyone tries to avoid.

For data fetching, API calls, analytics tracking, or setting up event listeners, this delay does not matter. The user does not care whether an analytics beacon fires a few milliseconds after the paint. In fact, pushing non-visual work until after paint keeps the initial render responsive. But for layout-dependent corrections, useEffect is simply too late.

How useLayoutEffect Blocks the Paint

useLayoutEffect runs synchronously, immediately after React mutates the DOM but before the browser has a chance to calculate layout or paint pixels. It blocks the paint pipeline entirely.

If you perform the same dropdown measurement inside useLayoutEffect, the sequence changes. React commits the initial DOM update, runs your layout effect, and your state update triggers a synchronous re-render. React commits the corrected coordinates, and only then does the browser paint. The user sees one frame, already correct.

That blocking behavior is both the feature and the risk. Because useLayoutEffect prevents the browser from painting until it finishes, any heavy computation inside it freezes the UI. Even a few dozen milliseconds of blocked paint feels like jank to the user. This is why the React docs explicitly tell you to start with useEffect and only promote to useLayoutEffect when you actually observe a flicker you cannot tolerate.

When to Use Each Hook

Most of your logic belongs in useEffect. Use it for:

  • Fetching data from an API
  • Setting up subscriptions or event listeners
  • Sending analytics events
  • Any side effect that does not read or mutate layout immediately

Reserve useLayoutEffect for operations that must read the DOM and write back before the user sees the frame:

  • Measuring element dimensions, such as width, height, or scroll position
  • Calculating coordinates for tooltips, popovers, or context menus
  • Preventing visible layout shifts when the visual position depends on rendered geometry

If you are unsure which to choose, default to useEffect. Move to useLayoutEffect only when you notice visual instability. This rule alone will keep the vast majority of React applications running smoothly.

The Server-Side Rendering Gotcha

If you use Next.js, Remix, or any framework that renders React on the server, you will hit a warning with useLayoutEffect. Because the server has no DOM, the hook has nothing to measure. React warns you that it expected a browser environment and did not find one. During hydration, this mismatch can also cause subtle bugs because the server-rendered markup and the client’s first intended render may differ.

The standard fix is an isomorphic hook that selects the right effect based on the environment:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Use this wrapper in any component that must measure DOM nodes but might execute during server rendering. It silences the warning and keeps your server output consistent.

Performance and Best Practices

Since useLayoutEffect blocks painting, keep the body of the hook as light as possible. Read the layout value, compute the correction, and write it back. Do not fetch data, parse large objects, or run expensive algorithms inside it. Heavy code here will stall the main thread and make your interface feel frozen.

When you measure elements, use React refs rather than document.getElementById. Refs are tied to your component instance, survive re-renders without query tricks, and work reliably with portals or conditional rendering. Global ID lookups break component encapsulation and can return null at exactly the moment you need them.

useEffect is the right default for nearly every side effect. It lets the browser paint without interruption and handles data, events, and external synchronization cleanly. useLayoutEffect is a specialized tool for a specific problem: reading layout and writing back before the paint. Master the timing difference between them, and you will stop chasing flickers and start preventing them.

The real takeaway: Start with useEffect for everything. The moment you see a tooltip or modal blink into the wrong place before correcting itself, that is your signal. Switch to useLayoutEffect, measure the DOM, adjust your layout, and let the browser paint once—correctly.