資深前端模擬面試:從效能觀念題到 useRef 實作題的深度驗收
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(17)
1. Outline
- 起點 · Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]
一場完整錄下的資深前端模擬面試:前 18 分鐘是 12 題觀念問答(效能、React 內部機制、JavaScript 地基、框架與網路),後 12 分鐘是一道 stopwatch 實作題,用來驗收「答得出來」和「寫得出來」之間的落差。
2. YouTuber 的思維推導
Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]
Dev. Aditya · 30m36s · 字幕 en · vision=on (使用者指定 vision=true。影片後半是螢幕分享的實作題,畫面上的程式碼是理解卡關點與修正的關鍵,讀圖才看得懂 ref.current、clearInterval 的位置。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 2s |
| segment | 1m31s | 3m30s |
| shot | 56s | 4s |
| analyze | 12m30s | 15m00s |
| render | 5s | – |
這支影片是一場真實錄下的資深前端模擬面試,作者(面試官 Dev.Aditya)的推導主軸不是「教觀念」,而是「示範一場面試怎麼由淺入深地測出一個人的深度」。他的出發點是最開放的效能題,因為這題會逼受試者自己攤開知識地圖;接著他從受試者主動說出的名詞(tree shaking)往下追問,確認那是真懂還是背詞。
確認基本盤之後,他把問題往「這件事底層怎麼運作」推:reconciliation、瀏覽器渲染流程;再往「同一個目的的兩種手段有何差別」推:debounce vs throttle、useRef vs useState。接著轉進 JavaScript 語言本身的 closure 與 this,測的是框架之外的地基。中段他刻意留了一題受試者答不出來的 global error boundary,並在對方繞了三次之後直接給答案(componentDidCatch),示範面試官在卡關時該怎麼收——不羞辱、給答案、往下走。
理論題收尾在幾個「不要一次全給」的優化題(虛擬列表、code splitting、WebSocket)之後,他把整場交給實作:一題看起來只要五行的 stopwatch。作者真正的結論藏在這一題——他讓受試者依序踩進四個坑(interval id 存在區域變數所以清不掉、hook 巢狀呼叫、只清 interval 沒清 ref、start 沒有守門條件),每一步只給最小的提示,讓觀眾看見「觀念都答得出來」和「手真的寫得出來」之間的落差有多大。所以整支影片的推論是:資深前端的門檻不在能不能講出 closure 的定義,而在能不能把 useRef 這個你剛剛才正確定義過的東西,在有壓力的十分鐘裡正確地用出來。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 效能優化:從程式碼內到打包外 → 受試者把效能的討論停在「React 大多會自動處理」,但沒有說 React 自動處理的機制到底是什麼。面試官抓住這個缺口,下一題直接問:React 內部是怎麼決定只更新變動的那一塊?
- Reconciliation 與 diffing 演算法 → 受試者說明了 React 決定「哪些 DOM 要改」之後就停住了,但沒說 DOM 改完之後瀏覽器還要做什麼才會變成畫面。面試官順著這個沒接上的下半段,把問題交給瀏覽器本身。
- 瀏覽器渲染流程:Critical Rendering Path → 到這裡兩段都在談「瀏覽器與框架自己做的事」。面試官接著把問題轉向開發者主動控制的部分:當使用者高頻觸發事件時,你怎麼決定要不要真的執行?
- Debounce 與 Throttle 的差別 → 這一段是本場第一次出現「兩個功能相近的工具怎麼選」的題型,而且答案的關鍵在時間與時機。面試官接著把層級從框架 API 往下拉到 JavaScript 語言本身:既然 debounce 要在延遲後還記得原本的參數與計時器,那支撐這件事的語言機制是什麼?
- Closure:函式記得外層變數 → closure 說明了函式怎麼決定「看得到哪些變數」,但還有另一個變數的可見性不是由出生位置決定的——this。面試官接著把問題轉到這個唯一的例外。
- this 的指向與三種調整方式 → 這一段結束了 JavaScript 語言地基的檢查——closure 與 this 都過關了。面試官接著回到 React,而且挑了一組跟 debounce / throttle 同型的題目:兩個看起來都在「記住一個值」的 hook,差別在哪裡?
- useRef 與 useState 的分工 → useRef 與 useState 都是 hooks,但 hooks 是後來才出現的。面試官接著往回追一層:hooks 之前 React 是怎麼處理元件的誕生與消失?
- 生命週期方法到 useEffect → 受試者強調自己只用過函式元件與 hooks。面試官記下了這一點,並在下一題設下陷阱——問一個至今只有 class 元件才有原生解法的問題。
- SSR 為什麼快,又為什麼利於 SEO → 受試者順利答完框架層的正向題,而且再次展現他熟悉的是 Next.js 的功能而非 React 的底層機制。面試官現在用上一段記下的那條線索設陷阱:問一個只有 class 元件才有原生解法的題目。
- 全域錯誤邊界:卡關與正解 → 這一題受試者卡住了,面試官給完答案就換題。他接下來挑的是幾個受試者一定答得出來的優化題,用意是在進入實作前把節奏調回來——而這些題目共享同一個想法:不要一次把全部東西交出去。
- 一萬筆列表與 code splitting → 這兩題都在減少「一次送出去的量」,但都還是客戶端主動去要資料。最後一題觀念題把方向反過來:如果需要伺服器主動推資料給你呢?
- WebSocket 與請求/回應模型的差別 → 到這裡所有觀念題都問完了,受試者除了 error boundary 之外幾乎全數答對,包含第 7 段那個 useRef 的定義。面試官接著要驗收的是最後也最關鍵的一件事:這些答得出來的觀念,手寫得出來嗎?
- 實作題開場:碼表的三個要求 → 第一版的 setInterval id 沒有被存在任何跨 render 存活的地方。下一段面試官按下 stop,這個結構性問題立刻現形。
- clearInterval 失效:提示改用 useRef → 受試者拿到了「把 interval 存進 ref」這個方向,開始動手改寫。但他把 useRef 寫進了 useEffect 裡面,下一段編輯器立刻報錯。
- hook 不能巢狀,與命名的建議 → 受試者的 ref 寫成了 stopWatchRef 而不是 stopWatchRef.current。下一段面試官先補上這個 .current,然後請他連按四次 start——真正的考點終於登場。
- 連按 start 的 bug 與錯誤方向 → 三個方向都被擋掉,而編輯器的型別錯誤已經把答案指出來了:ref 從來沒有被設回 null。下一段面試官揭曉這個 bug,並補上最後一塊拼圖。
- 真正的修法:清掉 ref 並在啟動前檢查
3. 逐段說明
Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]
1. 效能優化:從程式碼內到打包外 0:00–2:55
第一題直接問「你怎麼改善應用程式效能」。答題者建立了一條由內而外的檢查順序:先用 profiling 工具找不必要的 re-render,用 memo / useMemo / useCallback 擋掉;再看 state 結構是否造成連鎖 re-render,把 prop drilling 的狀態上移到 Redux 等全域狀態;再把圖表這類重元件改成 dynamic import 搭配 Suspense。最後才走出程式碼,看 bundle 大小、tree shaking 與 gzip。面試官追問 tree shaking,答題者說明它是把沒被用到的 import 從 bundle 移除。
推理因為開場題必須讓受試者自己攤開知識地圖,所以面試官挑了範圍最大的效能題;而受試者的回答方式本身就是答案的一部分——他沒有隨口丟名詞,而是建立了一條由內而外的檢查順序:先看元件層(不必要的 re-render,用 memoization 擋掉)、再看狀態層(結構不良與 prop drilling 造成連鎖 re-render,把狀態上移到全域)、再看載入層(重元件改 dynamic import 搭 Suspense),最後才走出程式碼看 bundle 本身(大小、tree shaking、gzip)。面試官在他說出 tree shaking 之後立刻回頭追問定義,這是在確認名詞是背來的還是懂的。
AI 補充這條順序值得記下來,因為它同時是一份除錯清單:從「畫面重畫太多次」往「送給瀏覽器的東西太多」走。受試者省略了一步——他說「用 profiling 工具找不必要的 re-render」,但沒說怎麼判定「不必要」。實務上的判準是:這次 re-render 有沒有讓畫面的任何一個像素改變;React DevTools Profiler 的 highlight updates 可以直接看出哪些元件重畫了卻沒改變外觀。另外他把 memoization 講得像萬用解,但 React.memo / useMemo 本身也有比較成本,只有在元件重、或它的 props 幾乎不變時才划算;先修 state 結構通常比到處包 memo 有效。至於他最後說「用 React 這類框架大多會自動處理」,指的是 bundler(webpack / Vite / Turbopack)在 production build 預設就會做 tree shaking 與壓縮,不是 React 本身做的。
2. Reconciliation 與 diffing 演算法 2:55–3:48
面試官從「效能」轉向「React 為什麼能只更新一部分」。答題者說明 React 維護一份 virtual DOM(真實 DOM 的副本),變動時拿新舊 virtual DOM 比對,用 diffing 演算法找出改變的部分再更新真實 DOM,整個比對加更新的流程就叫 reconciliation。
推理因為上一段整段都在談「少做一點事」,所以面試官接著問 React 內部本來就在做的那件「少做一點事」是什麼。受試者的推導是三步:React 在記憶體裡維護一份 virtual DOM,也就是真實 DOM 的輕量副本;狀態變動時產生一份新的 virtual DOM,拿去和舊的比對;比對用的是 diffing 演算法,找出差異後只把那部分寫回真實 DOM。他明確把「比對 + 更新」這整條流程命名為 reconciliation,把演算法本身命名為 diffing。
- CReact維護virtual DOM,狀態變動時用diffing演算法比對,只把差異寫回真實DOM→ 畫地圖
- Ediffing演算法的兩個關鍵假設:不同型別整棵重建、同層子元素靠key配對→ 存+演練
AI 補充這個回答正確但少了關鍵的一步:為什麼要多維護一份副本?因為直接操作真實 DOM 很貴——每次寫入都可能觸發瀏覽器重算 layout。virtual DOM 是用便宜的 JavaScript 物件比對,換掉昂貴的 DOM 寫入。另外受試者沒提到 diffing 演算法的兩個關鍵假設,而這正是上一段 re-render 問題的根源:第一,不同型別的元素直接整棵重建,不往下比;第二,同一層的子元素靠 key 來配對,這就是列表沒給 key、或拿陣列 index 當 key 會出錯的原因。理解這兩點,才知道為什麼上一段說的「memo 擋 re-render」是在 reconciliation 之前就先剪掉一整棵子樹,效果比讓 diffing 慢慢比對好得多。
術語:diffing algorithm
3. 瀏覽器渲染流程:Critical Rendering Path 3:48–4:36
問題再往下一層:瀏覽器拿到 bundle 之後怎麼變成畫面。答題者依序拆解——解析 HTML、解析 CSS、建 DOM、建 CSSOM,接著算 layout、paint,最後交給 GPU 合成並輸出像素,並指出這整條流程叫 critical rendering path。
推理因為上一段把責任交棒給了瀏覽器,所以面試官順勢把層級再往下推一層。受試者依序拆出這條流水線:解析 HTML、解析 CSS,分別建出 DOM 與 CSSOM;把兩者合起來算 layout,決定每個元素的位置與大小;接著 paint 畫出各層的內容;最後交給 GPU 合成,輸出成螢幕上的像素。他自己把整條流程命名為 critical rendering path。
AI 補充順序正確,但中間漏了一個對前端最有實務意義的環節:paint 之後、送 GPU 之前還有 compositing(合成)這一層,瀏覽器把畫面拆成多個 layer 分別上傳 GPU,再由 GPU 疊起來。這一層之所以重要,是因為它決定了哪些 CSS 屬性便宜、哪些貴:改 width / top 會回到 layout 重算(reflow),改 background-color 從 paint 開始重來,而改 transform / opacity 只動到合成階段,可以完全交給 GPU。這正是「動畫用 transform 不要用 top / left」這條老規則的來源。另外他也沒提到這條路徑會被阻塞——外部 CSS 是 render-blocking,沒有 defer / async 的 script 會擋住 HTML 解析,這才是上一段講的 bundle 大小之所以會拖慢首次畫面的真正機制。
4. Debounce 與 Throttle 的差別 4:36–6:20
面試官問兩者差異。答題者先說共同目的:減少連續觸發的使用者事件;差別在「怎麼減」。debounce 是等使用者停手後延遲時間到才觸發一次;throttle 是在每段延遲區間內最多觸發一次。他用 300ms 搜尋框舉例:連續輸入時 debounce 只在停下後發一次,而 throttle 在同樣情境會固定間隔各發一次。面試官複述確認 throttle 是固定間隔執行。
推理因為前三段都在談「一次更新有多貴」,所以面試官接著問「怎麼少觸發幾次」。受試者先點出兩者的共同目的:減少連續的使用者事件;再把差別收在一句話上——debounce 是延遲之後只發一次,throttle 是每段延遲區間內最多發一次。接著他用 300 毫秒的搜尋框把差別演出來:連續打字時 debounce 會一路等到使用者停手、再過 300 毫秒才發一次;同樣的輸入在 throttle 下則會固定每 300 毫秒發一次。面試官複述「throttle 是固定間隔執行」來確認理解一致。
AI 補充受試者的定義正確,但漏了選擇的判準,而那才是面試想聽的:問「中間過程的結果有沒有用」。搜尋建議只有最後一次輸入有意義,用 debounce;捲動位置、滑鼠拖曳、視窗縮放這類過程本身就要即時反饋,用 throttle。另一個他沒提的細節是 leading / trailing——debounce 預設在延遲結束時執行(trailing),但按鈕防連點需要的是 leading:立刻執行第一次、後續忽略。這一點在本片最後的實作題裡會再出現一次,因為受試者遇到連按 start 的問題時第一個想到的正是 debounce。
5. Closure:函式記得外層變數 6:20–7:07
轉進 JavaScript 語言本身。答題者定義 closure 為:內層函式能記住並存取外層函式的變數,即使外層函式已經執行完畢。他用 count 變數與 increment 內層函式舉例,並點出這在多數其他語言不會發生,是 JavaScript 的特性。
推理因為前面幾題都靠框架 API 就能答,面試官需要確認地基是不是也穩,所以問了 closure。受試者的定義是:一個函式能記住並存取它外層函式的變數,即使外層函式早已執行完畢。他用最小的例子演示——外層函式裡有一個 count 變數與一個 increment 內層函式,呼叫 increment 時仍然能存取 count。他還補了一句對照:這在多數其他語言不會這樣運作,是 JavaScript 的特性。
AI 補充定義正確,但少了「為什麼」。關鍵在於 JavaScript 的函式在被建立時,會帶著一個指向它出生環境的參照;只要這個函式還活著,那個環境就不會被垃圾回收,即使建立它的函式已經 return。所以 closure 不是函式「複製」了變數,而是「共用」了同一個變數——這解釋了為什麼在迴圈裡用 var 建立多個函式時,它們最後拿到的是同一個值,而 let 因為每圈都建立新的綁定就不會有這個問題。closure 也是這一場後半實作題的隱形主角:受試者把 setInterval 的 id 存進區域變數時,那個變數確實被 closure 記住了,但每次 re-render 都會建立一份新的,於是 stop 按鈕拿到的是新的那份、清不掉舊的計時器。
6. this 的指向與三種調整方式 7:07–8:38
延續語言特性,面試官問 this。答題者給的定義是:this 指向「呼叫該函式的那個物件」,物件呼叫自己的方法時 this 就是該物件,否則落到全域 this。接著補上箭頭函式沒有自己的 this、會從語彙作用域往外拿,以及可用 bind / call / apply 指定 this,並提到 explicit binding 與 implicit binding 兩種綁定。
推理因為上一段已經建立了「作用域由出生位置決定」的框架,所以這一題自然是它的對照。受試者給的定義是:this 指向呼叫該函式的那個物件;用 obj.fn() 呼叫時 this 就是 obj,否則落到全域 this。接著他主動加上一層對比:箭頭函式沒有自己的 this,會從語彙作用域往外拿——也就是回到上一段那條「由出生位置決定」的規則。最後他補上第三層:想要更大的彈性可以用 bind、call、apply 明確指定 this,並把這兩類情況命名為 explicit binding 與 implicit binding。
AI 補充這是全場最完整的一個回答,三層結構清楚。可以補的是完整的優先順序,實務上按這個順序判定:new 綁定(this 是新建的物件)> 明確綁定(bind / call / apply)> 隱式綁定(obj.fn())> 預設綁定(非嚴格模式下是 globalThis,嚴格模式下是 undefined)。而箭頭函式不參與這整套規則,它根本沒有自己的 this。另外要注意一個受試者沒點破的陷阱:隱式綁定會在函式被「取出來」時消失——const f = obj.fn; f() 的 this 就不是 obj 了,這正是把方法直接當 callback 傳進 addEventListener 或 setTimeout 時會出錯的原因,也是 class 元件時代要在 constructor 裡寫 this.handleClick = this.handleClick.bind(this) 的理由。
7. useRef 與 useState 的分工 8:38–9:49
回到 React。答題者用「會不會觸發 re-render」切開兩者:useRef 用來記住一個跨 re-render 仍然存在的值,更新它不會重新渲染;useState 是會觸發 re-render、進而更新畫面的狀態。他用 focus 元素當例子說明何時該選 useRef,面試官複述確認「useState 才觸發 re-render」。
推理因為第 4 段的 debounce / throttle 已經證明受試者能用一句話切開相近的概念,面試官再用同型題確認他在 React 上也做得到。受試者抓的判準是「會不會觸發 re-render」:useRef 用來記住一個跨 re-render 仍然存在的值,改它不會重新渲染;useState 是會觸發 re-render、進而讓畫面更新的狀態。他用「想 focus 某個元素、但不想為此重新渲染」當例子。面試官複述確認只有 useState 會觸發 re-render。
- CuseRef記住跨re-render仍存在的值,改它不會觸發re-render;useState才會→ 畫地圖
- AuseRef像口袋隨身記事本寫了馬上看得到;useState像公告欄要等下次巡視才更新→ 批判類比
AI 補充判準抓得準,但少了一個更根本的差別:useRef 的更新是同步且立即可見的,ref.current = 1 之後馬上讀就是 1;而 useState 的更新是排程的,setState 之後在同一次 render 裡讀到的仍是舊值。這條差別決定了什麼東西該放進 ref——凡是「需要立刻讀到最新值、而且不該影響畫面」的東西:計時器 id、上一次的值、DOM 節點、是否為首次 render 的旗標。反過來說也有一條硬規則:ref 的改變不會通知 React,所以任何要顯示在畫面上的東西都不能只放在 ref 裡。這一段的內容會在二十分鐘後的實作題被逐字驗收——那題的正解就是「把 interval id 放進 ref」,而受試者在這裡明明答對了定義,實作時卻卡住。
8. 生命週期方法到 useEffect 9:49–11:02
面試官問 React 的生命週期。答題者以 hooks 出現為分界:以前有 componentDidMount、componentDidUnmount 等在元件誕生與銷毀時觸發的方法;hooks 之後改由 useEffect 承接。他說明依賴陣列的三種行為——空陣列時只在掛載時跑、回傳的函式在卸載時跑、陣列內的值變動時重新觸發。
推理因為上一段確認受試者熟悉 hooks,面試官用生命週期題測他知不知道 hooks 取代了什麼。受試者以 hooks 的出現為分界線推導:以前有 componentDidMount、componentDidUnmount 這類在元件掛載與卸載時觸發的方法,統稱 lifecycle methods;hooks 之後這些職責改由 useEffect 承接。接著他拆解依賴陣列的三種行為——空陣列時只在掛載時跑一次、回傳的函式在卸載時跑、陣列裡的值變動時重新觸發。
AI 補充推導正確,但有兩處需要修正。第一,正確的方法名是 componentWillUnmount,不是 componentDidUnmount——元件卸載後就沒有實例可以呼叫方法了,所以只有 will 沒有 did。第二,他在描述空依賴陣列時說「會在元件 unmounted 時觸發」,前後文顯示他要說的是 mounted,這是口誤但值得記住正確版本:空陣列 = 只在掛載後跑一次。更重要的是一個他完全沒提到的觀念轉向:useEffect 不是 lifecycle 的一對一替代品。React 官方文件明確指出,思考方式應該從「在哪個時間點做什麼」換成「這個 effect 要跟哪些值同步」。依賴陣列不是效能開關,而是宣告「我用到了這些值」;漏填就會造成上一段提過的 stale closure。React 18 的 StrictMode 在開發模式下會刻意把 effect 掛載、卸載、再掛載一次,就是為了逼出沒有正確清理的 effect。
「I think in it's React 13, hooks was introduced」
「If the dependency array is empty, whatever is there in the use effect will fire when the component is unmounted」
9. SSR 為什麼快,又為什麼利於 SEO 11:02–13:02
問題擴大到 Next.js 的 SSR。答題者說明伺服器先算好、建好 DOM 再把 HTML 交給客戶端,因此渲染是即時的;且 SSR 每一個 request 都會執行,適合資料頻繁且不可預期更新的頁面,例如電商的商品列表頁。面試官追問其他好處,答題者提到效能,面試官補上 SEO,答題者接著說因為 HTML 已在伺服器產好,LCP、FCP 這些指標會比較好看。
推理因為前面幾段都在談「畫面在瀏覽器裡怎麼產生」,面試官把同一個問題挪到伺服器端:如果 HTML 在伺服器就先算好,會怎麼樣?受試者的推導是:伺服器先計算資料、建好 DOM 元素、把完成的 HTML 交給客戶端,所以渲染是即時的,不必等客戶端再發 API 請求。接著他補上 SSR 的一個特性——每一個 request 都會重新執行,因此適合資料頻繁且不可預測更新的頁面,並以電商的商品列表頁為例。面試官追問還有什麼好處,受試者只答得出效能,面試官直接補上 SEO;受試者接住並說明因為 HTML 已在伺服器產好,LCP、FCP 這些指標會比較好看。
AI 補充回答方向正確,但把兩件事混在一起了。SSR 讓使用者「更早看到內容」是真的(改善 FCP、LCP),但頁面要能互動還得等 JavaScript 下載並完成 hydration——在那之前按鈕是沒反應的。所以 SSR 改善的是「看得到」的時間,不是「用得動」的時間,有時甚至會讓可互動時間變晚。另外受試者說 SSR 適合「頻繁不可預測更新的頁面」是對的,但漏了 Next.js 真正的決策矩陣:內容幾乎不變用 SSG(build 時產生)、會變但可容忍幾秒延遲用 ISR(定時重新產生)、必須每次都最新或跟使用者身分有關才用 SSR。電商商品列表其實最常用 ISR,因為它比 SSR 便宜得多。至於 SEO,現代 Google 爬蟲確實會執行 JavaScript,但排隊渲染會延遲索引,而且其他社群平台的爬蟲多半不執行 JS——這才是 SSR 對 SEO 真正的價值。
術語:LCP / FCP
「First input delay and it will make it better」
10. 全域錯誤邊界:卡關與正解 13:02–15:40
這題答題者答不出來。他先把 global error boundary 理解成「出錯時顯示的 fallback 畫面」,提到 Next.js 的 error page,又轉去講在 API 共用函式裡用 try/catch。面試官兩次把問題拉回「任何元件出錯時如何全域捕捉、避免整個 app 崩潰」,並問他有沒有 class component 經驗;答題者說只用過 function component。最後面試官直接給出答案:class component 的 componentDidCatch 方法,包住整個應用來攔截錯誤,而 function component 沒有對應寫法。
推理因為前面確認了受試者的 React 知識建立在 hooks 之上,面試官問 global error boundary 就是要測那條界線在哪。受試者的推導繞了三圈:先把它理解成「出錯時顯示的 fallback 畫面」,提到 Next.js 有 error page 功能;再轉去講在共用的 API 函式裡用 try / catch 捕捉。面試官兩次把問題拉回精確的定義——任何元件裡發生錯誤時如何全域捕捉、避免整個應用崩潰——並直接問他有沒有 class 元件經驗,受試者答只用過函式元件。他最後猜了 AggregateError,仍不對。面試官於是揭曉:class 元件有 componentDidCatch 方法,用它包住整個應用就能即時攔截錯誤;而函式元件沒有對應的寫法。
AI 補充面試官的正解正確但講得太短,有兩點值得補。第一,error boundary 需要兩個方法搭配:getDerivedStateFromError 負責更新 state 以顯示 fallback UI,componentDidCatch 負責記錄錯誤(送到 Sentry 之類的服務)。第二,也是最實用的一點——實務上今天沒有人手寫 class,大家用的是 react-error-boundary 這個套件,它把 class 包好,對外提供函式元件的 API 與 useErrorBoundary hook。至於受試者提到的 Next.js error page 其實不算離題:App Router 的 error.tsx 就是框架幫你套好的 error boundary,只是它綁定路由層級,無法像手動放置的 boundary 那樣精細地包住某一個 widget。還有一個兩邊都沒提的邊界:error boundary 只攔得住 render 階段的錯誤,事件處理器、setTimeout、以及非同步程式碼裡的錯誤它一律接不到——那些仍然要靠 try / catch,也就是受試者最先講的那個方向。所以他其實答對了一半,只是沒能把兩半接起來。
「But, in functional based component, we do not have any community we can say」
11. 一萬筆列表與 code splitting 15:40–17:45
兩題共用同一個想法:不要一次把全部東西交出去。列表題答題者提出 virtual list——只渲染使用者看得到的項目,捲動時再把其他項目掛上 DOM;面試官追問函式庫,答案是 react-window。code splitting 題他用五、六頁的應用舉例:使用者只進 settings 頁時,就只送該頁需要的程式碼,面試官順勢把它歸結為「降低 bundle 大小」。
推理因為要在進入實作前確認基本盤穩固,面試官連問兩題「怎麼不要一次全給」。列表題受試者答 virtual list——只渲染使用者視窗內看得到的項目,捲動時再把後續項目掛上 DOM,把一萬個節點壓到十幾個;面試官追問函式庫,受試者猜了「React Virtual DOM」,面試官補正為 react-window。code splitting 題他用五、六頁的應用舉例:使用者只進 settings 頁時就只送該頁需要的程式碼,不必把全部頁面的程式一起下載;面試官順勢把它歸結成一句「就是降低 bundle 大小」。
AI 補充兩題的答案都對,但可以各補一層。虛擬列表最難的不是「只渲染看得到的」,而是撐出正確的捲軸高度與處理高度不固定的項目——項目高度不一時要邊捲邊量測、動態修正,這是 TanStack Virtual 這類函式庫的主要價值。另外現在有純 CSS 的輕量方案 content-visibility: auto,適合結構簡單的長列表。至於 code splitting,受試者說得像是伺服器有選擇性地送——實際上是 bundler 在打包時就把程式切成多個 chunk,瀏覽器依路由或 dynamic import 去要對應的檔案。這正好接回第 1 段他自己提過的 dynamic import:那是寫在原始碼裡的切點,code splitting 是它在打包層的結果。要注意切得太碎反而變慢,因為每個 chunk 都是一次額外的網路往返。
術語:virtualization
「There are a bunch of libraries that we can we can use like React Virtual DOM is there」
12. WebSocket 與請求/回應模型的差別 17:45–18:48
最後一題觀念題。答題者以 HTTP 的「一個 request 對一個 response」作對照:WebSocket 是先建立連線、監聽某個 room,之後送進該 room 的資料會持續推給客戶端,不需要客戶端不斷發請求。面試官複述為「即時資料傳輸」,答題者補上股票即時報價的使用情境。
推理因為前面所有題目都預設 HTTP 的請求/回應模型,面試官用最後一題觀念題檢查受試者知不知道還有別的模型。受試者的推導從對照開始:HTTP 是一個 request 對一個 response;WebSocket 則是先建立連線、開始監聽某些 room,之後只要有資料送進那個 room 就會持續推給客戶端,客戶端不需要一直發請求。面試官把它複述成「即時資料傳輸」,受試者補上股票即時報價當使用情境。
AI 補充描述正確,但「room」其實不是 WebSocket 本身的概念——那是 Socket.IO 這類函式庫在協定之上加的抽象。原生 WebSocket 只給你一條雙向管線,房間、自動重連、斷線緩衝都要自己或靠函式庫實作。另外面試官沒追問、但資深職位通常會考的是選型:只需要伺服器單向推播(通知、進度條、AI 逐字輸出)時,Server-Sent Events 更簡單——它走 HTTP,瀏覽器內建自動重連,也不需要另一套基礎設施。真正需要 WebSocket 的是雙向且高頻的場景:協作編輯、多人遊戲、聊天室。至於受試者舉的股票報價,兩者都可行,取決於客戶端要不要送東西回去。還有一個常被忽略的實務點——WebSocket 是長連線,伺服器端要處理連線數上限、心跳偵測與水平擴展時的訊息廣播,成本比看起來高。
術語:polling
13. 實作題開場:碼表的三個要求 18:48–21:09
進入螢幕分享的實作題。面試官示範需求:兩顆按鈕,按 start 每秒累加,按 stop 立即停住,再按 start 從停下的數字接著跑,而且連按多次 start 也不能加速。答題者打開本機編輯器(提到公司目前用 Claude Code),先用 setInterval 寫出第一版,面試官提醒先把 CTA 做成按鈕、外觀不用管。


