文章中英模式
布鲁斯前端React面试题目 - useEffect 与 useLayoutEffect的使用选择
深入比较useEffect与useLayoutEffect的执行时机、适用场景与效能影响。了解两者在React渲染流程中的区别,正确选择何时使用useLayoutEffect来避免闪烁问题,以及如何在SSR环境中正确处理。
文章中英模式

懒得看文章?那就来看视频吧
两者执行时机的根本区别
useEffect和useLayoutEffect的API完全相同,但它们在React渲染流程中的执行时机不同:
| Hook | 执行时机 | 同步/异步 | 画面更新时机 |
|---|---|---|---|
useEffect | 渲染后,浏览器绘制后 | 异步 | 用户先看到更新,然后执行effect |
useLayoutEffect | 渲染后,浏览器绘制前 | 同步 | 执行完effect后,用户才看到更新 |
渲染流程对比图:
useEffect的流程:
- 1. React更新虚拟DOM并计算差异
- 2. React更新实际DOM
- 3. 浏览器绘制画面(用户看到更新)
- 4. 然后执行useEffect回调
useLayoutEffect的流程:
- 1. React更新虚拟DOM并计算差异
- 2. React更新实际DOM
- 3. 执行useLayoutEffect回调
- 4. 浏览器绘制画面(用户看到更新)
// 基本语法相同,但执行时机不同
import { useEffect, useLayoutEffect } from 'react';
function ExampleComponent() {
const [state, setState] = useState(initialState);
// 在画面绘制后执行(非阻塞)
useEffect(() => {
// 这里的代码在画面更新后执行
// 即使执行耗时操作,用户也已经看到了UI
}, [dependencies]);
// 在画面绘制前执行(阻塞)
useLayoutEffect(() => {
// 这里的代码在DOM更新后但画面绘制前执行
// 在这里的操作会阻塞画面更新,直到执行完成
}, [dependencies]);
return <div>...</div>;
}两者的适用场景对比
简单来说,useEffect和useLayoutEffect的选择取决于你需要副作用何时执行:
| Hook | 适用场景 | 实际例子 |
|---|---|---|
useEffect推荐优先使用 |
|
|
useLayoutEffect特定场景使用 |
|
|
记住: 除非你需要在画面绘制前进行DOM操作(避免闪烁或需要精确测量),否则应该使用useEffect。useLayoutEffect会阻塞渲染,可能影响性能。
对性能的影响
选择合适的hook会对应用的性能产生明显影响:
useEffect的性能特性
- 1. 非阻塞渲染 - 画面更新不会等待effect完成
- 2. 允许浏览器先绘制UI再执行耗时操作
- 3. 对于大多数副作用来说更高效
- 4. 可能导致视觉闪烁(如果effect改变了可见元素)
useLayoutEffect的性能特性
- 1. 阻塞渲染 - 会延迟画面更新直到effect完成
- 2. 在effect中的耗时操作会导致明显的UI延迟
- 3. 避免了闪烁问题
- 4. 对于需要同步DOM更新的操作更合适
// 性能影响案例:闪烁问题
// ❌ 使用useEffect可能导致闪烁
function FlickeringExample() {
const [width, setWidth] = useState(0);
const divRef = useRef();
useEffect(() => {
// 问题:先渲染初始状态,用户看到这一帧
// 然后才测量并更新,导致内容跳动
setWidth(divRef.current?.getBoundingClientRect().width || 0);
}, []);
return <div ref={divRef} style={{ width: `${width}px` }}>内容可能闪烁</div>;
}
// ✅ 使用useLayoutEffect避免闪烁
function SmoothExample() {
const [width, setWidth] = useState(0);
const divRef = useRef();
useLayoutEffect(() => {
// 优点:在画面绘制前完成测量和更新
// 用户只会看到最终结果,不会看到中间过程
setWidth(divRef.current?.getBoundingClientRect().width || 0);
}, []);
return <div ref={divRef} style={{ width: `${width}px` }}>内容不会闪烁</div>;
}实用例子:何时选择每一种
使用 useLayoutEffect 的典型例子
// 例子1:弹出提示定位 - 在画面绘制前计算位置
function Tooltip({ text, targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
// 获取目标元素和提示框的尺寸
// 计算提示框位置(置于目标元素上方中央)
// 立即更新位置状态,确保渲染前完成
if (targetRef.current && tooltipRef.current) {
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: targetRect.top - tooltipRect.height - 10,
left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
});
}
}, [targetRef]);
return <div ref={tooltipRef} style={{position: 'fixed', top: `${position.top}px`, left: `${position.left}px`}}>{text}</div>;
}
// 例子2:滚动到特定元素 - 避免闪烁
function ScrollToElement({ elementId }) {
useLayoutEffect(() => {
// 找到目标元素并立即滚动到该位置
// 在画面绘制前执行,用户不会看到中间过程
const element = document.getElementById(elementId);
if (element) element.scrollIntoView({ behavior: 'smooth' });
}, [elementId]);
return null;
}使用 useEffect 的典型例子
// 例子1:数据读取
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 非阻塞操作,用户可以先看到加载状态
setLoading(true);
fetchUser(userId)
.then(data => {
setUser(data);
setLoading(false);
})
.catch(error => {
console.error(error);
setLoading(false);
});
}, [userId]);
if (loading) return <div>加载中...</div>;
if (!user) return <div>找不到用户</div>;
return <div>{user.name}</div>;
}SSR (服务器端渲染) 的特殊考量
在服务器端渲染(SSR)环境中,useLayoutEffect会显示警告并且不会执行,因为服务器上没有「布局阶段」。这是因为:
- 1. 服务器没有浏览器环境,无法进行DOM测量或更新
- 2. SSR过程中不存在「浏览器绘制」的概念
- 3. 服务器上的React只渲染出HTML字符串,不涉及实际DOM操作
处理SSR环境中的useLayoutEffect:
// 方法1:条件性使用不同的hook
import { useEffect, useLayoutEffect } from 'react';
// 根据环境选择适当的hook
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
function MyComponent() {
useIsomorphicLayoutEffect(() => {
// 在浏览器中,这会表现得像useLayoutEffect
// 在服务器上,这会表现得像useEffect(实际上不执行)
// ...
}, []);
return <div>...</div>;
}
// 方法2:将DOM操作延迟到客户端
function SSRSafeComponent() {
const [isMounted, setIsMounted] = useState(false);
// 在客户端首次渲染时设置为已挂载
useEffect(() => {
setIsMounted(true);
}, []);
// 只在客户端渲染时才执行DOM操作
useLayoutEffect(() => {
// 这个effect只会在客户端执行
if (isMounted) {
// 安全进行DOM测量和操作...
}
}, [isMounted]);
return <div>...</div>;
}🔥 常见面试题目
(一) useEffect和useLayoutEffect的主要区别是什么?
解答: 两者的主要区别在于执行时机:
useEffect
• 异步执行
• 画面更新后执行
• 不阻塞渲染
useLayoutEffect
• 同步执行
• 画面更新前执行
• 阻塞渲染
两者API完全相同,只是执行时机不同,适用于不同场景。
(二) 为什么大多数情况下应该优先使用useEffect?
解答: 优先使用useEffect的原因:
- 1. 性能更好:不阻塞画面更新
- 2. 适合大多数场景:数据获取、订阅设置等不需要阻塞UI
- 3. SSR友好:服务器端渲染环境中表现更一致
- 4. 避免卡顿:耗时操作不会延迟UI显示
(三) 什么情况下应该使用useLayoutEffect?
解答: 适合useLayoutEffect的场景:
防止闪烁
元素位置或样式需要在用户看到前调整
DOM测量与定位
工具提示、弹出层需要精确定位
动画初始化
设置正确初始状态避免跳动
滚动位置控制
在页面显示前调整滚动位置
// 避免闪烁的简单例子
function Tooltip({ text, targetRef }) {
const tooltipRef = useRef(null);
useLayoutEffect(() => {
// 测量目标元素位置
const targetRect = targetRef.current.getBoundingClientRect();
// 在画面更新前定位提示框
tooltipRef.current.style.top = targetRect.bottom + 'px';
tooltipRef.current.style.left = targetRect.left + 'px';
}, []);
return <div ref={tooltipRef}>{text}</div>;
}(四) 如何在SSR环境中安全使用useLayoutEffect?
解答: 服务器没有DOM,无法执行布局效果。解决方案:
- 1. 创建同构hook,根据环境选择不同hook
- 2. 使用客户端检测,确保只在浏览器执行
// 同构hook方案
const useIsomorphicLayoutEffect =
typeof window !== 'undefined'
? useLayoutEffect
: useEffect;
// 客户端检测方案
function SafeComponent() {
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(true);
}, []);
useLayoutEffect(() => {
if (isBrowser) {
// 安全进行DOM操作
}
}, [isBrowser]);
return <div>...</div>;
}最佳实践:将需要布局测量的代码分离到纯客户端组件中。