文章中英模式
布魯斯前端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>;
}最佳實踐:將需要布局測量的代碼分離到純客戶端組件中。