推理因為前面的問答已經證明受試者的知識廣度足夠,面試官改測執行力。他先在瀏覽器裡示範預期行為,把需求壓縮成三條可驗收的規則:按 start 每秒累加一次;按 stop 立刻停住;再按 start 從停下的數字接著跑。示範完他追加了第四條,也是這題真正的考點——連按多次 start 不能讓數字加速。受試者打開本機的 Cursor 編輯器(順帶提到公司目前用的是 Claude Code),用 useState 存 count、用另一個 isCounting 狀態當開關,在 useEffect 裡依 isCounting 呼叫 setInterval。面試官只提醒把 CTA 做成按鈕、外觀不用管。
AI 補充從截圖可以看出第一版的結構已經埋好了後面所有的坑。程式碼是:useEffect 監看 isCounting,為真時呼叫 setInterval 每秒 setCount(prev => prev + 1);兩顆按鈕分別把 isCounting 設成 true 與 false。setCount 用函式形式是對的(呼應第 7 段提過的批次更新),但有兩個結構性問題。第一,setInterval 回傳的計時器 id 沒有被接住——它只存在於那次 effect 的 closure 裡(見第 5 段),一旦元件因為 count 改變而 re-render,就再也沒有人握著那個 id 可以清掉。第二,effect 沒有回傳清理函式,所以 isCounting 每次變動都會疊加一個新的 interval,舊的照跑。值得注意的是,這一題的正解正是他在第 7 段親口說對的那句話——「useRef 用來記住跨 re-render 仍然存在的值」,而計時器 id 正是這種值。
14. clearInterval 失效:提示改用 useRef 21:09–24:15
第一版的 stop 沒有作用。面試官指出問題在 clearInterval 拿不到正確的 interval id,並給提示:把 interval 存進 useRef 的 context 裡。答題者卡在「怎麼把值放進 ref」,以為 useRef 一定要綁在某個元素上;面試官說明可以直接把任意值存進 ref.current,停止時再從那裡拿來清掉。

