useLayoutEffect vs. useEffect: Really Understanding the Timing Difference
AI generated
{ }
React 19 · Rendering Pipeline · Hooks
useLayoutEffect vs. useEffect
Why the moment of execution decides between visible flicker and a clean paint

useEffect runs after the paint, useLayoutEffect synchronously before it. A tiny timing difference on paper, one that in practice decides whether a DOM measurement for a tooltip or a scroll position visibly flickers or not.

12 min read useLayoutEffect · useEffect Rendering Pipeline

1. The Rendering Pipeline in Three Phases

React's rendering pipeline runs through three consecutive phases: in the render phase, React computes the new virtual tree, in the commit phase, React actually applies the resulting changes to the real DOM, and in the paint phase, the browser finally draws those DOM changes as pixels on screen.

useEffect and useLayoutEffect hook in at different points relative to that third phase. useEffect runs asynchronously after the paint, useLayoutEffect on the other hand runs synchronously after the commit, but before the browser paints anything at all. That seemingly small difference decides in practice whether a DOM measurement causes visible flicker for the user or not.

2. useEffect: Running After the Paint

useEffect schedules its callback execution through the browser's scheduling queue. The browser is allowed to paint the current frame in the meantime, before the effect even runs, which keeps the main thread responsive for the user throughout. For data fetching, subscriptions, event listener registration, or logging, effects without immediate visual impact, that is the ideal and recommended default.

The cost of this non-blocking behavior shows up as soon as a DOM property is read inside useEffect and used to change the layout, say the actual height of a tooltip that was just rendered. By that point the browser has already painted the old, still incorrect state, so the following correction is perceived as a brief but clearly visible flicker.


function NaiveTooltip({ triggerRef, children }) {
  const [top, setTop] = useState(0);

  useEffect(() => {
    // This measurement runs AFTER the first visible paint,
    // the tooltip briefly appears in the wrong spot and then jumps
    const rect = triggerRef.current.getBoundingClientRect();
    setTop(rect.bottom + 8);
  }, [triggerRef]);

  return <div style={{ position: 'fixed', top }}>{children}</div>;
}

3. useLayoutEffect: Synchronous Before the Paint

useLayoutEffect runs synchronously, right after React applies the DOM mutations during the commit phase, but before the browser paints the next frame. React deliberately delays the paint until the callback function has fully completed.

That makes it possible to run measurements like getBoundingClientRect() or offsetHeight inside useLayoutEffect, and a resulting setState call gets processed before the visible frame. The user never gets to see the intermediate state with the wrong position, because it never gets painted.


function CorrectTooltip({ triggerRef, children }) {
  const [top, setTop] = useState(0);

  useLayoutEffect(() => {
    // This measurement runs BEFORE the visible paint,
    // the resulting setState call is processed in the same cycle
    const rect = triggerRef.current.getBoundingClientRect();
    setTop(rect.bottom + 8);
  }, [triggerRef]);

  return <div style={{ position: 'fixed', top }}>{children}</div>;
}

4. A Concrete Flicker Scenario

A tooltip should appear directly above its trigger element, but its actual height is only known once it has rendered at least once. With useEffect, the tooltip therefore first appears at an assumed, still incorrect position and then visibly jumps to its actual correct spot once the measurement completes.

Depending on screen refresh rate and the complexity of the tooltip content, this jump can be perceived as noticeable, distracting flicker. It becomes especially apparent with rapid consecutive interactions, say hovering over several list items in a row, where several such position jumps become visible in quick succession.

5. The Fix Using useLayoutEffect

Move that same measurement from useEffect into useLayoutEffect, and React computes the correct position within the same commit cycle. The resulting setState call triggers an additional, but still synchronous, render and commit pass before the browser draws anything on screen at all.

The user therefore sees the correctly positioned tooltip directly, with no visible intermediate state and no perceptible jump. The trade-off is that React delays the paint for this extra synchronous cycle, which can become noticeable with expensive measurements or deeply nested component trees and should therefore be used deliberately, not by default.


function Tooltip({ triggerRef, children }) {
  const tooltipRef = useRef(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    const triggerRect = triggerRef.current.getBoundingClientRect();
    const tooltipRect = tooltipRef.current.getBoundingClientRect();

    let top = triggerRect.bottom + 8;
    if (top + tooltipRect.height > window.innerHeight) {
      top = triggerRect.top - tooltipRect.height - 8; // flip above instead
    }
    setPos({ top, left: triggerRect.left });
  }, [triggerRef]);

  return (
    <div ref={tooltipRef} style={{ position: 'fixed', top: pos.top, left: pos.left }}>
      {children}
    </div>
  );
}

6. Server Side Rendering and the useLayoutEffect Warning

No DOM and no paint cycle exist on the server. React therefore emits a warning for useLayoutEffect inside a server rendered component, to the effect of: useLayoutEffect does nothing on the server. The effect simply never runs there, because there's no browser sense commit moment for useLayoutEffect to hook into.

A common pattern instead is a custom useIsomorphicLayoutEffect hook that falls back to useEffect on the server, which also doesn't run there but produces no warning, and uses regular useLayoutEffect in the browser. The decision is usually made via a check of typeof window !== 'undefined' at module import time.


import { useEffect, useLayoutEffect } from 'react';

// Falls back to useEffect on the server (no warning),
// uses the synchronous useLayoutEffect in the browser
export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

7. Other Sensible Use Cases

Besides tooltip positioning, useLayoutEffect is useful for saving and restoring scroll position around a DOM update, say for a chat list that inserts new messages at the top, where the existing scroll position would otherwise visibly jump upward. Synchronously reading ResizeObserver measurements before the first visible frame falls into this category too.

