題目與適用情境
一個 Next.js App Router 商品頁會預先渲染 HTML,再由瀏覽器載入 JavaScript,使收藏按鈕和庫存提醒表單 可以互動。線上錯誤監控顯示,少量首次造訪出現 React error #418;受影響使用者有時看到時間或主題閃動, 表單標籤偶爾短暫失去關聯,收藏按鈕也可能被重新建立。本機開發環境和後續客戶端導覽大多正常。
程式碼審查發現同一個 Client Component 在渲染期間做了四件危險的事:用瀏覽器預設時區格式化時間、依 localStorage 分支主題、用 Math.random() 產生表單 ID,並輸出一段可能形成無效巢狀結構的富文字。 正式環境鏈路前面還有 CDN,部分使用者安裝了會修改頁面的瀏覽器擴充功能。請說明如何找出真正造成伺服器 快照與客戶端首次渲染不同的環節,並提出不關閉整頁 SSR、不批次壓制警告的修復。
2026 年的 KORE1 資深前端面試題直接提出「正式環境出現 hydration mismatch、在本機無法重現」的排查 情境;MockIF 的 2026 React 題庫也把 hydration 列為進階架構問題。React 與 Next.js 官方文件明確要求 伺服器輸出與客戶端首次渲染一致,並列出時間、隨機值、瀏覽器 API、無效巢狀結構和外部 DOM 改寫等原因。 因此本題具有可核驗的當前面試代表性。核心能力是瀏覽器渲染與 React 水合除錯,分類為 frontend;題庫 中已有的事件迴圈、快取、效能、key 和陳舊閉包題都沒有涵蓋這條契約。
面試官考察重點
第一,能否準確描述水合。首次載入時,使用者先看到伺服器預先渲染的 HTML;瀏覽器解析它,React 再把元件 邏輯和事件處理器接到現有 DOM 上。Client Component 仍可能參與首次 HTML 預渲染,"use client" 不代表 首次載入只在瀏覽器渲染。比較邊界是「伺服器結果」與「客戶端第一次 render」,不是任意兩次後續渲染。
第二,能否從證據區分四類差異:應用輸入不一致,例如資料快照、時間、隨機值;執行環境不一致,例如 window、localStorage、語言和時區;瀏覽器把無效 HTML 修正成另一棵 DOM;以及 CDN、擴充功能或腳本 在 React 執行前改寫了節點。只盯著錯誤元件堆疊,很容易漏掉更早發生的解析或外部改寫。
第三,能否為不同根因選擇不同修復。穩定輸入應序列化並重用同一快照;使用者環境可以在伺服器由 cookie 或個人資料決定,也可以等水合後的 Effect 再更新;隨機 DOM ID 應使用 useId;無效巢狀結構應修正文件 結構;第三方函式庫確實無法預渲染時才局部關閉 SSR。
最後,能否證明問題消失。強回答會涵蓋正式環境建置、冷首次載入、慢速 JavaScript、時區與語言矩陣、 快取命中、乾淨瀏覽器與擴充功能環境,並同時檢查錯誤、DOM、互動狀態和視覺閃動。主控台安靜只是一個 結果,不是充分證據。
回答前需要釐清的問題
- 錯誤只發生在首次文件載入,還是客戶端導覽也會發生? 水合只針對既有 HTML 的首次接管。若只有
後續導覽出錯,應另查狀態、快取或非同步競態。
- 受影響的是文字、屬性、節點結構還是整棵子樹? 需要保存完整錯誤、元件堆疊、路由和發布版本,
避免只憑截斷後的正式環境錯誤編號猜測。
- 伺服器是否知道使用者的語言、時區、主題和實驗分桶? 能由 URL、cookie 或使用者資料決定的值應在
伺服器固定並傳給客戶端;未知值才需要穩定預留位置或水合後更新。
- 客戶端首次渲染使用的業務資料是否就是產生 HTML 的快照? 若瀏覽器在水合前已取得更新資料,首屏
必須仍從序列化快照開始,再在水合後重新整理。
- HTML 是否經過 CDN、翻譯代理、安全注入或最佳化器? 要取得來源站與邊緣回應,確認中間層有沒有
改寫空白、屬性、標籤或腳本順序。
- 錯誤是否只在裝有某類擴充功能、某個行動瀏覽器或某個地區出現? 這決定是否需要比較「網路回應正文」
和「React 啟動前的實際 DOM」。
- 頁面是否真的需要預渲染該子樹? 對 SEO、首屏和無 JavaScript 可讀性有價值的內容應保留 SSR;
純瀏覽器小元件才可能適合局部 ssr: false。
- 允許怎樣的視覺變化? 兩遍渲染可以保證水合一致,卻可能在慢速網路下產生明顯跳變;需要先約定
預留內容、版面穩定和無障礙要求。
30 秒回答框架
「水合要求伺服器 HTML 與客戶端第一次 render 產生同一結構和內容。我會先保存錯誤、元件堆疊、路由、 版本、語言和時區,然後對齊三份證據:網路收到的 HTML、瀏覽器解析且 React 尚未接管的 DOM、客戶端首次 渲染輸入。若網路 HTML 已不同,查資料快照和 CDN;若解析後才不同,查無效巢狀結構、擴充功能和前置腳本; 若 React 首次 render 不同,查時間、隨機值、瀏覽器 API 與提前重新整理。修復時讓兩端重用同一序列化 資料,明確固定 locale 和 timeZone,ID 用 useId,瀏覽器狀態用 cookie 或水合後的 Effect。 ssr: false 只隔離真正依賴瀏覽器的葉節點元件,suppressHydrationWarning 只處理無法避免的一層文字 差異。最後用正式環境建置和多環境冷載入確認沒有可恢復錯誤、重建、閃動或互動異常。」
分步深入解答
第一步:先寫出要守住的不變條件。
假設伺服器元件樹在輸入快照 S 下產生 HTML。瀏覽器可能把 HTML 解析為 DOM D,客戶端用輸入 C 做 第一次渲染。正常水合要求 D 與客戶端樹在 React 關心的節點、文字和屬性上相符。排查目標不是證明兩端 程式碼檔案相同,而是證明 S、解析過程和 C 最終得到相同結果。
React 官方說明不保證修補所有屬性差異;某些錯誤可恢復並重新產生子樹,最壞情況下事件處理器還可能接到 錯誤元素。因此不能把 hydration mismatch 當成單純的主控台雜訊。
第二步:建立可以重現正式環境首載的證據包。
記錄路由、部署版本、請求 ID、快取狀態、使用者語言、時區、主題來源、實驗分桶、User-Agent 和完整元件 堆疊,但不記錄敏感值。必須使用正式環境建置和完整重新整理;熱更新、開發雙呼叫或客戶端導覽不等同於 正式環境冷啟動。
對同一次請求保留三份內容:來源站或 CDN 回傳的回應正文、停用 JavaScript 時瀏覽器解析出的 DOM、啟用 JavaScript 後錯誤前後的 DOM。若回應正文正確而 Elements 面板裡的樹已經變化,優先檢查瀏覽器 HTML 修正、 擴充功能和啟動前腳本。若 DOM 直到 React 啟動才分叉,繼續核對首次 render 輸入。
第三步:逐一消除 render 中的非確定性。
下面的寫法會讓伺服器與瀏覽器使用不同環境,還讓每次 render 產生新 ID:
"use client"
export function StockNotice({ updatedAt }: { updatedAt: string }) {
const isBrowser = typeof window !== "undefined"
const theme = isBrowser ? localStorage.getItem("theme") ?? "light" : "light"
const inputId = `stock-${Math.random()}`
return (
<section data-theme={theme}>
<time dateTime={updatedAt}>{new Date(updatedAt).toLocaleString()}</time>
<label htmlFor={inputId}>到貨提醒</label>
<input id={inputId} />
</section>
)
}優先讓伺服器決定穩定值,並把同一值當成 Client Component 的首次輸入。語言、時區和主題如果已保存在 URL、cookie 或使用者資料中,就在伺服器讀取;時間格式化要明確傳入 locale 與 timeZone。DOM ID 使用 useId,前提是伺服器和客戶端元件樹本身保持一致:
"use client"
import { useId } from "react"
interface StockNoticeProps {
updatedAt: string
updatedLabel: string
initialTheme: "light" | "dark"
}
export function StockNotice({
updatedAt,
updatedLabel,
initialTheme,
}: StockNoticeProps) {
const inputId = useId()
return (
<section data-theme={initialTheme}>
<time dateTime={updatedAt}>{updatedLabel}</time>
<label htmlFor={inputId}>到貨提醒</label>
<input id={inputId} />
</section>
)
}如果伺服器確實不知道瀏覽器時區,就先渲染雙方一致的 UTC 標籤或固定預留內容,水合後在 useEffect 中 換成本地格式。這會多一次 render,必須預留尺寸並評估慢速網路下的跳變。業務資料也採用同一原則:HTML 使用的資料快照應隨頁面傳給客戶端,背景重新整理只能在水合完成後更新畫面。
第四步:檢查瀏覽器解析和外部改寫。
瀏覽器會寬容地修正無效 HTML。例如區塊元素放進段落、按鈕巢狀按鈕時,瀏覽器解析後的 DOM 可能已不是 React 產生的結構。應先修正語意和巢狀結構,再用 HTML 驗證與 DOM 檢查確認。不要用 Effect 掩蓋結構錯誤。
若只有邊緣快取命中時失敗,比較來源站與邊緣回應並關閉會改寫 HTML 的最佳化。若只有特定擴充功能環境 失敗,用乾淨瀏覽器重測並確認擴充功能在 React 前修改了什麼;應用可以記錄影響範圍,但不應為了不可控 擴充功能壓制所有真正的應用錯誤。第三方腳本也按同樣方法二分:逐個移出首載路徑,找到第一處 DOM 寫入。
第五步:依成本選擇逃生口。
useEffect 適合水合後才能讀取的環境資訊,但會增加第二遍渲染。dynamic 搭配 ssr: false 適合無法在 伺服器執行、又不承擔 SEO 和首屏內容的局部第三方元件;把整頁改成純客戶端會犧牲預渲染價值並擴大空白期。 suppressHydrationWarning 只是一層深度的逃生口,React 不會替你修補被壓制的文字;它適合真正無法避免的 單點差異,不適合時間、隨機 ID、主題或錯誤資料等可修復根因。
第六步:用故障矩陣證明修復。
至少涵蓋兩種語言、兩個時區、明暗主題、登入與匿名、快取命中與未命中、正常與慢速 JavaScript、乾淨設定 與已知擴充功能。每個組合做完整重新整理,斷言沒有 hydration 可恢復錯誤,關鍵子樹沒有被重建,輸入焦點 和事件正常,時間與主題在預期時機更新,版面沒有明顯跳動。再對 Date.now、Math.random、無參數 toLocaleString、render 中的 window 或 localStorage、危險巢狀結構和大範圍 suppressHydrationWarning 做靜態審查,防止復發。
高品質示範回答
「我先把問題限定在首次載入。Next.js 首次會讓 Client Component 參與預渲染,瀏覽器拿到 HTML 後,React 必須用同一初始輸入產生相同樹再接管事件。當前程式碼在 render 中讀取 localStorage、使用瀏覽器預設時區 並產生隨機 ID,三個值都可能與伺服器不同;富文字還可能被瀏覽器改寫結構。
我會從一筆真實錯誤取得完整元件堆疊、版本、locale、timeZone、主題來源與快取狀態,保存網路回應,並在 React 啟動前比較解析後的 DOM。回應已經錯就查伺服器快照和 CDN;解析後才變就查無效巢狀結構、擴充功能 或前置腳本;React 啟動時才變就對照客戶端首次 props 和環境分支。
修復上,伺服器從 cookie 或資料決定 locale、timeZone 和初始主題,產生 updatedLabel 並連同 ISO 時間 傳給元件;客戶端首次直接使用這些值,ID 改成 useId,HTML 巢狀結構依語意修正。伺服器不知道的瀏覽器 資訊先用固定預留內容,等 Effect 後更新。業務資料先重用產生 HTML 的序列化快照,再於背景重新整理。
我不會全域關閉 SSR,也不會用 suppressHydrationWarning 隱藏可修復問題。只有純瀏覽器第三方小元件才 考慮局部 ssr: false;只有無法避免的一層展示差異才用 suppress。最後執行正式環境建置,在語言、時區、 主題、快取、慢速網路和乾淨瀏覽器矩陣做冷載入,確認無 error #418、無子樹重建、無焦點遺失和可見閃動。」
常見錯誤
- 認為加上
"use client"就沒有伺服器 HTML。 首次載入時 Client Component 仍可被預渲染。 - 在 JSX 中用
typeof window分兩套 UI。 伺服器分支與瀏覽器第一次 render 天然可能不同。 - 把所有值移進 Effect 就算修好。 它能延後差異,卻可能造成雙渲染、空白、版面跳動和慢速網路體驗下降。
- 給根節點批次加
suppressHydrationWarning。 它只壓制有限層級的警告,也不會修補不一致文字。 - 看到正式環境錯誤就把整頁設為
ssr: false。 這會失去預渲染效益,也沒有解釋真正根因。 - 只比較「檢視原始碼」和水合後的 Elements。 中間缺少瀏覽器解析後、React 啟動前的 DOM 證據。
- 只測本機開發和客戶端導覽。 正式環境建置、完整重新整理、邊緣快取、時區和擴充功能才可能觸發原故障。
- 斷言主控台無警告就結束。 還要確認沒有重建、事件錯綁、焦點遺失、閃動與錯誤業務資料。
追問及應對
為什麼不能直接對所有時間文字使用 suppressHydrationWarning?
它是有限的一層逃生口,React 也不會替你修補被壓制的文字。若時間差來自兩端預設時區不同,根因可以用 明確 locale、timeZone 或共享格式化結果消除。只有產品明確接受首次文字與水合後文字不同,且該差異無法 避免時才考慮使用,並驗證輔助技術讀到的內容和視覺變化都可接受。
useEffect 與 ssr: false 應該如何選擇?
如果元件的穩定外殼和主要內容有預渲染價值,只是某個標籤依賴瀏覽器資訊,保留 SSR 並在 Effect 後更新 局部內容。若整個葉節點元件依賴無法在伺服器執行的第三方函式庫,預渲染沒有意義,也能提供尺寸穩定的 loading 狀態,才局部關閉 SSR。選擇依據是內容價值、依賴邊界和視覺成本,不是哪個寫法更快消掉警告。
為什麼無效 HTML 會造成 React 程式碼兩端相同卻仍然失敗?
伺服器傳送的是字串,瀏覽器會依 HTML 解析規則建立並修正 DOM。錯誤巢狀結構可能被自動閉合、移動或刪除, React 接管時面對的是修正後的樹,不是原字串表達的樹。比較回應正文和 React 啟動前的 DOM,並透過語意 修正與 HTML 驗證解決,才能關閉這條差異。
正式環境偶發且無法取得使用者頁面時,如何增加可觀測性?
收集可恢復錯誤的完整訊息或編號、元件堆疊、路由、部署版本、locale、timeZone、主題來源、快取狀態和 瀏覽器類型,並做抽樣與隱私過濾。為關鍵首屏輸入記錄不可逆摘要或版本號,用來判斷伺服器快照與客戶端 首次快照是否一致。指標依路由、版本和環境切片;修復後既看錯誤率歸零,也看子樹重建、互動和視覺指標 沒有倒退。