推理因為 stop 按鈕沒有作用,面試官直接指出病灶:這裡清不掉 interval,因為你手上沒有正確的計時器 id。他給的提示只有一句——把 interval 放進 useRef 的 context 裡,刻意不給寫法。受試者卡住的點不是不知道 useRef 是什麼(他在第 7 段才剛正確定義過),而是他認定 useRef 一定要綁在某個 DOM 元素上,於是不斷問「怎麼把值放進 ref」。面試官最後把那層誤解拆掉:ref 不需要綁任何元素,可以直接把任意值指派給 ref.current,停止時再從那裡取出來清掉。
AI 補充截圖裡看得到當時的中間狀態:stop 按鈕寫的是 onClick={() => clearInterval(1000)}——把延遲毫秒數當成計時器 id 傳了進去。這個寫法不會報錯,因為 clearInterval 收到不存在的 id 只會靜靜地什麼都不做,這正是這類 bug 難找的原因。同時 useEffect 的 else 分支裡還留著兩行註解掉的 clearInterval(interval),可以看出他原本想用區域變數接住 id,發現作用域不對之後就註解掉了——他其實摸到了正確方向,只差把那個變數換成 ref。這段暴露的是一個很典型的落差:受試者對 useRef 的記憶是「用來取得 DOM 元素」這個最常見的用例,而不是它的本質「一個跨 render 不變的容器」。判斷什麼該放進 ref 的標準只有一條——這個值變了,畫面需不需要跟著變?計時器 id 不需要,所以它屬於 ref。
15. hook 不能巢狀,與命名的建議 24:15–26:21
答題者把 useRef 寫進 useEffect 裡而報錯。面試官反問原因並揭曉:hook 不能放在另一個 hook 裡面。接著他建議這題根本不需要 useEffect,只要在 start/stop 事件裡處理即可,而且真實面試時應該把邏輯抽成獨立函式、取 startCounter / stopCounter 這類有意義的名字,而不是用一個共用函式靠參數分流。