Another classic case is CSS transition triggers, where a starting state has to be set, the DOM committed, and immediately afterward, in the same frame, the target state set, so the browser actually recognizes the transition as an animation rather than a jump and plays it correctly.


function ChatList({ messages }) {
  const listRef = useRef(null);
  const prevHeight = useRef(0);

  useLayoutEffect(() => {
    const el = listRef.current;
    const newHeight = el.scrollHeight;
    // Shift scroll position by exactly the newly inserted height,
    // before the browser paints the new state
    el.scrollTop += newHeight - prevHeight.current;
    prevHeight.current = newHeight;
  }, [messages]);

  return <div ref={listRef} className="chat-list">{/* ... */}</div>;
}

8. Rule of Thumb: When useLayoutEffect Is Truly Needed

If nothing inside the effect reads from the DOM in a way meant to synchronously trigger another layout update, useEffect is entirely sufficient. The vast majority of effects in a typical application, data fetching, subscriptions, event listener registration, analytics, and logging, fall into this category and benefit from useEffect keeping the main thread unblocked.

useLayoutEffect is only truly necessary when a visible intermediate state, be it flicker or a visible jump, would otherwise be unavoidable, because a DOM measurement directly influences the result of a synchronous state update. In every other case, using useLayoutEffect merely costs unnecessary perceived rendering performance without offering any real benefit.

9. Common Mistakes with useLayoutEffect

The most common mistake is using useLayoutEffect across the board instead of useEffect, without any real layout measurement actually taking place. That unnecessarily blocks the paint on every commit, which noticeably worsens the perceived responsiveness of the entire application for complex, deeply nested component trees, without producing a single visible benefit.

A second common mistake is asynchronous operations such as fetch calls or timers inside useLayoutEffect, which completely defeats its synchronous advantage, since the browser has to wait for the asynchronous operation anyway. On top of that, missing cleanup for measurements tied to resize or scroll events is common, leaving stale listeners quietly running after the component unmounts.

Criterion useEffect useLayoutEffect Recommendation
Execution timing Asynchronous, after the paint Synchronous, after commit, before the paint Choose useEffect as the default
Blocks the browser paint No Yes, until the callback completes Use useLayoutEffect sparingly
Behavior with server side rendering Doesn't run on the server, no warning Doesn't run on the server, produces a warning Use useIsomorphicLayoutEffect for SSR projects
Typical use case Data fetching, subscriptions, logging DOM measurement with an immediately following layout update Decide based on actual need, not by default

Mironsoft

React architecture, performance, and Magento frontend integration

React frontends that stay fast instead of slowing down with every feature?

We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.

Performance Audit

Systematically measuring and fixing re-renders, bundle size, and load times.

State Architecture

Cleanly separating context, client state, and server state instead of mixing everything.

Magento Integration

Building robust, type-safe GraphQL or REST integration with Magento.

10. Summary

useLayoutEffect vs. useEffect: The Essentials at a Glance

Timing

useEffect runs asynchronously after the paint, useLayoutEffect synchronously after commit, but before the paint.

Flicker

DOM measurements with an immediately following layout update belong in useLayoutEffect to avoid visible jumps.

SSR

useLayoutEffect produces a warning on the server, useIsomorphicLayoutEffect solves the problem cleanly.

Rule of Thumb

Without a real layout measurement, useEffect suffices, useLayoutEffect is the exception, not the default.

11. FAQ: useLayoutEffect vs. useEffect: The Essentials at a Glance

1What is the fundamental difference between useEffect and useLayoutEffect?
useEffect runs asynchronously after the paint, useLayoutEffect synchronously after the commit, but before the browser paints the frame. This timing difference decides whether a DOM measurement causes visible flicker or not.
2Why does a tooltip flicker when its position is computed in useEffect?
Because useEffect only runs after the paint, the browser has already painted the old, still incorrect position by that point. The following correction is therefore perceived as a brief, visible jump.
3How does useLayoutEffect fix this flicker problem?
useLayoutEffect runs synchronously before the paint, so a measurement and the resulting setState call get processed within the same cycle. The browser then paints the correct position directly, with no visible intermediate state.
4Why does useLayoutEffect produce a warning on the server?
No DOM and no paint cycle exist on the server for useLayoutEffect to hook into. React therefore warns that the effect simply never runs there.
5What is useIsomorphicLayoutEffect and when do I need it?
A hook that falls back to useEffect on the server to avoid the warning, and uses regular useLayoutEffect in the browser. It makes sense for server side rendering whenever a component would otherwise use useLayoutEffect directly.
6Does useLayoutEffect really block the browser?
Yes, React waits to paint the frame until the useLayoutEffect callback has fully completed. That can become noticeable with expensive computations or deeply nested trees, which is why it should be used deliberately.
7For which cases is useEffect entirely sufficient?
For the vast majority of effects without immediate visual impact, such as data fetching, subscriptions, event listener registration, analytics, and logging. useLayoutEffect is only needed when a DOM measurement causes an immediate layout update.
8Can I run asynchronous operations inside useLayoutEffect?
Technically yes, but it isn't sensible, because the browser has to wait for the asynchronous operation anyway and useLayoutEffect's synchronous advantage is completely lost as a result. Asynchronous operations belong in useEffect.
9What use cases besides tooltips benefit from useLayoutEffect?
Saving and restoring scroll position around DOM updates, say for a chat list, synchronously reading ResizeObserver measurements, and CSS transition triggers where the start and target state have to be set within the same frame.
10What is the most common mistake when using useLayoutEffect?
Using useLayoutEffect across the board instead of useEffect, without any real layout measurement taking place. That unnecessarily blocks the paint on every commit without producing any recognizable benefit.