Understanding the Execution Timing of useEffect and useLayoutEffect in React
This article explains the differences in execution timing between React's useEffect and useLayoutEffect hooks, analyzes their underlying mechanisms, demonstrates practical demos with performance monitoring, and delves into React's rendering process, scheduler, and related browser rendering concepts.
React provides two hooks, useEffect and useLayoutEffect , both used for side‑effects but differing in when they run relative to the browser's paint cycle. useEffect runs asynchronously after the browser has rendered the UI, while useLayoutEffect runs synchronously after DOM mutations but before the paint, allowing layout reads and immediate re‑renders.
Official Explanation
The signatures of the two hooks are identical, but useLayoutEffect is invoked synchronously after all DOM changes. It is intended for reading layout information and synchronously triggering updates before the visual update, whereas useEffect is deferred to avoid blocking visual updates.
Demo Example
A simple React component demonstrates the timing difference. The component logs the current count from both hooks and updates the count on button click.
import React, { useState, useEffect, useLayoutEffect } from 'react';
const EffectDemo = () => {
const [count, setCount] = useState(0);
useEffect(function useEffectDemo() {
console.log('useEffect:', count);
}, [count]);
useLayoutEffect(function useLayoutEffectDemo() {
console.log('useLayoutEffect:', count);
}, [count]);
return (
{ setCount(count + 1); }}>click me
);
};
export default EffectDemo;Performance monitoring in Chrome shows that useLayoutEffect runs before the DCL/FCP/FMP stages, while useEffect runs after those milestones, confirming the theoretical timing.
Simulation of the Rendering Process
A vanilla JavaScript example mimics the hook execution order using immediate function invocation, manual layout effect calls, and a delayed setTimeout to represent the async nature of useEffect .
<body>
<div id="app"></div>
<script type="text/javascript">
(function iife(){
function render(){
var appNode = document.querySelector('#app');
var textNode = document.createElement('span');
textNode.id = 'tip';
textNode.textContent = 'hello';
appNode.appendChild(textNode);
}
function useLayoutEffectDemo(){
console.log('useLayoutEffectDemo', document.querySelector('#tip'));
}
function useEffectDemo(){
console.log('useEffectDemo');
}
render();
useLayoutEffectDemo();
setTimeout(useEffectDemo, 0);
})();
</script>
</body>The console output and the browser's Performance panel illustrate the same four‑step sequence: render, synchronous layout effect, asynchronous effect, and final paint.
React Rendering Mechanics
React rendering consists of a reconciliation (scheduling) phase that determines which Fiber nodes need updates, followed by a commit phase that applies those changes to the DOM. Hooks are processed during the commit phase via the commitHookEffectListMount function, which iterates over a circular linked list of effect objects.
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
}Each effect is stored in the Fiber's updateQueue , forming a ring‑shaped structure that enables efficient traversal during commit. The commitHookEffectListMount function distinguishes between layout and passive effects using tags (e.g., 3 for layout, 5 for passive) and executes the appropriate callbacks.
function commitHookEffectListMount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
var create = effect.create;
effect.destroy = create();
// error handling omitted for brevity
}
effect = effect.next;
} while (effect !== firstEffect);
}
}The scheduler uses MessageChannel for micro‑task‑like asynchronous execution, ensuring that passive effects ( useEffect ) run after the browser has painted.
Browser Rendering Pipeline
Understanding the browser's rendering pipeline—style recalculation, layout, paint, and compositing—is essential for grasping why useLayoutEffect can block visual updates while useEffect cannot. References to Google’s rendering fundamentals and deeper analyses are provided for further study.
Other Lifecycle Functions
In class components, useEffect roughly corresponds to the combination of componentDidMount , componentDidUpdate , and componentWillUnmount . The article shows how React’s internal commitLifeCycles function maps these class lifecycle methods to hook execution.
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
case ClassComponent:
var instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
}
}
break;
}
}Overall, the article provides a comprehensive walkthrough of how useEffect and useLayoutEffect are parsed, scheduled, and executed within React’s Fiber architecture, and how these hooks interact with the browser’s rendering lifecycle.
Fulu Network R&D Team
Providing technical literature sharing for Fulu Holdings' tech elite, promoting its technologies through experience summaries, technology consolidation, and innovation sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.