推理因為錯誤已經跳出來,面試官先反問受試者知不知道原因,確認他是照著提示改還是真的理解。受試者答不出來,面試官揭曉:不能在一個 hook 裡面呼叫另一個 hook。接著他做了一件更有價值的事——不只修錯,還修結構。他指出這題根本不需要 useEffect,只要在 start 與 stop 的事件處理器裡直接處理就好;並且建議真實面試時應該把邏輯抽成獨立函式,取 startCounter、stopCounter 這種有意義的名字,而不是像受試者打算的那樣寫一個共用函式、靠參數字串分流。
AI 補充面試官的說法要精確一點:Rules of Hooks 的規定是 hook 只能在元件函式或自訂 hook 的最頂層呼叫,不能寫在條件、迴圈或巢狀函式裡。useEffect 的 callback 就是一個巢狀函式,所以在裡面呼叫 useRef 違規。理由是 React 靠呼叫順序而不是名稱來對應每次 render 的 hook 狀態,順序一變就會錯位。從截圖可以看到當時的畫面:整個 useEffect 被註解掉了一大半,只剩 stopWatchRef = setInterval(...) 這行留在按鈕的 onClick 裡——注意它寫的是 stopWatchRef 而不是 stopWatchRef.current,這個遺漏會在下一段被面試官抓出來。至於「不需要 useEffect」這條建議,值得單獨記住:React 官方文件有一篇專門講「你可能不需要 effect」,判準是——這件事是回應使用者的操作而發生的嗎?是的話就寫在事件處理器裡;只有需要跟外部系統同步時才用 effect。碼表由按鈕觸發,屬於前者。
16. 連按 start 的 bug 與錯誤方向 26:21–29:25
面試官先修正寫法:值要存進 stopwatchRef.current(第 12 行)。接著請答題者連按三四次 start,暴露出計數加速的 bug。答題者依序提出 debounce、加一個 hasStarted 狀態、useCallback 記憶化,面試官逐一擋掉並要求「不要多寫程式碼,只找出這個 bug」。

推理因為結構已經修好,面試官先處理最後一個語法層面的問題:值必須存進 stopWatchRef.current,位置在第 12 行。改完之後他請受試者連按三、四次 start,數字立刻加速——第 13 段開場就宣告過的第四條要求現在被驗收了。受試者依序提出三個方向:先想到 debounce,自己隨即發現不行(間隔久一點的兩次點擊仍會各觸發一次);再提出加一個 hasStarted 狀態來擋;最後猜是要用 useCallback 記憶化。面試官逐一擋掉,並收緊題目——不要多寫任何程式碼,只要找出這個 bug。
- E受試者想到debounce時自己發現不行——就算兩次點擊隔十秒仍各觸發一次→ 存+演練
- E編輯器跳出Type 'Timeout' is not assignable to type 'null'的錯誤→ 存+演練
AI 補充面試官擋掉這三個方向的理由值得拆開看,因為它們代表三種常見的思考偏差。debounce(第 4 段定義過)是在「事件太密集」時用的,但這裡的問題不是點擊太密集,而是每一次點擊都合法地又開了一個計時器——就算兩次點擊隔十秒,一樣會有兩個 interval 在跑。hasStarted 狀態方向是對的,但它是在原本就該存在的資訊之外,額外再造一個平行的真相來源;ref 裡有沒有值本來就已經代表「有沒有在跑」了,多一個 state 就多一個要同步的東西。useCallback 則完全無關——它只影響函式的參照是否穩定,不影響按鈕被按幾次。受試者自己也馬上意識到這點。從截圖可以看到此時程式碼已經重構成一個 updateCountLogic('start' | 'stop') 的共用函式,而編輯器正跳出 TypeScript 錯誤 Type 'Timeout' is not assignable to type 'null'——這其實是個線索:ref 的型別被宣告成可以是 null,但程式從來沒有把它設回 null 過。
17. 真正的修法:清掉 ref 並在啟動前檢查 29:25–30:36
面試官給出答案:stop 時只呼叫 clearInterval,ref 裡仍留著舊值,所以要一併把 ref 設為 null(第 16 行)。改完仍不動,因為 start 還缺一個守門條件——只有在 ref.current 沒有值時才啟動 interval,加上否定判斷後功能正常。面試官宣布面試結束,答題者只要求回饋。


推理因為受試者始終在「加東西」而不是「看現有狀態」,面試官直接給出診斷:stop 只呼叫了 clearInterval,ref 裡仍留著舊的計時器 id,所以程式無從判斷現在到底在不在跑。修法是在 stop 時一併把 ref 設成 null(第 16 行)。改完仍然沒解決連按的問題,因為還缺另一半——面試官補上最後一塊:只有在 ref.current 沒有值的時候才啟動 interval。受試者先把判斷式寫反了,加上否定之後碼表終於符合全部四條要求。面試官宣布面試結束,受試者只要求回饋。
AI 補充把兩塊拼起來,這題的完整解法只有三行的形狀:start 時先檢查 if (!ref.current) 才 setInterval 並把 id 存進 ref.current;stop 時 clearInterval(ref.current) 之後把 ref.current 設回 null。ref 在這裡同時扮演兩個角色——既是計時器 id 的保管處,也是「現在有沒有在跑」這個狀態本身。這正是第 16 段裡 hasStarted 那個方向被擋掉的理由:那個資訊本來就已經存在,只是沒被清乾淨所以不可信。從兩張截圖的對照可以看出修好的過程:第一張 count 停在 39、按鈕沒反應,第二張 count 從 5 開始正常累加。這一段也回頭驗證了整場面試的推論——受試者在第 7 段就正確說出「useRef 用來記住跨 re-render 存在的值」,在第 14 段拿到了「把 interval 存進 ref」的明示提示,卻仍需要面試官逐行帶著走完。知道一個 API 的定義,和知道它在一個具體問題裡該承擔什麼角色,中間隔著的正是這場面試想量的距離。
術語:single source of truth
4. 總結
面試官用一條由外而內、再由知到行的推理鏈組織了整場面試。他先用最開放的效能題讓受試者自己攤開知識地圖(第 1 段),從受試者的回答裡抓出 tree shaking 追問,確認名詞不是背的。接著他把層級一路往下推:React 靠 reconciliation 決定要改哪些 DOM(第 2 段),DOM 改完後瀏覽器還要走完整條 critical rendering path 才會出現畫面(第 3 段)。既然每次更新都這麼貴,開發者能做的第一件事就是少觸發幾次,於是問 debounce 與 throttle(第 4 段);而 debounce 要在延遲後還記得參數,靠的是 closure(第 5 段),順勢帶出唯一不由出生位置決定的 this(第 6 段)。地基確認後回到 React,用同型的對照題問 useRef 與 useState(第 7 段)——受試者正確答出「useRef 記住跨 re-render 的值」,這句話會在二十分鐘後被逐字驗收。往回追一層 hooks 取代了什麼(第 8 段),受試者說出「只用過函式元件」,面試官記下這條線索,先讓他答完擅長的 SSR(第 9 段),再兌現成陷阱:global error boundary 至今只有 class 元件的 componentDidCatch 有原生解(第 10 段),受試者卡住。面試官給完答案就用兩題「不要一次全給」的優化題調回節奏(第 11 段),再用 WebSocket 把請求/回應模型反過來收尾(第 12 段)。觀念題全數問完之後,真正的考點才登場:一道只要三行的 stopwatch。受試者依序踩進四個坑——interval id 存在 closure 裡所以清不掉(第 13、14 段)、把 useRef 寫進 useEffect 違反 Rules of Hooks(第 15 段)、連按 start 時往 debounce 與 useCallback 找錯方向(第 16 段),最後由面試官揭曉正解:stop 時要把 ref 設回 null,start 前要檢查 ref.current 是否為空(第 17 段)。整條鏈的結論是:ref 同時是計時器 id 的保管處和「有沒有在跑」的單一真相來源,而受試者在第 7 段就已經正確定義過這個工具——資深的門檻不在定義,在於能不能把定義用在具體問題上。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 8. 生命週期方法到 useEffect 10:25 |
「I think in it's React 13, hooks was introduced」 | hooks 不是在 React 13 引入的,而是 React 16.8(2019 年 2 月正式釋出)。React 也從未有過 13 這個版本號——16 之後直接跳到 17、18、19。 依據: React 16.8 release notes, 2019-02-06 |
| 11. 一萬筆列表與 code splitting 16:32 |
「There are a bunch of libraries that we can we can use like React Virtual DOM is there」 | 沒有名為「React Virtual DOM」的虛擬列表函式庫——virtual DOM 是 React 的內部機制(見第 2 段),跟列表虛擬化是兩件事。受試者應該是把兩個名詞混在一起了。實際的套件是 react-window、react-virtualized、react-virtuoso 與 TanStack Virtual,面試官當場補正為 react-window。 依據: react-window / react-virtualized 皆為 Brian Vaughn 所作;npm 上無 react-virtual-dom 列表套件 |
| 8. 生命週期方法到 useEffect 10:40 |
「If the dependency array is empty, whatever is there in the use effect will fire when the component is unmounted」 | 依賴陣列為空時,effect 內容是在元件「掛載後」執行一次,不是卸載時;卸載時執行的是 effect 回傳的清理函式。受試者下一句就正確描述了清理函式,可見是口誤,但字面說法會誤導。 依據: React 官方文件 useEffect:Reference / Parameters |
| 9. SSR 為什麼快,又為什麼利於 SEO 12:54 |
「First input delay and it will make it better」 | 兩個問題:一是 FID(First Input Delay)已於 2024 年 3 月被 INP(Interaction to Next Paint)取代,不再是 Core Web Vitals 指標;二是 SSR 不必然改善互動延遲——HTML 提早出現但 hydration 未完成時,使用者的點擊反而可能等更久。 依據: Google web.dev, INP replaced FID as a Core Web Vital in March 2024 |
| 10. 全域錯誤邊界:卡關與正解 15:34 |
「But, in functional based component, we do not have any community we can say」 | 面試官這句(語音辨識疑為 equivalent)意思是「函式元件沒有等價寫法」。就 React 原生 API 而言正確,但實務上函式元件完全能用 error boundary——透過 react-error-boundary 套件,或 Next.js App Router 的 error.tsx;只是底層仍是 class 實作。說「函式元件無法做錯誤邊界」會誤導。 依據: react-error-boundary 套件;Next.js App Router error.tsx 慣例 |
5. 推薦三個下一步
1. 往下挖深:React 的 Rules of Hooks 與 Fiber 內部機制
第 15 段面試官只說了「hook 不能放在 hook 裡」,但沒說為什麼。理解 React 靠呼叫順序而非名稱對應 hook 狀態,才會知道這條規則不是約定俗成而是實作的必然,也才能看懂 reconciliation 在 Fiber 架構下如何被中斷與恢復。
YouTube 搜尋:React Fiber architecture explained why rules of hooks exist React internals React hooks linked list implementation
2. 往旁邊對照:React 官方的「你可能不需要 useEffect」
第 15 段面試官指出這題根本不需要 useEffect,只要寫在事件處理器裡——這是整場面試裡最有價值卻最沒展開的一句建議。把「回應使用者操作」與「跟外部系統同步」這兩類情境分清楚,能一次解掉大量 effect 相關的 bug,包含第 8 段提到的 stale closure。
YouTube 搜尋:You Might Not Need an Effect React useEffect anti patterns React derive state instead of useEffect
3. 往上應用:把同型的計時器題寫成可重用的自訂 hook
這題的正解形狀(ref 存 id + 啟動前檢查 + 停止時清空)在 debounce、throttle、輪詢、動畫迴圈裡是同一套。把它抽成 useInterval 這類自訂 hook,等於把第 4、13、14、17 段的內容一次串起來,也是前端面試常見的下一階段考題。
YouTube 搜尋:useInterval custom hook Dan Abramov React custom hooks interview questions useTimeout useDebounce implementation React
- 接著看 ←15 Frontend Concepts Every Senior Dev Has Mastered · 面試站直接拿 critical rendering path、reconciliation、virtual DOM、memoization 當共同語言使用
- 接著看 ←9 JavaScript Concepts That Got Me To Senior Dev · closure 與 lexical scope 在這站是被驗收的答案,而不是被解釋的內容
- 相關 —Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer) · 同樣是面試題:一邊是整理好的標準答案,一邊是真實臨場的卡關與修正
- 相關 —Real Senior Frontend System Design Interview 2026 (AI Coding Included) · 兩場 2026 資深前端模擬面試:一場考觀念與手寫實作,一場考系統設計
- 接著看 →Frontend System Design Explained w/ Senior Engineer (Microfrontends, Monorepo, MCP UI, Reactjs) · reconciliation、hydration、WebSocket 等單點觀念在這站被放進整個系統設計裡
- 接著看 →Lauren Tan grokbot workshop 中文字幕 · 那站講透 useEffect 為何是地雷,這站在 CI 直接把它禁掉
- 接著看 ←You Wouldn't Believe These Developer Interview Mistakes... · 這站列出該補的基本功,那場模擬面試把它們一題一題問出來
- 相關 —Frontend System Design: Performance API, Chrome, React Profiler · 共用 layout、virtualization、code splitting:一站口頭答題,一站實際量給你看
- 接著看 ←JavaScript Visualized - Closures · 懂了 [[Environment]] 與 scope chain,再去看面試題怎麼考 closure
- 接著看 ←JavaScript Visualized - Execution Contexts · closure、scope chain、hoisting 在這站是機制,在那場模擬面試裡是被驗收的答案
- 相關 —Vapor Mode is the Future of Vue · 同談 virtual DOM 與 DOM 操作成本,一個講原理一個在面試裡被問