2026 年資深前端面試:15 題背後的同一套推理方式

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 34m17s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(17)

1. Outline

  1. 起點 · Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer)
    從「為什麼答對了還是被判定不夠 senior」出發,用 15 題示範同一套動作:先重新框定問題、再建立分類架構、然後在架構裡填內容、最後把結論接回成本或風險的判準。三個尺度依序展開——AI 工作流、CSS 與 JavaScript 基礎、前端系統設計。

2. YouTuber 的思維推導

Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer)

theSeniorDev · 50m40s · 字幕 en · vision=on (使用者指定 vision=true;且這支影片是白板教學型,作者大量說「這是我們的 padding box」「我把它改成藍色」「這是 V8 引擎」,說明高度依賴他當場畫的圖。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 2m11s 7m00s
shot 1m44s 10s
analyze 21m15s 25m00s
render 5s –

作者的出發點不是「這 15 題的正確答案是什麼」,而是「為什麼答對了還是被判定不夠 senior」。他的答案是:資深不在於知道結論,而在於答案裡看得見一條可驗證的推理鏈。整支影片就是同一套動作重複 15 次——先把問題重新框定(是載入慢還是輸入卡?

是 client render 還是 SSR?),再建立一個分類架構(static/dynamic、testing pyramid、input/output token、三根支柱、A/B/C 計分),然後在架構裡填內容,最後把結論接回一個成本或風險的判準。而串起這 15 題的暗線是 AI:AI 會為了 demo 好看而硬寫像素、複製狀態、堆假的測試覆蓋率、產出巨大檔案,於是所有「老派基本功」——box model、specificity、event loop、記憶體管理、bundle 切分——都從考古題變成了「你有沒有能力接住 AI 產出的爛攤子」的檢查點。

他的結論因此是一個循環:越懂底層,越能給 AI 正確的邊界;邊界越好,AI 產出的東西越少需要你改;改得越少,token 與 GPU 成本越低。這條循環同時也是他對「前端會不會消失」的回答:不會,但留下來的位置會集中在能定義邊界的人——做 design system 的人,和懂系統設計的人。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. 開場:被拒的理由是「不夠 senior」 → 如果資深的差別在於推理鏈,那第一題就要示範這條鏈長什麼樣:從一個 AI 寫出來的小 bug 出發,看能推到多深。
  2. Q1 hover 放大:reflow 還是 compositor → 這一題的推理停在「知道底層機制才知道 AI 的寫法貴在哪」。那麼問題就變成:面對整天在產生這種寫法的 AI,你的工作流要怎麼設計,才不用每次都事後救火?
  3. Q2 資深的 AI 工作流長什麼樣 → 工作流限制的是 AI 產出的方向,但沒有人能保證它每次都照做。所以下一個問題是:怎麼用自動化的方式,持續檢查產出的品質?
  4. Q3 用 AI 時怎麼守住程式碼品質 → static 那一側的答案已經完整了,但他自己也承認靜態檢查抓不到「檔案太大、邏輯全糊在一起」。這就把問題推到 dynamic 那一側:測試要怎麼寫,才不會變成另一種假象?
  5. Q4 測試:覆蓋率、金字塔、AAA → 他在推導裡兩次用「花多少 token 才找得到 bug」當作判準,等於已經把成本當成架構決策的變數。那接下來就該正面處理:token 花費本身該怎麼被設計進前端架構?
  6. Q5 怎麼把 token 花費壓低 → 兩個手段裡,micro-frontend 是有條件的,但「標準化」是無條件划算的。那就得先回答:一個能讓人與 AI 都少寫程式碼的 design system,到底該長什麼樣?
  7. Q6 什麼樣的 design system 算好 → design system 能保證顏色與間距一致,但保證不了版面在不同螢幕上不壞掉。要判斷版面為什麼壞,得先回到最底層:瀏覽器到底是怎麼決定一個元素佔多大?
  8. Q7 CSS box model 與 box-sizing → 現在知道單一元素怎麼算大小了,但實際頁面上同一個元素常常同時被好幾條規則指到。那麼下一個問題是:瀏覽器怎麼決定哪一條說了算?
  9. Q8 CSS specificity 怎麼算出勝負 → 現在單一元素的尺寸與樣式歸屬都清楚了,但這些規則放到不同螢幕上還要能活。所以問題升級成:一個版面要具備哪些條件,才會自己適應各種寬度?
  10. Q9 響應式設計的三根支柱 → 到這裡,CSS 這一側——尺寸、優先級、版面適應——已經連成一串了。但畫面卡頓的另一半原因不在 CSS,而在 JavaScript 什麼時候被執行。所以要換到執行模型:event loop。
  11. Q10 event loop 的完整運作 → 知道了工作是怎麼排隊的,下一步自然是問:當排隊的工作太重時,實際會長成什麼樣的線上問題?
  12. event loop 在生產環境的真實 bug → 他這個案例談的是輸入反應變慢。但真實情境裡使用者說「很慢」時,也可能指的是打開很慢。那就需要一套能先分辨、再對症下藥的流程。
  13. Q11 vibe coded 的 React 很慢怎麼辦 → 到這裡談的都是「怎麼讓現有的東西變快」。但如果問題不是慢,而是使用者數量整整多了一百倍呢?那就從最佳化變成了系統設計。
  14. Q12 object 與 map 的差別 → Map 可以用物件當 key,這帶出一個他還沒處理的後果:那些被當成 key 的物件,什麼時候才會被釋放?
  15. Q13 Map、WeakMap 與垃圾回收 → 記憶體與資料結構這一層談完了,前端基礎的部分也就收束了。剩下最後兩題是尺度最大的:當使用者變成一百倍時,整個前端要怎麼撐住?
  16. Q14 從一千到十萬日活怎麼擴 → 擴展的問題到這裡是關於「怎麼把東西送到使用者面前」。最後一題把方向反過來:資料要怎麼持續從伺服器流回使用者?
  17. Q15 LLM 聊天機器人用哪種即時協定

3. 逐段說明

Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer)

1. 開場:被拒的理由是「不夠 senior」 0:00–0:47

主持人指出前端工程師現在最常收到的拒絕理由不是答錯,而是「你不夠 senior」。因為把面試題答對已經不夠,你必須在答案裡透露出資深的技術深度。整支影片要用 15 題常見前端面試題示範「資深的人會怎麼答」,涵蓋前端基礎、前端系統設計,以及 AI 輔助的前端工程。

承上 前情提要裡最重要的背景是:現在的前端面試已經不是在檢查你會不會寫,而是在檢查你的判斷力來自哪裡。這一段就從這個背景開場。

推理作者從一個統計現象出發:前端工程師收到的拒絕理由,最常見的不是「答錯」而是「不夠 senior」。他把這件事翻譯成一個可操作的目標——答案本身不是產品,答案裡透露出的技術深度才是。因此整支影片的形式不是「15 個標準答案」,而是「同一題,junior 會怎麼答、senior 會怎麼答」的對照。

AI 補充作者沒有明說的是,「不夠 senior」在面試現場其實是一個很具體的判斷:面試官在聽你有沒有能力把一個開放問題**先切開再回答**。junior 的答案是一個結論(用 CDN、寫測試、用 memo),senior 的答案是一個結構加上取捨(在什麼條件下用、代價是什麼、不用會怎樣)。這也解釋了為什麼影片要把三個看似不相干的主題放在一起:前端基礎、系統設計、AI 輔助工程。它們是同一個能力在三個尺度上的展現——你能不能說出「為什麼」。

留給下一段 如果資深的差別在於推理鏈,那第一題就要示範這條鏈長什麼樣:從一個 AI 寫出來的小 bug 出發,看能推到多深。

2. Q1 hover 放大:reflow 還是 compositor 0:47–3:30

第一題是 AI 寫的按鈕靠改 width 做 hover 放大。Bogdan 指出改 width 會觸發 reflow,瀏覽器必須重算這塊版面、重跑整條 rendering,佔住 main thread。資深的做法是改用 CSS transform scale 加 transition,讓動畫直接走 compositor thread 與 GPU,不動 layout tree。他進一步說:用 JavaScript 去改更糟,因為那還要經過 event loop 與 call stack,改了 DOM 再引發 reflow;原則是能繞過 main thread 就繞過,必要時把元素提升成獨立的圖層。

承上 承上:上一段說資深的差別在於答案裡看得見推理鏈,這一段就用一顆會放大的按鈕當最小的示範題。

推理因為要示範推理鏈,所以作者刻意選了一個看起來只有一行答案的題目:AI 寫的按鈕靠改 width 做 hover 放大。他沒有直接說「改用 transform」,而是先問「改 width 會發生什麼」——會觸發 reflow,瀏覽器要重算這塊的 layout、重跑整條 render、再產生一次像素。有了這個代價,替代方案才有理由:CSS transform 的 scale 不動 layout tree,可以交給 compositor thread 在 GPU 上算完。最後他把同一條推理再往上推一層:如果改用 JavaScript 來改,成本更高,因為多了 event loop 與 call stack 這一段,改完 DOM 還是會回到 reflow。所以整段的結論不是「用 transform」,而是「能繞過 main thread 就繞過」。

AI 補充這裡要補一個作者略過的精確度問題:**不是所有 CSS transition 都會繞過 reflow**。繞過 layout 的只有少數「可合成」的屬性,實務上主要是 `transform` 與 `opacity`。如果你寫 `transition: width 0.2s`,每一影格照樣觸發 layout 與 paint,一點都不會比 JavaScript 改 width 便宜。真正的規則是**看你動的是哪個屬性**,而不是看你用的是 transition 還是 JavaScript。另外「promote 成獨立圖層」也不是免費的:每個 compositing layer 都要吃 GPU 記憶體,濫用 `will-change` 反而會讓行動裝置變慢,通常是短暫開啟、動畫結束就拿掉。

術語:layer promotion

reflow

重排

瀏覽器重新計算元素幾何位置與尺寸的過程,會連帶影響周圍元素。

reflow 又叫 layout。任何會改變幾何的變動都會觸發它:寬高、padding、字型大小、插入或刪除節點,甚至讀取 offsetHeight 這類會強迫瀏覽器立即算出結果的屬性。它昂貴的原因是影響範圍會往外擴散——改一個元素的寬度,同層與父層可能都要重算。reflow 之後通常還要重新 paint,最後才進 composite。所以效能優化的順序是:能只做 composite 就別 paint,能只 paint 就別 layout。

相關術語: compositor thread (被繞過的對象)、layer promotion (迴避手段)

出處:第 2 段「Q1 hover 放大:reflow 還是 compositor」

compositor thread

合成執行緒

瀏覽器中專責把已繪製好的圖層組合成畫面的執行緒,可獨立於 main thread 運作。

它是捲動之所以能維持流暢的原因:即使 main thread 正忙著跑 JavaScript,compositor 仍能把既有圖層平移、縮放、調透明度並送去 GPU。這也是為什麼 transform 與 opacity 的動畫在卡頓的頁面上還是順的。代價是它只能做「移動與混合既有像素」,一旦內容本身要改變(文字換行、尺寸影響鄰居),就必須回到 main thread。

相關術語: main thread (分擔工作)、CSS transform (由它執行)

出處:第 2 段「Q1 hover 放大:reflow 還是 compositor」

main thread

主執行緒

瀏覽器分頁中同時負責跑 JavaScript、計算樣式、layout 與 paint 的那一條執行緒。

「不要阻塞 main thread」之所以是前端第一守則,是因為它被三種工作共用:你的 JavaScript、瀏覽器的排版計算、以及畫面繪製。任何一項佔太久,其他兩項就得等,使用者看到的就是點了沒反應或動畫掉格。判斷一段程式碼貴不貴,實用的問法是「它會不會佔住 main thread,佔多久」。

相關術語: compositor thread (對照組)

出處:第 2 段「Q1 hover 放大:reflow 還是 compositor」

CSS transform

CSS 變換

用矩陣運算對元素做位移、縮放、旋轉的 CSS 屬性,不改變它在版面中佔的位置。

關鍵在於「不改變它在版面中佔的空間」:`transform: scale(1.1)` 讓元素在視覺上變大,但版面仍然當它是原本的尺寸,所以周圍元素不會被推開,也就沒有 reflow。這既是它便宜的原因,也是它的限制——想要真的把鄰居推開,就得動 layout。

相關術語: reflow (不觸發)

出處:第 2 段「Q1 hover 放大:reflow 還是 compositor」

layer promotion

圖層提升

把某個元素單獨提升成一個合成圖層,讓它的變化不必重畫其他內容。

常見手段是 `will-change: transform` 或早年的 `translateZ(0)` hack。提升之後,該元素的動畫可以完全在 compositor 完成。但每個圖層都要獨立的紋理記憶體,圖層太多會爆記憶體、反而更慢,行動裝置尤其明顯。實務做法是動畫開始前才加、結束後移除。

相關術語: compositor thread (服務對象)

出處:第 2 段「Q1 hover 放大:reflow 還是 compositor」

確定錯誤/已過時 1:36
「Because CSS transitions they bypass the reflow and they go straight to what we call the compositor thread.」
繞過 reflow 的是特定屬性,不是 transition 這個機制本身。只有可合成的屬性(實務上主要是 transform 與 opacity,以及 filter)能直接交給 compositor;`transition: width` 或 `transition: top` 一樣會每格觸發 layout 與 paint。他後面舉的例子(scale)剛好是對的,但把理由歸給「transitions」會讓人誤以為只要寫成 transition 就便宜。
依據: CSS Triggers / Chrome 渲染管線文件:layout → paint → composite,僅 transform、opacity 等屬性可跳到 composite
留給下一段 這一題的推理停在「知道底層機制才知道 AI 的寫法貴在哪」。那麼問題就變成:面對整天在產生這種寫法的 AI,你的工作流要怎麼設計,才不用每次都事後救火?

3. Q2 資深的 AI 工作流長什麼樣 3:30–7:38

第二題:除了「開 Claude Code」之外,你的 AI 前端工作流是什麼。Bogdan 說 junior 的答案就是「我用 Claude Code」,資深的答案要有結構:一、先用 plan mode,並在規劃時接上你的 design system 與 Figma MCP,讓 agent 用既有積木規劃;二、加上結構性約束,特別是 state 要遵守 single record principle、CSS 用相對單位,因為模型為了 demo 好看會過度貼合 prompt,重複 state、硬寫顏色與像素;三、programming to interfaces,盯緊 component 的 props 與 state 形狀。他還補上一條 code review 原則:全域設定、測試、linter、type checker、package.json 這類「爆炸半徑大」的改動才需要人工細看。

他列出的三條結構性約束(single record principle、相對單位、programming to interfaces)
5:02 · 他列出的三條結構性約束(single record principle、相對單位、programming to interfaces)
承上 承上:上一段的結論是「懂底層才知道 AI 的寫法貴在哪」,這一段就把視角從單一 bug 拉到工作流——與其事後修,不如一開始就限制 AI 的產出空間。

推理因為問題已經變成「怎麼從源頭限制」,所以作者把答案設計成三層遞進的約束,一層比一層抽象。第一層是**素材**:先用 plan mode 規劃,並讓 agent 透過 Figma MCP 與既有 design system 取材,讓它用你的積木而不是自己發明。第二層是**結構**:state 遵守 single record principle、CSS 用相對單位。他解釋了為什麼需要這一層——模型被最佳化成「一個 prompt 就給出漂亮 demo」,代價是硬寫顏色與像素、重複狀態。第三層是**邊界**:programming to interfaces,盯緊 component 的 props 與 state 形狀,因為邊界一旦定好,AI 執行得其實不錯。最後他補上人力該花在哪:只有全域設定、測試、linter、type checker、package.json 這種爆炸半徑大的改動才值得逐行看。

AI 補充作者把 single record principle 講得像是一條 AI 專屬規則,其實它就是資料庫正規化裡「同一份事實只存一次」搬到前端 state。重要的是它為什麼在 AI 時代特別容易被違反:模型是一段一段地產生程式碼,每個元件在局部看起來都合理,於是同一筆資料被複製到三個地方,各自有各自的更新路徑——bug 不會在寫的時候出現,而是在其中一份忘了更新時出現。第三層的「programming to interfaces」在前端的具體形式,多數人會漏掉:它不是叫你寫一堆 TypeScript interface,而是**先決定 props 的形狀再讓 AI 填實作**。props 形狀決定了狀態放哪裡、誰負責更新、能不能重用,一旦形狀錯了,裡面的程式碼寫得再漂亮也救不回來。至於「只細看爆炸半徑大的檔案」這條,可以直接落成機制:把這些路徑列進 CODEOWNERS 或 review checklist,不要依賴自己每次記得。

plan mode

規劃模式

讓 coding agent 先產出實作計畫、經人確認後才動手寫程式的工作模式。

它的價值不在於「多一個步驟」,而在於把最貴的錯誤提前。計畫階段的錯誤改一句話就好,實作階段的同一個錯誤要改十個檔案。在 Claude Code 裡是 plan mode,其他工具有各自的說法,但共同點是:這一步的產物是人可以快速讀完並反駁的文字,而不是幾百行 diff。

相關術語: MCP (常搭配使用)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

MCP

模型上下文協定

Model Context Protocol,讓 LLM 以標準介面連上外部資料源與工具的協定。

作者用的是 Figma MCP:agent 不再靠你貼截圖猜設計,而是直接讀到設計檔裡的實際數值——間距、色票、元件名稱。這改變的不只是準確度,而是「有沒有一個可以最佳化的標的」。沒有 MCP 時,模型只能對著你的文字描述最佳化;接上之後,它有一份客觀的來源可以對齊。

相關術語: design system (作為資料來源)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

single record principle

單一紀錄原則

同一筆資料在整個狀態架構裡只有一份權威來源,其他地方只能引用它。

在前端的具體樣子是:使用者資料只放在一處,需要它的元件用 id 去取,而不是各自複製一份物件。違反它的後果通常延遲出現——畫面 A 更新了、畫面 B 沒有,或是兩份資料開始緩慢地不一致。它跟資料庫的正規化是同一件事,只是搬到 client 端;也是為什麼多數 state 管理工具都鼓勵用扁平的 normalized 結構存資料。

相關術語: programming to interfaces (同屬結構約束)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

programming to interfaces

面向介面編程

先定義模組之間的契約,再各自實作,讓實作可以替換而不影響呼叫端。

在前端最實際的形式是 props 與型別的形狀。先決定「這個元件收什麼、吐什麼」,實作細節就變成可替換的內部事務。這條原則在 AI 協作下特別關鍵:模型很擅長在給定邊界內填程式碼,但很不擅長自己決定邊界——它會傾向把邊界弄得越寬越好,因為那樣一次就能生出可運作的東西。

相關術語: single record principle (同屬結構約束)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

design system

設計系統

一套共用的設計決策與可重用元件,讓不同人做出來的介面保持一致。

它不是元件庫的同義詞。元件庫只是它的一部分,前面還有設計決策(間距級距、字級、顏色語意),後面還有使用規範與工具。在這一段它扮演的角色是「AI 的取材來源」:有了它,模型不需要發明按鈕長什麼樣,只需要選用哪一顆。

相關術語: MCP (透過它被讀取)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

blast radius

爆炸半徑

一處改動出錯時,可能波及的範圍大小。

它是決定「哪裡該人工細看」的判準,比「改了幾行」有用得多。一個元件寫得普通,影響僅止於那個畫面;但 package.json 加一個相依、tsconfig 放寬一個選項、linter 關掉一條規則,影響的是整個專案往後每一次改動。這也是這段給出的具體 code review 策略:從 config 與相依開始看,而不是從 diff 最長的檔案開始看。

相關術語: micro-frontend (用來縮小它)

出處:第 3 段「Q2 資深的 AI 工作流長什麼樣」

留給下一段 工作流限制的是 AI 產出的方向,但沒有人能保證它每次都照做。所以下一個問題是:怎麼用自動化的方式,持續檢查產出的品質?

4. Q3 用 AI 時怎麼守住程式碼品質 7:38–10:55

第三題問在 AI 產碼的情況下怎麼維持品質。Bogdan 先示範資深答題法:先建立分類再展開,不要劈頭就說「我們有 linter」。他把品質措施分成 static(不執行程式)與 dynamic(執行程式)兩類。dynamic 就是 unit / integration / end-to-end 測試;static 因為便宜又確定,能加就多加:先 TypeScript,再 ESLint 或 Oxlint,最後是超出 lint 的靜態分析——cyclomatic complexity 與 code duplication 分析,因為 AI 只做局部最佳化,很會把同一段邏輯複製到很多地方。他也提醒看檔案長度:AI 常產出把邏輯與子元件全塞在一起的巨大檔案,靜態檢查會過,但一要寫 unit test 就得先拆。

static 與 dynamic 兩大分類的白板結構,是這題答題架構的骨架
8:15 · static 與 dynamic 兩大分類的白板結構,是這題答題架構的骨架
static 那一側依序展開:TypeScript → linter → 複雜度與重複度分析
9:40 · static 那一側依序展開:TypeScript → linter → 複雜度與重複度分析
承上 承上:上一段留下的問題是「怎麼持續檢查 AI 的產出」,這一段給出的答案是先把檢查手段分類,再決定投資順序。

推理因為問題是「持續檢查」,所以作者先示範他心目中資深答題的第一動作:不要一開口就報工具名,先建立分類。他把品質措施切成 static(不執行程式就能檢查)與 dynamic(要跑起來才知道)兩類。分完之後,投資順序就自己浮現了:static 便宜、快、結果確定,能加就多加。他因此給出遞進的三層——先 TypeScript 建立型別邊界,再 linter(ESLint 或更快的 Oxlint),最後是超出 lint 的靜態分析:cyclomatic complexity 與 code duplication。第三層之所以是新需求,理由直接接回上一段:AI 只做局部最佳化,會把同一段邏輯複製到很多地方。最後他點出這套檢查的盲點——檔案再大再肥,靜態檢查一樣會過,所以還要看檔案長度,而「能不能寫 unit test」正好是逼你拆檔案的力量。

AI 補充這裡值得補上作者沒明說的成本結構:static 檢查之所以該優先,是因為它的成本是**固定的**(跑一次 CI 幾十秒),而 dynamic 檢查的成本會隨程式碼量線性成長(測試要寫、要維護、跑得越來越久)。在 AI 每天產出數百行的情況下,任何隨程式碼量成長的成本都會失控,所以先把能固定成本的關卡設滿,是理性的順序。另外「code duplication 分析」在實務上要小心設定:AI 產生的重複常常是**結構相同但字面不同**(變數名不一樣、順序調過),純字串比對的工具會漏掉,要選有 token 級或 AST 級比對的工具才抓得到。他提到的 NPM 工具名稱在自動字幕裡是「follow」,這多半是聽寫錯誤——這個領域比較通行的是 jscpd(重複度)與 ESLint 的 complexity 規則。

術語:code duplication analysis

static analysis

靜態分析

不執行程式,只靠讀原始碼結構就找出問題的檢查方式。

它的三個優點都跟 AI 協作有關:確定(同樣的輸入永遠同樣的結果)、快(秒級)、以及成本不隨程式碼量爆炸。型別檢查、lint、複雜度與重複度分析都屬於這一類。它的天花板也很明確——它只看得到結構,看不到行為,所以「型別全過、lint 全綠」跟「功能正確」是兩件事。

相關術語: cyclomatic complexity (一種)、code duplication analysis (一種)

出處:第 4 段「Q3 用 AI 時怎麼守住程式碼品質」

cyclomatic complexity

循環複雜度

用程式中獨立執行路徑的數量來量化一段邏輯有多難理解與測試。

直覺算法是「分支數加一」:每個 if、每個 &&、每個 case 都增加一條路徑。它有用的地方不是那個絕對數字,而是它同時預測了兩件事——這個函式要寫幾個測試才蓋得完,以及人讀它時要在腦中同時保留幾種情況。函式複雜度超過十左右通常就該拆,因為那表示它做了不只一件事。

相關術語: static analysis (屬於)

出處:第 4 段「Q3 用 AI 時怎麼守住程式碼品質」

code duplication analysis

重複程式碼分析

掃描整個專案、找出功能或字面重複的區塊並量化比例的工具。

它在 AI 時代的重要性被大幅放大:模型一次只看得到局部脈絡,很容易在第三個地方重寫一次已經存在的函式。而重複的代價是延後的——改需求時你只改到其中兩處。要注意工具的比對層級:純文字比對會漏掉「改了變數名的複製」,token 或 AST 層級的比對才抓得到。

相關術語: static analysis (屬於)

出處:第 4 段「Q3 用 AI 時怎麼守住程式碼品質」

Oxlint

Oxlint

用 Rust 寫的 JavaScript/TypeScript linter,主打比 ESLint 快上數十倍。

它出現的理由跟這一段的論證一致:如果靜態檢查要在每次存檔、每次 commit 都跑,速度本身就是一種品質。它與 ESLint 的取捨是規則覆蓋度與生態系外掛,實務上常見的做法是用 Oxlint 跑高頻的快速檢查,保留 ESLint 跑專案特有的自訂規則。

相關術語: static analysis (屬於)

出處:第 4 段「Q3 用 AI 時怎麼守住程式碼品質」

留給下一段 static 那一側的答案已經完整了,但他自己也承認靜態檢查抓不到「檔案太大、邏輯全糊在一起」。這就把問題推到 dynamic 那一側:測試要怎麼寫,才不會變成另一種假象?

5. Q4 測試:覆蓋率、金字塔、AAA 10:55–13:50

第四題問在 AI 產出大量程式碼的前提下怎麼測試。除了說出三種測試,資深要再深一層:一、看 test coverage,目標 65% 以上但別衝過 85%,因為 AI 什麼都能寫測試,過高會變成沒驗證過的假覆蓋率;二、畫出 testing pyramid 並定位自己在哪裡,反駁「有 end-to-end 就好」的謬誤——e2e 掛掉只知道壞了、不知道壞在哪,除錯要燒很多時間與 token,而 unit test 能一段一段排除嫌疑;三、用 AAA(arrange / act / assert)統一測試結構,也讓 AI 照這個格式寫,人才讀得下去。

testing pyramid 與「只寫 e2e」那個反例的對照圖
11:55 · testing pyramid 與「只寫 e2e」那個反例的對照圖
AAA pattern 的三個階段
13:25 · AAA pattern 的三個階段
承上 承上:上一段把品質分成 static 與 dynamic 兩側,static 已經談完,這一段處理 dynamic 那一側,也就是測試。

推理因為「寫測試」這個答案太淺,作者刻意再往下鑽三層。第一層是**量**:test coverage 該落在 65% 以上、但不要衝過 85%。他給的理由才是重點——AI 幾乎什麼都能寫測試,過高的數字往往是沒人驗證過的假覆蓋率。第二層是**形狀**:畫出 testing pyramid,並針對「有 e2e 就夠了」的流行說法給出反駁。他的反駁不是訴諸原則,而是算除錯成本:e2e 掛掉只告訴你系統壞了,不告訴你壞在哪,於是你得用時間或 token 去搜;有 unit test 時可以一段一段排除嫌疑,快也便宜。第三層是**結構**:用 AAA(arrange / act / assert)統一測試的骨架,並且直接把這個格式交代給 AI,讓產出的測試人讀得下去。

AI 補充作者對覆蓋率上限的說法值得再精確一點:85% 這個數字不是普世常數,重點在於**覆蓋率量的是「這行有被執行到」,不是「這個行為有被驗證」**。AI 很容易寫出跑過所有分支卻沒有任何有意義斷言的測試,覆蓋率報表照樣漂亮。所以真正該搭配看的是斷言的品質,或用 mutation testing(故意改壞程式碼,看有沒有測試因此失敗)來驗證測試本身有沒有效。至於 testing pyramid 的取捨,他推導出的其實是一條可以量化的判準:**測試的價值 ≈ 訊號的定位精度 ÷ 維護成本**。unit test 定位精度高、維護成本低但涵蓋範圍窄;e2e 反過來。「只寫 e2e」在 AI 時代誘人的原因也很實際——e2e 描述的是使用者行為,最好交給 AI 生成;但它同時也是最貴的除錯入口,這正是他要擋下來的地方。

術語:AAA patterntest double

test coverage

測試覆蓋率

被測試執行到的程式碼比例,常以行、分支或函式為單位計算。

它最大的誤用是被當成品質指標。它其實只是「風險地圖」:低覆蓋率的區域確定沒被測到,但高覆蓋率不保證測得對——一個沒有任何 assert 的測試也能把覆蓋率拉滿。實務上比較有意義的用法是看趨勢與分佈:核心邏輯是不是比周邊高,新增的程式碼有沒有拉低整體。

相關術語: testing pyramid (常一起評估)

出處:第 5 段「Q4 測試:覆蓋率、金字塔、AAA」

testing pyramid

測試金字塔

描述測試比例的模型:底層大量 unit test,中層 integration,頂層少量 end-to-end。

它的形狀不是美學偏好,而是成本推論的結果:越往上,一個測試涵蓋的範圍越大、但執行越慢、越不穩定、失敗時越難定位。前端有個長年爭議是中間層的邊界很模糊——渲染一個元件並模擬點擊,算 unit 還是 integration?多數團隊的務實做法是不糾結命名,只問「這個測試壞掉時,我要花多久才知道哪裡壞了」。

相關術語: test coverage (常一起評估)、AAA pattern (內部結構)

出處:第 5 段「Q4 測試:覆蓋率、金字塔、AAA」

AAA pattern

三段式測試結構

把每個測試寫成 arrange(準備)、act(執行)、assert(斷言)三段的固定骨架。

它的價值在於讓「這個測試在測什麼」變成掃一眼就知道的事:中間那一段永遠只有一個動作,斷言永遠在最後。當測試是 AI 寫的、而且數量很多時,這種可預測的形狀直接決定了人願不願意讀它。一個延伸的判斷法是:如果你的 act 需要好幾行,通常表示被測的介面設計得不好。

相關術語: test double (用於 arrange)

出處:第 5 段「Q4 測試:覆蓋率、金字塔、AAA」

test double

測試替身

在測試中頂替真實相依的假物件,涵蓋 mock、stub、fake、spy 等。

它們的差別在於「假到什麼程度」:stub 只回固定值,spy 額外記錄呼叫過程,mock 連預期的呼叫方式都先講好,fake 則是一個能跑但簡化的真實實作。前端最常見的替身是 API 回應與計時器。用得太少測試會慢又不穩,用得太多則會變成在測試你的假設而不是程式——這也是「假覆蓋率」的另一個來源。

相關術語: AAA pattern (用於 arrange)

出處:第 5 段「Q4 測試:覆蓋率、金字塔、AAA」

留給下一段 他在推導裡兩次用「花多少 token 才找得到 bug」當作判準,等於已經把成本當成架構決策的變數。那接下來就該正面處理:token 花費本身該怎麼被設計進前端架構?

6. Q5 怎麼把 token 花費壓低 13:50–16:52

第五題問怎麼降低前端的 token 花費。Bogdan 先把成本拆成 input 與 output,input 便宜得多,所以真正的槓桿是「做一個功能需要改動的程式碼量」。這正好回到傳統軟體工程的老目標:用最少的改動交付功能,也就是模組化與解耦。具體兩招:一、標準化——用 atomic CSS、design tokens、可重用元件,新功能幾乎不用寫 CSS;二、架構——人多的時候把 monolith 拆成 micro-frontends,餵給模型的只有那一塊。他還預言計價方式會從 token 轉成 GPU 運算時間,但結論不變:一樣是回到解耦、hexagonal architecture、SOLID。

monolith 與 micro-frontends 的切分示意
15:40 · monolith 與 micro-frontends 的切分示意
承上 承上:上一段用「找一個 bug 要燒多少 token」當判準,這一段把 token 本身抬上檯面,當成架構決策的一級變數。

推理因為成本已經是判準,作者先把它拆開:輸入與輸出是兩種價格,input 明顯便宜。拆完之後結論就很清楚——真正的槓桿不在讓模型少講話,而在**做一個功能需要改動的程式碼量**。這一步是整段的樞紐,因為它把新問題翻譯成了老問題:用最少的改動交付功能,正是傳統軟體工程一直在追求的模組化與解耦。於是他的兩個具體手段都不新:一是標準化,用 atomic CSS、design tokens 與可重用元件,讓新功能幾乎不需要寫新的樣式;二是架構,人多的時候拆成 micro-frontend,讓餵進去與改動的都只是其中一塊。最後他做了一個預測來檢驗自己的結論:計價方式遲早會從 token 換成 GPU 時間;而換了之後結論不變——依然是回到解耦、hexagonal architecture、SOLID。這個「換了單位還成立」的檢驗,正是他想示範的資深思考。

AI 補充作者說 input 比 output 便宜是對的,但少了一個對前端架構更有解釋力的因素:**prompt caching**。重複送出的前綴(專案規則、型別定義、常用檔案)可以被快取,命中時價格再降一個級距。這讓「穩定的東西放前面、變動的東西放後面」變成一個真的架構考量,也讓「檔案結構穩定」本身有了金錢價值。另外 micro-frontend 這個建議必須連著代價一起講,否則在面試裡反而扣分:它會帶來重複的執行時期相依、跨團隊的版本協調、以及整體 bundle 變大。他自己在後面談擴展性時也會鬆口說單體前端撐得住——所以正確的判準不是流量,而是**同時改同一份程式碼的人數**。

atomic CSS

原子化 CSS

用大量單一用途的小 class 組合出樣式,而不是為每個元件寫一段新 CSS。

Tailwind 是最流行的實作。它跟這一段的論證直接相關:當樣式是從既有的字彙裡選出來的,新功能就不需要新增 CSS 檔案,改動量與需要讀進模型的上下文都變小。代價是 HTML 變得冗長,而且它只有在**字彙受限**時才划算——一旦允許任意值(像 `w-[327px]`),它就退化回硬寫像素。

相關術語: design tokens (取值來源)

出處:第 6 段「Q5 怎麼把 token 花費壓低」

micro-frontend

微前端

把單一前端應用拆成可獨立開發、部署的多個子應用,在執行時組合起來。

它解決的是組織問題而不是技術問題:多個團隊改同一份程式碼時的協調成本與互相拖累。代價很實在——共用相依可能被重複載入、跨子應用的狀態與路由要另外設計、整體體積通常變大。判準是團隊數量與部署節奏,而不是使用者數量。

相關術語: blast radius (用來縮小它)

出處:第 6 段「Q5 怎麼把 token 花費壓低」

hexagonal architecture

六邊形架構

把核心邏輯與外部世界用明確的 port 與 adapter 隔開的架構風格,又稱 ports and adapters。

核心概念是:業務邏輯不知道資料從 HTTP、localStorage 還是測試假資料來,只認得介面。前端的對應是把資料抓取與框架相依推到邊緣,讓核心邏輯是純函式。它跟這一段的關聯是改動量——換掉一個外部服務時,只有 adapter 要改,核心不動,需要讀進模型與需要重測的範圍都小。

相關術語: programming to interfaces (同一原則的架構版)

出處:第 6 段「Q5 怎麼把 token 花費壓低」

留給下一段 兩個手段裡,micro-frontend 是有條件的,但「標準化」是無條件划算的。那就得先回答:一個能讓人與 AI 都少寫程式碼的 design system,到底該長什麼樣?

7. Q6 什麼樣的 design system 算好 16:52–20:12

第六題問好的 design system 長什麼樣,Bogdan 給出四個步驟:一、定義 design tokens,在現代 CSS 用 custom properties 存在 root,換品牌色只要改一行;二、建可重用元件,想要開箱即用的無障礙就用 Radix 這類 headless 元件再套自己的 token,不要重造輪子;三、套 composition patterns,把 button、input 組成可直接用的 form,讓人與 LLM 可以自助地選擇要用 token、用元件、還是用組合好的整塊;四、工具,Storybook 部署出來讓大家看得到,再加一個 design-to-code MCP 讓 Figma 成為 agent 的最佳化標的。他順帶回應「前端要消失了」的說法:留在前端的兩條路是走全端或做 design system。

四個步驟與 composition pattern 的自助式分層
18:50 · 四個步驟與 composition pattern 的自助式分層
承上 承上:上一段的結論是標準化能無條件降低改動量,這一段就把「標準化」展開成一個可以檢查的四步結構。

推理作者把 design system 拆成由抽象到具體的四層,而且每一層都給出「換需求時要改幾個地方」的驗證。第一層 design tokens:用 CSS custom properties 存在 root,換品牌色是一行的事。第二層可重用元件:想要開箱即用的無障礙就用 Radix 這類 headless 元件再套自己的 token,因為 dropdown 這種東西自己做無障礙的成本高得不合理。第三層 composition patterns:把 button 與 input 組成 form,讓使用者可以自助地選擇要用 token、用元件、還是用組合好的整塊。第四層工具:Storybook 讓元件看得到、摸得到,再加一個 design-to-code MCP,讓 Figma 成為 agent 的最佳化標的——這正好接回第二段提過的 MCP。最後他用這個結構回應「前端會不會消失」:留下來的兩條路是往全端走,或往定義系統的人走。

AI 補充這四層真正的意義是**自助的層級**,作者講到了但沒點破:使用者可以按需求選擇進入的高度,用 token、用元件、或用組合好的區塊,三種都是合法用法。這正是它對 AI 特別有效的原因——模型最缺的就是「該從哪一層開始」的判斷,而一個分層清楚的系統把這個判斷變成了選擇題。另外有一個他略過的落差:design tokens 存在 Figma 與存在 CSS 是兩份資料,只要是人工同步,遲早會漂移。所以第四層的重點與其說是「接上 MCP」,不如說是**讓其中一邊成為唯一來源**,用工具把另一邊生成出來(例如用 Style Dictionary 之類的工具把 token 產成 CSS 變數)。沒有這一步,設計與程式碼的一致性仍然靠人的自律。

術語:headless components

design tokens

設計代幣

把顏色、間距、字級等設計決策抽成具名變數,讓所有介面引用同一份來源。

重點在於命名的層級。原始值(blue-500)之上通常要有語意層(color-brand、color-danger),元件才引用語意層。這樣換品牌時改的是語意層的對應,而不是滿專案搜尋色碼。它也是判斷 AI 產出品質最快的方法之一:看它寫的是 token 名稱還是十六進位色碼。

相關術語: CSS custom properties (常見載體)、atomic CSS (取值來源)

出處:第 7 段「Q6 什麼樣的 design system 算好」

CSS custom properties

CSS 自訂屬性

以 `--name: value` 宣告、用 `var(--name)` 引用的原生 CSS 變數。

它跟預處理器變數的關鍵差別是**執行時期可變**:它會跟著 CSS 的繼承與層疊走,所以可以在某個容器內覆寫而只影響那一塊,這正是主題切換與多品牌能靠它實現的原因。也因此它可以被 JavaScript 直接讀寫,不需要重新編譯。

相關術語: design tokens (承載)

出處:第 7 段「Q6 什麼樣的 design system 算好」

headless components

無樣式元件

只提供行為、狀態與無障礙處理,完全不帶視覺樣式的元件庫。

Radix、Headless UI、Ark 都屬於這一類。它們解決的是一個很不對稱的成本問題:dropdown、dialog、combobox 的樣式你半小時就能寫好,但鍵盤操作、焦點鎖定、ARIA 屬性、螢幕閱讀器行為要正確做完是好幾天的事,而且很難自己驗證。把這一半外包、樣式自己來,是目前最划算的分工。

相關術語: composition patterns (被組合)

出處:第 7 段「Q6 什麼樣的 design system 算好」

composition patterns

組合模式

把小元件組裝成更大、貼近實際用途的元件的做法。

它的反面是把所有變化都塞進 props,最後生出一個有二十個布林開關的元件。組合的好處是新的使用情境不需要改動既有元件,只要換一種組法;React 裡最常見的具體手法是用 children 與 slot 把結構的決定權交還給使用端。

相關術語: headless components (組合素材)

出處:第 7 段「Q6 什麼樣的 design system 算好」

Storybook

Storybook

把元件的各種狀態獨立渲染成可互動頁面的開發與展示工具。

它的作用遠不只展示:每個 story 其實是一個被命名的元件狀態,可以直接拿來做視覺回歸測試與互動測試。對這一段的論證來說,它是讓 design system 成為「大家看得到的共同資產」的那一步——沒有它,元件庫只存在於原始碼裡,實際上等於不存在。

相關術語: design system (展示載體)

出處:第 7 段「Q6 什麼樣的 design system 算好」

留給下一段 design system 能保證顏色與間距一致,但保證不了版面在不同螢幕上不壞掉。要判斷版面為什麼壞,得先回到最底層:瀏覽器到底是怎麼決定一個元素佔多大?

8. Q7 CSS box model 與 box-sizing 20:12–24:40

第七題回到基本功:CSS box model。頁面就是一堆巢狀的盒子,每個元素由外而內是 margin box、border box、padding box、content box,outline 與 box shadow 畫在 border 之後、margin 之前。主持人補充為什麼 2026 年還要問這題:懂 box model 才修得動 AI 產生的版面 bug,而且很多十年經驗的人根本說不清楚,所以反而好突出。接著是這題的真正陷阱:box-sizing。預設是 content-box(extrinsic width 指的是內容的寬),border-box 才是把 padding 與 border 算進去。AI 很愛硬寫 width 300px,於是桌機好看、手機爆掉,再用一堆 breakpoint 補洞。最後他點名 margin collapsing 是進階題的常客。

四層盒子由外到內標好的完整圖,這題的核心畫面
21:03 · 四層盒子由外到內標好的完整圖,這題的核心畫面
content-box 與 border-box 在同一個 width 下的差別
22:35 · content-box 與 border-box 在同一個 width 下的差別
承上 承上:上一段最後留下「版面為什麼會在不同螢幕上壞掉」,要回答它就得先講清楚瀏覽器怎麼決定一個元素佔多大——也就是 box model。

推理作者先建立整體圖像:頁面就是一堆巢狀的盒子,每個元素由外而內是 margin box、border box、padding box、content box,outline 與 box shadow 畫在 border 之後、margin 之前。接著主持人插進一個關鍵的追問——2026 年了為什麼還考這個?答案有兩層:懂 box model 才修得動 AI 產生的版面 bug,而且很多十年經驗的人講不清楚,所以這題反而是拉開差距的地方。有了這個動機,作者才把重點推到真正的陷阱:box-sizing。預設的 content-box 表示你指定的 width 只算內容,padding 與 border 是往外加的;border-box 才是把它們算進去。他接著把這個機制連回 AI:模型很愛硬寫 `width: 300px`,於是桌機好看、手機爆掉,再用一堆 breakpoint 補洞——這正是「用 AI 前面 80% 很快、後面 20% 卡死」的典型現場。最後他點名 margin collapsing 是進階題的常客。

AI 補充自動字幕在這裡把 extrinsic 與 intrinsic 講反了,值得把正確的版本說清楚,因為這是這題最容易被問倒的地方。**extrinsic sizing** 是「由外部指定尺寸」,也就是你寫 `width: 300px` 的情況;**intrinsic sizing** 是「由內容決定尺寸」,例如 `width: max-content` 或根本不寫 width 的 block 元素。瀏覽器的預設不是 extrinsic,而是「寬度撐滿可用空間、高度由內容決定」。而 box-sizing 只在你指定了尺寸時才有意義:它決定那個數字算到哪一層為止。實務上幾乎所有專案都會寫 `*, *::before, *::after { box-sizing: border-box }`,因為 border-box 才符合人的直覺——說 300 就是佔 300。至於他推薦去查的 margin collapsing:相鄰的上下 margin 會合併成較大的那一個,而不是相加;它是早期為了排版文章段落而設計的例外,也是「明明加了 margin 卻沒有變化」這類 bug 的常見來源,而且只發生在垂直方向、且父子之間沒有 padding、border 或形成 BFC 時。

CSS box model

CSS 盒模型

描述每個元素由 content、padding、border、margin 四層盒子組成的模型。

四層由內而外是 content box、padding box、border box、margin box。背景色會畫到 border box 為止,padding 有背景而 margin 沒有——這解釋了「為什麼加 padding 色塊變大、加 margin 沒變」。outline 與 box-shadow 則畫在 border 之外但完全不佔空間,所以用 outline 做 focus 樣式不會推動版面,這也是它比 border 適合當焦點指示的原因。

相關術語: box-sizing (決定計算方式)、margin collapsing (它的例外)

出處:第 8 段「Q7 CSS box model 與 box-sizing」

box-sizing

盒子計算方式

決定 width 與 height 這兩個數字算到哪一層盒子的 CSS 屬性。

預設值 content-box 表示你寫的 width 只涵蓋內容,padding 與 border 額外往外長,所以 `width: 300px; padding: 20px` 實際佔 340px。border-box 則把 padding 與 border 算進那 300px 之內。幾乎所有現代專案都會全域設成 border-box,因為它讓「這塊佔多寬」變成看得出來的事,也讓百分比寬度與 padding 可以同時使用而不溢出。

相關術語: CSS box model (作用於)、extrinsic sizing (只在此時生效)

出處:第 8 段「Q7 CSS box model 與 box-sizing」

extrinsic sizing

外部指定尺寸

尺寸由外部明確給定(例如 width: 300px),而不是由內容決定。

它的對照是 intrinsic sizing——尺寸由內容決定,例如 `max-content`、`fit-content` 或不指定寬度的情況。響應式設計出問題時,最常見的根因就是把本來該由內容或容器決定的尺寸寫死成外部指定值。一個實用的自我檢查是:這個數字如果換成手機寬度還成立嗎?

相關術語: box-sizing (與之相關)

出處:第 8 段「Q7 CSS box model 與 box-sizing」

margin collapsing

外距合併

垂直方向相鄰的 margin 會合併成其中較大的一個,而不是相加。

它只發生在垂直方向,且只在一般文件流中。父子之間也會合併——父層沒有 padding、border 或未形成 BFC 時,子元素的上 margin 會「漏」到父層外面,造成「明明是子元素的 margin,卻推動了父層」的困惑。flex 與 grid 容器的子項不會發生 margin collapsing,這也是現代版面較少踩到它的原因。

相關術語: CSS box model (它的例外)

出處:第 8 段「Q7 CSS box model 與 box-sizing」

確定錯誤/已過時 23:02
「However, the default of the browser is extrinsic width.」
說反了。瀏覽器的預設是 box-sizing: content-box,而尺寸來源的預設是 intrinsic(由內容或可用空間決定),不是 extrinsic。extrinsic 指的正是你自己寫死 width 的那種情況。他後面描述的行為(瀏覽器先算內容寬、再把 padding 加上去)其實就是 content-box 的行為,講法與用詞在這裡對不上。
依據: CSS Box Sizing Level 3:初始值 box-sizing: content-box;intrinsic sizing 指 max-content / fit-content 等由內容決定的尺寸
留給下一段 現在知道單一元素怎麼算大小了,但實際頁面上同一個元素常常同時被好幾條規則指到。那麼下一個問題是:瀏覽器怎麼決定哪一條說了算?

9. Q8 CSS specificity 怎麼算出勝負 24:40–28:22

第八題問 CSS specificity。Bogdan 用一個具體例子:一邊是 form.big-form 設白底,一邊是 #my-form 設藍底,同一個元素同時符合。瀏覽器收集所有規則後跑 cascade algorithm,在同一 layer 內比 specificity,用 A、B、C 三層計分:ID 加 A、class 或屬性加 B、元素型別加 C。所以 form.big-form 是 0-1-1,#my-form 是 1-0-0。關鍵是這不是加總而是逐位比大小,先看 A,A 分出勝負就不看後面,所以 ID 那條贏,背景是藍的。他最後說可以在 Chrome DevTools 的 computed styles 看到被劃掉的那條,hover 上去還會顯示它的 specificity。

兩條選擇器與那段 HTML markup 並排,這題的題目本體
25:45 · 兩條選擇器與那段 HTML markup 並排,這題的題目本體
A / B / C 三欄的計分結果與比較方式
27:05 · A / B / C 三欄的計分結果與比較方式
承上 承上:上一段留下的問題是「同一個元素被多條規則指到時誰說了算」,這一段就把那套計分方式攤開。

推理作者沒有直接背規則,而是先造一個最小的衝突情境:一邊是 `form .big-form` 設白底,一邊是 `#my-form` 設藍底,而 HTML 上的那個 form 同時符合兩者。有了具體衝突,機制才有登場的理由:瀏覽器收齊所有規則後跑 cascade algorithm,在同一 layer 內用 specificity 決勝,計分分成 A、B、C 三欄——ID 加 A,class 或屬性加 B,元素型別加 C。於是 `form .big-form` 是 0-1-1,`#my-form` 是 1-0-0。他接著強調最關鍵、也最常被答錯的一點:這**不是加總**,而是從左往右逐位比大小,前一位分出勝負就不看後面。所以 ID 那條贏,背景是藍的。最後他把答案落回可驗證的地方——在 Chrome DevTools 的 computed styles 看得到被劃掉的規則,hover 上去還會顯示它的 specificity。

AI 補充作者刻意跳過了 cascade 的其他步驟,但面試裡常常追問,值得補上完整的優先順序:**origin 與 importance → cascade layers → specificity → 出現順序**。specificity 只是其中一關,而且是相對靠後的一關。這解釋了兩個常見困惑:為什麼 `!important` 可以贏過任何選擇器(它在更前面的關卡就分勝負了),以及為什麼在 `@layer` 生效時,一個低 specificity 的規則反而可能贏——因為 layer 的順序比 specificity 先比。另外現代選擇器提供了刻意操控分數的手段:`:where()` 內的內容一律算 0 分,`:is()` 則取括號內最高的那一個。這讓「寫一條容易被覆寫的預設樣式」變成可以設計的事,也是設計系統很依賴的技巧。

cascade algorithm

層疊演算法

瀏覽器在多條規則指向同一元素時,決定最終採用哪一條的完整流程。

它的順序是:先比 origin 與 importance(作者樣式、使用者樣式、瀏覽器預設,加上 !important 會翻轉),再比 cascade layer,再比 specificity,最後才比在原始碼中的出現順序。理解這個順序的實際好處是除錯——當你發現一條高 specificity 的規則竟然輸了,答案幾乎一定在更前面的關卡。

相關術語: CSS specificity (其中一關)

出處:第 9 段「Q8 CSS specificity 怎麼算出勝負」

CSS specificity

選擇器優先級

用三欄數字量化選擇器精確程度、以決定樣式勝負的機制。

三欄由高到低是 ID、class 或屬性或偽類、元素型別或偽元素。它是逐位比較而非加總,所以一個 ID(1-0-0)永遠贏過任意多個 class(0-99-0)。內聯樣式在計分之上,`!important` 又在更上層。實務上的建議不是去贏 specificity 競賽,而是把整體分數壓低、壓平——分數越接近,覆寫就越容易,這也是 utility class 與 `:where()` 這類做法流行的原因。

相關術語: cascade algorithm (屬於)、computed styles (結果呈現於)

出處:第 9 段「Q8 CSS specificity 怎麼算出勝負」

computed styles

計算後樣式

所有規則競爭完畢後,元素實際套用的最終樣式值。

在 DevTools 的 Computed 分頁看到的就是它,而 Styles 分頁會把落敗的宣告加上刪除線並保留來源,讓你看得出是被哪一條蓋掉的。這是驗證 specificity 推論最快的方式,比在腦中重算一遍可靠得多。

相關術語: CSS specificity (呈現其結果)

出處:第 9 段「Q8 CSS specificity 怎麼算出勝負」

留給下一段 現在單一元素的尺寸與樣式歸屬都清楚了,但這些規則放到不同螢幕上還要能活。所以問題升級成:一個版面要具備哪些條件,才會自己適應各種寬度?

10. Q9 響應式設計的三根支柱 28:22–30:35

第九題問響應式設計的核心原則,Bogdan 強調這要在設計階段就先定好,不然 AI 產出的東西在你的螢幕上很美、在手機上打不開。三根支柱:一、用相對單位而不是 px——rem 相對於 root、em 相對於父層,做可重用元件用 em,要跟整個 app 的尺度一致就用 rem;二、選對 layout 演算法——block 與 flex 對響應式最友善,grid 只有在你很熟或願意配大量 media query 時才划算;三、breakpoints 與 media queries——直接借 Tailwind 已經測過的那組,或叫 agent 全域套用 Tailwind 的 breakpoint,不要自己重造。

rem 與 em 各自相對於誰的對照
29:10 · rem 與 em 各自相對於誰的對照
三根支柱列完的完整白板
30:05 · 三根支柱列完的完整白板
承上 承上:上一段解決了單一元素套用哪條規則,這一段把尺度放大到整個版面在不同寬度下的行為。

推理作者先給出一個時機上的主張:響應式要在設計階段就決定,不然 AI 產出的東西在你的螢幕上很美、在手機上打不開。這一句把責任從「事後 debug」移到「事前約束」,跟第二段的工作流是同一個邏輯。接著他把原則收斂成三根支柱。第一根是相對單位:不要用 px,改用百分比、rem 或 em;他還分辨了兩者——rem 相對於 root,em 相對於父層,所以做可重用元件用 em、要跟整個應用的尺度一致就用 rem。第二根是選對 layout 演算法:block 與 flex 對響應式最友善,grid 只有在你很熟、或願意配大量 media query 時才划算。第三根是 breakpoint 與 media query:直接借 Tailwind 已經被大量驗證過的那組,或叫 agent 全域套用,不要自己重造。

AI 補充「永遠不要用 px」講得太滿,值得修正成一條更精確的規則:**會隨使用者設定或容器改變的東西才用相對單位**——字級、行高、間距、元素寬度。而不該隨字級縮放的東西用 px 反而正確,最典型的是 border 寬度(1px 的邊框放大成 1.2px 只會變糊)與某些圖示尺寸。真正該避免的其實是**用 px 寫死那些應該跟著容器或字級走的尺寸**。另外他這套支柱少了現在最關鍵的一項:**container queries**。media query 問的是「視窗多寬」,但可重用元件真正在意的是「我被放進來的這個容器多寬」——同一張卡片在側欄與主欄要有不同版面,用 media query 是做不到的,因為視窗寬度一樣。`@container` 現在主流瀏覽器都支援了,它讓元件真正做到自我響應,這也剛好呼應上一段 design system 的目標。至於他說 grid 需要大量 media query,這其實是個過時的印象:`repeat(auto-fit, minmax(...))` 這類寫法可以完全不用 breakpoint 就自動換行。

rem

根字級單位

相對於 root 元素(html)字級的長度單位。

1rem 預設是 16px,但關鍵在於它會跟著使用者在瀏覽器設定裡調整的字級走——這是它比 px 更該用在字級與間距上的真正理由,屬於無障礙需求而不只是美觀。因為它只認 root,所以同一個 `1.5rem` 在頁面任何角落都是同一個大小,適合用來建立全站一致的尺度。

相關術語: em (對照組)

出處:第 10 段「Q9 響應式設計的三根支柱」

em

父層字級單位

相對於當前元素字級的長度單位,用在 font-size 時則相對於父層。

它會累積:巢狀元素各自設定 1.2em 時,實際倍率是連乘的,這是它最常被詬病的地方。但同一個特性用在元件內部是優點——按鈕的 padding 寫成 em,換一個字級整顆按鈕就等比例縮放,不必為每種尺寸各寫一套。判準是「這個尺寸該跟著誰變」:跟著自己的字級用 em,跟著全站尺度用 rem。

相關術語: rem (對照組)

出處:第 10 段「Q9 響應式設計的三根支柱」

breakpoint

斷點

版面切換配置的螢幕寬度界線。

直接沿用框架提供的那一組(Tailwind 是 640/768/1024/1280/1536)通常比自訂好,理由不是那些數字神奇,而是它們已經被大量真實裝置驗證過,而且團隊裡每個人講的「md」都是同一件事。更好的心法是「內容決定斷點」:把視窗慢慢拉窄,版面在哪裡開始難看,那裡才是斷點。

相關術語: media query (由它實作)

出處:第 10 段「Q9 響應式設計的三根支柱」

media query

媒體查詢

根據視窗尺寸或裝置特性有條件套用 CSS 的語法。

它問的永遠是「視窗」或「裝置」的狀態,這既是它的力量也是它的限制:一個可重用元件無法用它得知自己被放在多寬的容器裡。這個缺口由 container query(`@container`)補上,兩者的分工是——版面層級用 media query,元件層級用 container query。它也不只能問寬度,`prefers-reduced-motion`、`prefers-color-scheme` 都屬於同一機制。

相關術語: breakpoint (實作它)

出處:第 10 段「Q9 響應式設計的三根支柱」

見仁見智 28:50
「So, you never want to use pixels, for example.」
太絕對。該用相對單位的是會隨使用者字級設定或容器變動的尺寸(字級、間距、元素寬度);border 寬度、細線、某些圖示尺寸用 px 反而正確,因為它們不該跟著字級放大。真正的問題不是 px 本身,而是把應該跟著容器或字級走的尺寸寫死。
依據: WCAG 1.4.4 要求文字可縮放至 200%,指向字級用相對單位;但並未要求所有長度都用相對單位
留給下一段 到這裡,CSS 這一側——尺寸、優先級、版面適應——已經連成一串了。但畫面卡頓的另一半原因不在 CSS,而在 JavaScript 什麼時候被執行。所以要換到執行模型:event loop。

11. Q10 event loop 的完整運作 30:35–34:45

第十題要 event loop 的高層全貌。JavaScript 之所以單執行緒,是因為 DOM 是唯一的 singleton,多執行緒改 DOM 會產生無法預測的平行化 bug。所以有一條 call stack 負責執行,加上兩個佇列:promise 的 then 進 microtask queue,瀏覽器事件與 timer 的 callback 進 macrotask queue。這些結構住在 V8 引擎裡,而 loop 本身是瀏覽器用 C++ 實作的。迴圈的節奏是:main thread 空閒就執行 stack,清空後取一個 microtask,接著先讓 main thread 做 rendering,再回頭取下一個。他用「兩條輸送帶共用同一個機器人」比喻 JavaScript 與 rendering 共搶 CPU,這就是「不要阻塞 main thread」的真正意思,也是兩者不能同時跑的原因:邊 render 邊改 DOM 會讓 UI 狀態壞掉。

stack 與 microtask / macrotask 兩個佇列的關係圖
31:40 · stack 與 microtask / macrotask 兩個佇列的關係圖
V8 引擎的邊界,以及 loop 其實在瀏覽器的 C++ 那一側
32:35 · V8 引擎的邊界,以及 loop 其實在瀏覽器的 C++ 那一側
兩條輸送帶共用同一個機器人的比喻圖
33:35 · 兩條輸送帶共用同一個機器人的比喻圖
承上 承上:上一段把畫面適應的問題交給 CSS 解完,剩下的卡頓來自 JavaScript 什麼時候被執行,這一段就進到執行模型本身。

推理作者從一個因果問題開始:JavaScript 為什麼是單執行緒?答案是 DOM 是唯一的共享結構,多執行緒同時改它會產生無法預測的平行化 bug——瀏覽器其實是多執行緒的,是刻意不讓 JavaScript 多執行緒。有了這個前提,剩下的結構就都有理由存在:一條 call stack 負責執行,加上兩個佇列讓事情能非同步排隊——promise 的 then 進 microtask queue,瀏覽器事件與 timer 的 callback 進 macrotask queue。他還補了一個常被忽略的邊界:這些結構住在 V8 引擎裡,而 loop 本身是瀏覽器用 C++ 實作的,兩者不是同一個東西。最後他把整個節奏收成一個比喻——JavaScript 與 rendering 是兩條輸送帶共用同一個機器人,所以「不要阻塞 main thread」不是一句口號,而是這個結構的直接後果。

AI 補充他描述的迴圈節奏有一個地方要修正,而且這正是面試最愛追問的細節:**microtask queue 不是一次取一個,而是一次清空。** 正確的節奏是:執行一個 macrotask(例如一個 click handler)→ 把 microtask queue 整個排乾,包含執行過程中新產生的 microtask →(必要時)rendering → 再取下一個 macrotask。這個差別有實際後果:一個不斷產生新 microtask 的 promise 迴圈會讓瀏覽器**永遠回不到 rendering**,畫面完全凍住;而同樣邏輯用 setTimeout 寫成 macrotask,畫面反而還能更新。「應該用哪一種排隊」因此是有答案的,不是風格問題。另外還有一層他沒提但很值得知道的:rendering 並不是每次迴圈都做,瀏覽器會對齊螢幕更新頻率,約每 16.7 毫秒一次,這就是 `requestAnimationFrame` 存在的位置——它保證你的程式碼在下一次繪製之前執行,這也是為什麼用它做動畫比 setTimeout 準。

event loop

事件迴圈

持續檢查佇列並把待辦工作送進 call stack 執行的循環機制。

它讓單執行緒的 JavaScript 得以處理非同步:耗時的工作交給瀏覽器其他執行緒去做,完成後把 callback 排進佇列,等 stack 空了再執行。要注意 Node.js 的 event loop 有更多階段(timers、poll、check…),跟瀏覽器的模型不完全一樣,面試時值得說清楚你講的是哪一個。

相關術語: call stack (餵給它工作)、microtask queue (來源之一)

出處:第 11 段「Q10 event loop 的完整運作」

call stack

呼叫堆疊

記錄目前正在執行的函式與其區域環境的後進先出結構。

每次呼叫函式就推入一個 stack frame,裡面有參數、區域變數與 closure 的參照;函式返回就彈出。錯誤訊息裡的 stack trace 就是它的快照,而無窮遞迴造成的 stack overflow 是它容量的上限。關鍵性質是:只要 stack 不空,event loop 就完全不會前進——這就是「阻塞」的精確定義。

相關術語: event loop (被它填入)

出處:第 11 段「Q10 event loop 的完整運作」

microtask queue

微任務佇列

存放 promise 回呼等高優先工作的佇列,會在每個任務之後被整個清空。

來源包括 promise 的 then/catch/finally、queueMicrotask、以及 MutationObserver。它跟 macrotask 最重要的差別是「一次清空」——執行 microtask 期間新產生的 microtask 也會在同一輪被處理完。這保證了 promise 鏈的執行緊接在當前任務之後、不被畫面更新插隊,但也代表無止盡的 microtask 會讓畫面永遠凍住。

相關術語: macrotask queue (優先於)

出處:第 11 段「Q10 event loop 的完整運作」

macrotask queue

巨任務佇列

存放事件回呼、setTimeout 等一般任務的佇列,每輪只取一個執行。

click handler、setTimeout、setInterval、網路事件的 callback 都排在這裡。每輪迴圈只取一個,執行完就把 microtask 排乾、必要時 render,然後才取下一個。這個節奏正是為什麼把大量工作用 setTimeout 切成小塊可以讓畫面保持回應——你主動在每一塊之間留出了 render 的機會。

相關術語: microtask queue (優先級較低)

出處:第 11 段「Q10 event loop 的完整運作」

V8

V8 引擎

Chrome 與 Node.js 使用的 JavaScript 引擎,負責解析、編譯與執行程式碼。

作者特別區分 V8 與 event loop 是有意義的:V8 只提供語言本身——stack、heap、垃圾回收、JIT 編譯。setTimeout、DOM、fetch 這些都不是 JavaScript 語言的一部分,而是宿主環境(瀏覽器或 Node)提供的,event loop 也是。這解釋了為什麼同一段程式碼在瀏覽器與 Node 的非同步行為可能有細微差異。

相關術語: event loop (由宿主提供)

出處:第 11 段「Q10 event loop 的完整運作」

確定錯誤/已過時 32:55
「And we don't immediately pick the microtask.」
他描述成「執行一個 microtask 後就停下來去做 rendering,下一輪才取下一個」。實際規格是:每個 macrotask 結束後,microtask queue 會被**整個排乾**(包含執行過程中新增的),然後才輪到 rendering。這個差別有實際後果——不斷產生 microtask 的迴圈會讓畫面永遠不更新,而同樣的工作排成 macrotask 則不會。
依據: HTML Living Standard, event loop processing model:perform a microtask checkpoint 在每個 task 之後、update the rendering 之前
留給下一段 知道了工作是怎麼排隊的,下一步自然是問:當排隊的工作太重時,實際會長成什麼樣的線上問題?

12. event loop 在生產環境的真實 bug 34:45–35:45

主持人追問理論之外有沒有真的遇過 event loop 的線上 bug。Bogdan 說最常見的就是推進 stack 的那份工作太重,而且多半出在元件框架:表單上掛了太多 event handler,使用者一打字就觸發一堆 re-render,畫面看起來卡卡的。解法是把 stack 當成「擅長做零碎工作」的東西,只餵它很小的獨立單位,並在大型元件框架裡盡量用 memoization 讓 re-render 變便宜。

承上 承上:上一段留下的問題是「排隊的工作太重時會長成什麼樣」,主持人正好追問有沒有真的遇過。

推理作者給的案例把前一段的抽象結構落回具體現場:使用者在表單裡打字時畫面卡卡的。他的診斷直接對應到那個結構——問題出在推進 stack 的那份工作太重,而且多半來自元件框架:掛了太多 event handler,每次輸入都觸發一連串 re-render,全部擠在同一條 stack 上。解法因此也是從結構推出來的:stack 擅長的是零碎工作,所以要餵它很小的獨立單位,並用 memoization 讓每次 re-render 變便宜。

AI 補充這個診斷是對的,但少了一個更常見也更容易修的根因:**受控輸入把每一個按鍵都變成一次全樹更新**。每打一個字就 setState 一次,如果那個 state 位置太高,整棵子樹都要重新渲染。除了 memoization,實務上更有效的三種手段是:把輸入的 state 下推到最靠近的元件、對昂貴的後續動作(搜尋、驗證)做 debounce、以及在 React 18 之後用 `useDeferredValue` 或 `startTransition` 把非緊急的更新降級,讓打字本身永遠優先。另外「打字卡頓」還有一個容易被忽略的來源:卡的往往不是 JavaScript 本身,而是它引發的 layout——這剛好接回第二段的 reflow。用 DevTools 的 Performance 錄一段,就能分辨到底是 scripting 還是 rendering 佔掉了時間。

memoization

記憶化

把計算結果依輸入快取起來,相同輸入再次出現時直接回傳結果。

在 React 裡有三個形式:`useMemo` 快取值、`useCallback` 快取函式參照、`React.memo` 快取整個元件的渲染結果。它不是免費的——比較與儲存本身有成本,包裝一個很便宜的計算通常是淨虧。判準是「這個計算比一次淺層比較貴嗎」。React Compiler 出現後,多數這類手動包裝會由編譯器自動插入。

相關術語: call stack (減輕其負擔)

出處:第 12 段「event loop 在生產環境的真實 bug」

留給下一段 他這個案例談的是輸入反應變慢。但真實情境裡使用者說「很慢」時,也可能指的是打開很慢。那就需要一套能先分辨、再對症下藥的流程。

13. Q11 vibe coded 的 React 很慢怎麼辦 35:45–38:53

第十一題給了一個情境:PM 抱怨一個 vibe coded 的 React 專案很慢。Bogdan 先澄清是載入慢還是輸入反應慢,通常是載入慢。載入慢的路徑:先用 bundle analyzer 看 bundle size,砍掉或延後不必要的套件,用 dynamic import 與 code splitting 依路徑或使用者行為切,再考慮 SSR 與 server components 讓送到瀏覽器的 JavaScript 更少——因為 critical rendering path 上那些大檔案都要下載、解析、直譯完才輪到渲染。輸入卡頓的路徑則全在 re-render:要嘛避免(React.memo 比較 props),要嘛讓它更快(useCallback、useMemo);再加兩條少被提的——把不需要 state 的邏輯搬出元件、以及不要無謂地把 state 往上提,state 放在越接近使用的地方越好。

承上 承上:上一段的案例是輸入反應慢,但使用者說「很慢」時也可能指載入慢,這一段就從釐清這兩者開始。

推理作者的第一動作不是給方案而是**先分辨**:是載入慢還是輸入反應慢?他判斷多半是載入慢,於是先走這條路。載入慢的推理鏈很直接:慢的原因是送出去的 JavaScript 太多,而 critical rendering path 上的大檔案都要下載、解析、直譯完才輪到渲染。所以行動順序是——先用 bundle analyzer 看清楚組成,砍掉或延後不必要的相依,用 dynamic import 依路徑或使用者行為做 code splitting,再考慮 SSR 與 server components 從源頭少送 JavaScript。另一條路是輸入反應慢,全部收斂到 re-render:要嘛避免(React.memo 比較 props,父層重繪時不跟著重繪),要嘛讓它更快(useCallback、useMemo 避免每次重建函式與值)。他還加了兩條比較少人提的:把不需要 state 的邏輯搬到元件外面,避免每次渲染都重建;以及不要無謂地把 state 往上提,因為 state 越高,它一變動要重繪的子樹就越大。

AI 補充他的順序是對的,但可以再加一條更前面的動作:**先量,再改**。用 Lighthouse 或 web-vitals 看是 LCP(載入)還是 INP(互動)差,數字會直接告訴你走哪條路,不必猜。這也讓「PM 說很慢」變成可以驗收的目標。另外 memoization 那一段有個容易踩空的地方:`React.memo` 做的是 props 的淺層比較,只要父層每次渲染都傳一個新建立的物件或函式進去,它就永遠比不相等,包了等於沒包——所以 `React.memo` 幾乎必須和 `useCallback`/`useMemo` 一起用才有效。這也是 React Compiler 的價值所在:它在編譯期自動處理這些包裝,讓這一整類手動最佳化變成不必要。最後補一個他略過但影響很大的載入因素:字型與圖片。JavaScript 只是 critical rendering path 的一部分,未預載的字型與未指定尺寸的圖片經常才是 LCP 與版面跳動的主因。

術語:lifting state up

critical rendering path

關鍵渲染路徑

瀏覽器從收到 HTML 到畫出第一個畫面之間必經的一連串步驟。

順序是:解析 HTML 建 DOM → 解析 CSS 建 CSSOM → 合併成 render tree → layout → paint。CSS 與同步的 script 都是阻塞的,所以放在 head 的大檔案會直接推遲第一次繪製。最佳化它的三個方向是減少必要位元組、減少必要往返、以及把非必要資源移出這條路徑(defer、preload、async)。

相關術語: code splitting (用來縮短它)

出處:第 13 段「Q11 vibe coded 的 React 很慢怎麼辦」

code splitting

程式碼分割

把 bundle 切成多個可按需載入的區塊,而不是一次送出全部。

最自然的切分邊界是路由:使用者沒去的頁面不必先下載。其次是互動觸發的重量級元件——圖表、編輯器、地圖。在 React 裡用 `React.lazy` 加 `Suspense`,打包工具則靠動態 `import()` 自動產生分割點。切得太細也有代價:每個 chunk 都是一次請求,過多的小檔案會被往返時間吃掉好處。

相關術語: critical rendering path (縮短它)

出處:第 13 段「Q11 vibe coded 的 React 很慢怎麼辦」

React.memo

React.memo

包住元件、在 props 淺層相等時跳過重新渲染的高階元件。

它只比較 props 的第一層參照。父層每次渲染都新建的物件、陣列或行內函式會讓比較永遠失敗,所以它幾乎必須搭配 `useMemo`/`useCallback` 才有效。也因此它不該到處亂加——包裝與比較本身有成本,只有在該元件確實昂貴、且 props 確實穩定時才划算。

相關術語: memoization (一種應用)

出處:第 13 段「Q11 vibe coded 的 React 很慢怎麼辦」

server components

伺服器元件

只在伺服器執行、渲染結果送到瀏覽器、本身的程式碼不進 bundle 的 React 元件。

它跟 SSR 不是同一件事:SSR 是把元件先在伺服器渲染成 HTML,但那個元件的 JavaScript 仍然要送到瀏覽器做 hydration;server component 的程式碼根本不會被送出去。因此它對 bundle 的影響是結構性的——資料處理與相依重的邏輯留在伺服器,客戶端只留真正需要互動的部分。代價是它不能有狀態或事件處理,得明確劃出 client 邊界。

相關術語: critical rendering path (減少其負擔)

出處:第 13 段「Q11 vibe coded 的 React 很慢怎麼辦」

lifting state up

狀態上提

把共用的狀態移到共同的父元件,讓多個子元件讀到同一份資料。

它是解決兄弟元件共享資料的標準做法,但被濫用時會變成效能問題:狀態放得越高,它一變動要重新渲染的子樹就越大。實用的判準是「把 state 放在仍然能被所有需要它的元件讀到的最低位置」。當它真的必須放很高時,改用 context 加上細分的訂閱、或外部 store,可以避免整棵樹跟著動。

相關術語: React.memo (常一起使用)

出處:第 13 段「Q11 vibe coded 的 React 很慢怎麼辦」

留給下一段 到這裡談的都是「怎麼讓現有的東西變快」。但如果問題不是慢,而是使用者數量整整多了一百倍呢?那就從最佳化變成了系統設計。

14. Q12 object 與 map 的差別 38:53–39:56

第十二題回到 JavaScript 基礎:object 與 Map 的差別。最大的一點是 object 的 key 只能是字串或 symbol,Map 的 key 幾乎可以是任何東西,所以 Map 很適合拿來當「用任意值當索引」的記憶體空間。另外 object 要迭代得先過 Object.entries 或 keys,Map 本身就帶 iterator。但實務上大家還是用 object 居多,因為語法更順手;只有當你的 key 真的需要超出字串與 symbol 時才換 Map。

承上 承上:上一段把效能問題收在 React 的抽象層,這一段回到更底層的 JavaScript 資料結構,為後面的記憶體討論鋪路。

推理作者從最實質的差別切入:object 的 key 只能是字串或 symbol,Map 的 key 可以是任何值,包括物件本身。因為這一點,Map 適合當「用任意值當索引」的儲存空間。他補了第二個差別:object 要迭代得先過 `Object.entries` 或 `keys`,Map 本身就是可迭代的。但他接著給了一個很誠實的實務判斷——大家還是多用 object,因為語法更順手;只有當 key 真的需要超出字串與 symbol 時才換 Map。

AI 補充還有兩個差別在面試裡常被追問,值得補上。第一是**順序保證**:Map 嚴格保留插入順序,而 object 的整數型 key 會被排到前面並升冪排列——所以用 object 存 id 對應時,取出的順序可能跟你放進去的不一樣,這是很隱蔽的 bug 來源。第二是**乾淨程度**:object 會繼承 `Object.prototype` 上的東西,所以 `'toString' in obj` 是 true,Map 沒有這個問題(要避開的話得用 `Object.create(null)`)。另外效能上,頻繁新增刪除 key 的場景 Map 通常明顯較快,因為引擎對 object 的最佳化假設是形狀穩定。至於 JSON 序列化,object 直接可用而 Map 不行,這常常才是實務上真正的決定因素。

Map

映射

ES6 提供的鍵值集合,key 可以是任意型別並保留插入順序。

它與 object 的差別包含四項:key 型別不限、順序有保證、`size` 可直接取得、以及沒有原型鏈上的干擾。它本身可迭代,可以直接 `for...of` 或展開成陣列。缺點是不能直接 JSON 序列化,而且語法比物件字面量囉唆——這正是它在實務上普及度不高的主要原因。

相關術語: iterator (內建實作)、Symbol (另一種 key 型別)

出處:第 14 段「Q12 object 與 map 的差別」

Symbol

符號

ES6 的原始型別,每次建立都產生唯一值,常用來當不會撞名的物件 key。

它的用途是在別人的物件上加屬性而不擔心覆蓋——因為兩個 symbol 永遠不相等,即使描述文字一樣。內建的 well-known symbol 則用來掛語言層級的行為,`Symbol.iterator` 就是其中之一,決定一個物件能不能被 `for...of` 走訪。

相關術語: Map (可作為其 key)

出處:第 14 段「Q12 object 與 map 的差別」

iterator

迭代器

實作了 next() 方法、能被 for...of 逐一取值的物件協定。

一個物件只要有 `Symbol.iterator` 方法回傳迭代器,就能被 `for...of`、展開運算子與解構使用。Array、Map、Set、字串都內建了它,普通 object 沒有——這就是為什麼 object 要先轉成 `Object.entries` 才能走訪。這個協定也是後面 WeakMap 最大的限制所在。

相關術語: Map (實作於)

出處:第 14 段「Q12 object 與 map 的差別」

留給下一段 Map 可以用物件當 key,這帶出一個他還沒處理的後果:那些被當成 key 的物件,什麼時候才會被釋放?

15. Q13 Map、WeakMap 與垃圾回收 39:56–42:10

第十三題把 Map 的話題接到記憶體管理。Map 裡的 key 不會被垃圾回收:即使那個物件在程式其他地方已經沒人參照,Map 仍然抓著它,一直塞就會漏記憶體。所謂「沒被用到」在記憶體管理上的意思是「從根部不可達」——物件要靠掛在 DOM 或 event handler 上才活著,沒掛住的就會被標記回收。WeakMap 的差別就在這裡:key 一旦不可達就會被移走,而 WeakMap 本身還留在記憶體裡。代價是 WeakMap 沒有 iterator,你沒辦法遍歷它,必須自己留著 key,所以寫起來比較麻煩;但拿來做函式的 memoization 時,它比純 Map 安全得多。

承上 承上:上一段留下「被當成 key 的物件什麼時候會被釋放」,這一段就把 Map 的話題接到記憶體管理。

推理作者先講後果再講機制:Map 裡的 key 不會被回收,即使那個物件在程式其他地方已經沒人用了,Map 還抓著它,一直塞就漏記憶體。接著他補上「沒被用到」在記憶體管理裡的精確意思——不是「沒人呼叫」,而是**從根部不可達**;物件要靠掛在 DOM 或 event handler 這類還活著的東西上才留得下來,掛不住的就會被標記並回收。有了這個定義,WeakMap 的差別就清楚了:它對 key 的參照不算數,key 一旦不可達就連同那筆值一起被移走,而 WeakMap 本身還在。代價是它沒有 iterator,你不能遍歷它,必須自己留著 key——這一點正好呼應上一段的迭代器協定。最後他給出使用時機:拿來替函式做快取時,WeakMap 比 Map 安全得多。

AI 補充他的結論對,但機制的說法要修正:**不是垃圾回收器「進不去」Map**,而是 Map 對它的 key 持有強參照。GC 完全看得見 Map 裡的東西,正因為看得見、而且那是一條從根部出發的有效參照鏈,所以它判定那個物件還活著。WeakMap 的差別在於它的參照被明確定義為弱參照,計算可達性時不算數。這個修正很重要,因為它解釋了為什麼 `weakMap.set(key, valueThatReferencesKey)` 這種寫法仍然會漏——問題從來不在容器,而在參照關係。補充兩件實務上的事:WeakMap 的 key 必須是物件或 symbol,不能是字串或數字(因為原始值沒有身分可以判斷可達性);還有它沒有 `size`,因為那個數字隨時可能因為 GC 而改變,暴露出來會讓 GC 的時機變成可觀察的行為。至於作者說的 memoization 用途——用 WeakMap 以物件參數當 key 快取結果,物件一被丟棄,快取自動消失,不需要手動清理,這確實是它最漂亮的應用。

術語:memory leak

WeakMap

弱映射

以弱參照持有 key 的 Map,key 一旦不可達就會連同對應的值一起被回收。

它的 key 只能是物件或 symbol,而且不可迭代、沒有 size——這些限制都是同一個原因造成的:內容可能隨時被 GC 拿走,任何列舉行為都會讓 GC 時機變成可觀察的。最典型的用途有兩個:以物件為 key 的 memoization,以及把額外資料掛在別人的物件或 DOM 節點上而不阻止它被釋放。

相關術語: Map (弱參照版本)、garbage collection (受其影響)

出處:第 15 段「Q13 Map、WeakMap 與垃圾回收」

garbage collection

垃圾回收

執行環境自動找出不再可達的物件並釋放其記憶體的機制。

現代 JavaScript 引擎用的是 mark-and-sweep:從一組根(全域物件、目前的 stack、DOM 樹)出發走訪所有可達物件並標記,沒被標記的就回收。V8 還會分代——多數物件很快就死,所以新生代掃得頻繁而便宜,活過幾輪的才晉升老生代。理解它的實際好處是:你無法命令它何時執行,只能控制參照關係。

相關術語: reachability (判準)

出處:第 15 段「Q13 Map、WeakMap 與垃圾回收」

reachability

可達性

從根物件出發能否經由參照鏈走到某個物件,是判斷它該不該被回收的唯一標準。

「沒被用到」在記憶體管理裡沒有意義,只有「走不走得到」才算數。這解釋了前端最常見的幾種洩漏:忘了移除的 event listener(DOM 節點被 handler 抓著)、沒清掉的 setInterval(callback 抓著整個 closure)、以及被推進全域陣列的快取。只要那條參照鏈還在,物件就活著。

相關術語: garbage collection (其判準)、memory leak (違反時發生)

出處:第 15 段「Q13 Map、WeakMap 與垃圾回收」

memory leak

記憶體洩漏

程式已經不再需要、但仍然可達因而無法被回收的記憶體。

在單頁應用裡特別致命,因為分頁可能開著好幾個小時,每次路由切換都留下一點殘渣。診斷方式是 Chrome DevTools 的 Memory 分頁:做一次操作、回到起點、強制 GC,比較前後的 heap snapshot,看哪一類物件的數量只增不減。

相關術語: reachability (由它造成)

出處:第 15 段「Q13 Map、WeakMap 與垃圾回收」

留給下一段 記憶體與資料結構這一層談完了,前端基礎的部分也就收束了。剩下最後兩題是尺度最大的:當使用者變成一百倍時,整個前端要怎麼撐住?

16. Q14 從一千到十萬日活怎麼擴 42:10–47:35

第十四題是前端系統設計:日活從一千長到十萬。純 client-rendered 的情況很單純,靜態資產丟 CDN 就好——但資深的差別在於「怎麼用 CDN」:依變動頻率做選擇性快取,把 React、React DOM 這種穩定 vendor 在 Webpack 或 Vite 層切成長壽 chunk 給很長的 max-age,自己的元件與邏輯切成另一包給幾分鐘的 max-age,使用者才不用每次重下載全部。micro-frontends 則只在開發人數多、想縮小爆炸半徑時才需要,十萬日活單體前端也扛得住。SSR 就複雜得多:所有人都得回你的伺服器渲染,形成瓶頸也帶來延遲。現代解法是把渲染推到邊緣(例如 Cloudflare Workers),但這樣就得連資料庫也放到邊緣:靜態資產、worker 後端、唯讀複本組成一個 edge location,寫入仍要回主資料庫,因此寫很貴、而且只有最終一致性。他也提醒這種架構只有在流量真的分散且效能極關鍵時才值得,快取失效多、出事極難除錯。

bundle 切成穩定 vendor 與常改的應用碼兩包的示意
43:40 · bundle 切成穩定 vendor 與常改的應用碼兩包的示意
edge location 的完整圖:靜態資產、worker 後端、唯讀複本與主 DB
46:30 · edge location 的完整圖:靜態資產、worker 後端、唯讀複本與主 DB
承上 承上:上一段收束了前端基礎,這一段把尺度放到最大——使用者變成一百倍時的系統設計。

推理作者先做了一件跟第十一題一樣的事:**先分辨情境**。他問這是 client-rendered 還是 server-rendered,因為兩者的答案完全不同。client-rendered 的情況很單純,靜態資產丟 CDN 就好;但他馬上指出這正是 junior 與 senior 的分水嶺——大家都會說「用 CDN」,差別在於**怎麼用**。他的做法是依變動頻率做選擇性快取:在 Webpack 或 Vite 層把 React、React DOM 這種穩定 vendor 切成長壽 chunk 給很長的 max-age,自己的元件與邏輯切成另一包給幾分鐘的 max-age,使用者才不必每次重下載全部。這條推理的漂亮之處在於它把 build 設定與快取策略連起來——你怎麼切 bundle,決定了你能不能分開快取。至於 micro-frontend,他自己給了限縮:只有開發人數多時才需要,十萬日活單體前端也撐得住。接著他處理 SSR:所有人都得回伺服器渲染,形成瓶頸也帶來延遲。現代解法是把渲染推到邊緣,但這樣就得連資料庫也放到邊緣——靜態資產、worker 後端、唯讀複本組成一個 edge location,寫入仍要回主庫,所以寫很貴、而且只有最終一致性。最後他自己踩煞車:這種架構複雜、快取失效多、出事極難除錯,多數情況不值得。

AI 補充這裡有一個他沒點破、但是整套快取策略成立的前提:**內容雜湊檔名**。長 max-age 之所以安全,是因為檔名裡帶著內容雜湊(`vendor.a3f9c2.js`),內容一變檔名就變,等於新的資源,根本不需要等快取過期。所以正確的組合是「HTML 不快取或短快取、帶雜湊的資產設 `max-age=31536000, immutable`」,而不是靠猜幾天該重新檢查。他說的「七天檢查一次」在有雜湊檔名的情況下其實太保守了。另外 vendor 分包這個做法近年有個反轉值得知道:當所有資產都走 HTTP/2 或 HTTP/3、而且部署頻繁時,把 vendor 獨立出來的收益會下降,因為 vendor 版本一升級整包就失效,反而不如按路由切分來得實在。至於邊緣的最終一致性,他提到 CAP 但沒說到前端最痛的那個具體症狀:使用者剛送出的寫入,重新整理後看不到自己剛剛改的東西。實務上的緩解是「讀自己的寫入」——寫入後短時間內把該使用者的讀取導回主庫,或直接用本地樂觀更新蓋住那段延遲。

術語:edge computingCAP theorem

CDN

內容傳遞網路

分散在全球的伺服器節點,把靜態資源快取在離使用者最近的地方。

它同時解決兩件事:延遲(距離短)與源站負載(多數請求根本不會回到你的伺服器)。要用得好,關鍵在快取鍵與失效策略——哪些 header 會參與快取鍵、部署時怎麼讓舊資源失效。內容雜湊檔名之所以是最佳實踐,正是因為它讓「失效」這個困難問題消失:新內容就是新網址。

相關術語: max-age (由它控制)、edge computing (延伸形態)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

max-age

快取有效期

Cache-Control 中指定資源可被視為新鮮的秒數。

常見組合有三種:帶內容雜湊的資產用 `max-age=31536000, immutable`(一年,且不必再驗證);HTML 用 `no-cache`(每次都問一下有沒有更新,但可以用 304 省流量);API 回應用短的 max-age 加 `stale-while-revalidate`(先給舊的、背景更新)。理解 `no-cache` 不是「不快取」而是「用前先驗證」,是這題很容易加分的細節。

相關術語: CDN (作用於)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

edge computing

邊緣運算

把運算放在靠近使用者的分散節點上執行,而不是集中在單一資料中心。

Cloudflare Workers、Vercel Edge Functions 都屬於這一類,通常跑在啟動極快的輕量執行環境上而不是完整的 Node 程序。它對 SSR 特別有價值,因為渲染這件事本身沒有狀態。但只要牽涉到資料,延遲就會跑回資料庫那一端——這正是作者接著要放唯讀複本的原因,也是邊緣架構真正的複雜度所在。

相關術語: read replica (常搭配)、CDN (延伸自)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

read replica

唯讀複本

從主資料庫非同步複製而來、只接受讀取的資料庫副本。

它讓讀取可以水平擴展並靠近使用者,但寫入仍然只能回主庫。複製有延遲,通常是毫秒到秒級,這個延遲就是最終一致性的來源。前端最常見的後果是「送出後立刻重新整理,看不到自己剛剛的修改」,緩解方式是寫入後短時間內把該使用者的讀取導回主庫。

相關術語: eventual consistency (造成)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

eventual consistency

最終一致性

分散式系統中,各副本在沒有新寫入的情況下最終會收斂到相同狀態,但短時間內可能不一致。

它是為了可用性與延遲而做的交換,CAP 定理描述的正是這個取捨。判斷能不能接受的方式是問「這筆資料不一致幾秒鐘,使用者會受到什麼傷害」——社群動態可以,帳戶餘額不行。前端的職責通常是把這段窗口藏起來:樂觀更新、明確的載入狀態、或在關鍵操作後強制讀主庫。

相關術語: read replica (由它產生)、CAP theorem (理論依據)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

CAP theorem

CAP 定理

分散式系統在網路分區發生時,只能在一致性與可用性之間擇一。

常被簡化成「三選二」,但更精確的說法是:分區容忍性在分散式系統裡是必須接受的前提,所以真正的選擇只在分區發生的那一刻——要拒絕服務以保持一致,還是繼續服務但可能回傳舊資料。作者的邊緣架構選的是後者,這也是絕大多數面向使用者的網站的選擇。

相關術語: eventual consistency (其後果)

出處:第 16 段「Q14 從一千到十萬日活怎麼擴」

留給下一段 擴展的問題到這裡是關於「怎麼把東西送到使用者面前」。最後一題把方向反過來:資料要怎麼持續從伺服器流回使用者?

17. Q15 LLM 聊天機器人用哪種即時協定 47:35–50:40

最後一題問把 LLM 聊天機器人接進前端要用什麼即時通訊協定。polling 最好做、最穩,但等於自己 DDoS 後端,一萬個 client 乘上去就把後端打掛,擴展性差。WebSocket 開的是雙向通道,很適合真人對真人的聊天,但後端負擔重也較複雜。而 LLM 的通訊是不對稱的:client 送一次 query,模型回一串一串的 token。所以最合適的是 Server-Sent Events——一個 POST 帶上對話,然後在那個端點上聽,模型生成多少就收多少。它是瀏覽器原生 API,連函式庫都不用,OpenAI 的套件底下也是這個;打開 ChatGPT 或 Claude 的 network tab,在 event stream 分頁就看得到 token 一顆顆進來。

WebSocket 雙向通道的示意
48:35 · WebSocket 雙向通道的示意
client 送一次、模型回一串 token 的不對稱示意,SSE 的理由
49:25 · client 送一次、模型回一串 token 的不對稱示意,SSE 的理由
承上 承上:上一段處理的是怎麼把資產送到使用者面前,這一段把方向反過來——資料要怎麼持續從伺服器流回使用者。

推理作者用一貫的方式排序候選方案,每個都先給優點再給致命傷。polling 最好做、最穩,但等於自己 DDoS 後端——他還刻意接回上一題的數字:一萬個 client 乘上去,後端就掛了。WebSocket 開的是雙向通道,很適合真人對真人的聊天,因為訊息真的是雙向的,但後端負擔重也較複雜。到這裡他做了整段的關鍵一步:回頭看**通訊的形狀**。LLM 的通訊是不對稱的——client 送一次 query,模型回一長串 token。既然形狀不對稱,就不需要付雙向通道的代價,Server-Sent Events 正好對上:一個 POST 帶上對話,然後在那個端點上聽,模型生成多少就收多少。而且它是瀏覽器原生 API,連函式庫都不用;OpenAI 的套件底下也是這個,打開 ChatGPT 或 Claude 的 network tab,在 event stream 分頁就看得到 token 一顆顆進來。這個「用資料流的形狀決定協定」的推法,正是整支影片想示範的資深判斷。

AI 補充結論是對的,但有兩個實務細節會讓這個答案更完整。第一,**原生的 `EventSource` 只能發 GET,而且不能自訂 header**——所以真正在做 LLM 串流時,多數人並不是用 EventSource,而是用 `fetch` 加 `POST`,再自己讀 `response.body` 這個串流並解析 SSE 格式(`data:` 開頭的行)。作者說「用純 JavaScript 就能做」是對的,但省略了這一步,照 `EventSource` 的字面去做會卡在無法傳送對話內容與 API 金鑰。第二,**取消**在 LLM 場景特別重要:使用者按下停止時要能真的中止生成,用 fetch 搭配 `AbortController` 很自然,這也是另一個偏好 fetch 而非 EventSource 的理由。最後補一個他沒提的取捨:SSE 走的是單一 HTTP 連線,在 HTTP/1.1 下每個網域的並行連線數有限(通常六條),多個分頁同時開串流可能互相卡住;HTTP/2 之後這個限制才消失。

polling

輪詢

用固定間隔重複向伺服器請求,檢查有沒有新資料。

它的優點是實作與除錯都最簡單,任何後端都支援,斷線重連也不必特別處理。代價是請求量等於使用者數乘上頻率,而且絕大多數請求是白跑的。折衷做法是 long polling——伺服器把請求掛住直到有資料或逾時才回應,用一次往返換掉多次空轉,這也是很多即時功能在上 WebSocket 之前的過渡方案。

相關術語: WebSocket (對照組)

出處:第 17 段「Q15 LLM 聊天機器人用哪種即時協定」

WebSocket

WebSocket

在單一 TCP 連線上建立全雙工通道的協定,雙方都能主動送訊息。

它從一次 HTTP 升級交握開始,之後就脫離 HTTP 的請求/回應模型。適合真正雙向且高頻的場景:多人協作、遊戲、真人聊天。代價是伺服器要維持大量長連線的狀態,橫向擴展時需要額外的訊息匯流層來跨機器廣播;斷線重連、心跳、訊息順序都得自己處理,這些通常才是實際的複雜度來源。

相關術語: Server-Sent Events (雙向版本)

出處:第 17 段「Q15 LLM 聊天機器人用哪種即時協定」

Server-Sent Events

伺服器推送事件

伺服器透過一條長時間開啟的 HTTP 回應,持續單向推送文字事件給瀏覽器的機制。

格式極簡:每則訊息是以 `data:` 開頭的一行文字,空行結束。因為走的就是普通 HTTP,所以代理、壓縮、驗證全部沿用既有基礎設施,斷線也會自動重連。它的限制是單向、只能傳文字,而原生的 `EventSource` 只支援 GET 且無法自訂 header——這就是為什麼 LLM 串流實務上多半改用 fetch 加 POST 自行解析同樣的格式。

相關術語: WebSocket (單向輕量版)、polling (取代對象)

出處:第 17 段「Q15 LLM 聊天機器人用哪種即時協定」

留給下一段 總結收束。

4. 總結

整支影片其實只有一個主張:資深不在於知道結論,而在於答案裡看得見一條可驗證的推理鏈。它先用一顆 hover 會放大的按鈕示範這條鏈能推到多深——從 reflow 推到 compositor thread,再推到「能繞過 main thread 就繞過」。接著把視角從單一 bug 拉到工作流:與其事後修,不如先用 plan mode、design system 與結構性約束限制 AI 的產出空間;限制之後還要能持續檢查,於是品質被切成 static 與 dynamic 兩側,static 因為成本固定而優先,dynamic 則要靠 testing pyramid 避免掉進「只寫 e2e」的陷阱。談測試時反覆出現的判準「找一個 bug 要燒多少 token」,直接把成本抬成架構變數,於是問題被翻譯回老問題——用最少的改動交付功能,也就是標準化與解耦。標準化展開成 design system 的四層,而 design system 保證不了版面在不同螢幕上不壞,於是回到最底層:box model 決定一個元素佔多大,specificity 決定哪條規則說了算,響應式三支柱決定版面怎麼適應。CSS 這一側收完,卡頓的另一半交給 event loop——單執行緒、兩個佇列、與 rendering 共用同一個 CPU,這條結構同時解釋了打字卡頓的線上 bug 與 React 該怎麼優化。最後兩題把尺度拉到最大:擴展的關鍵不是「用 CDN」而是怎麼依變動頻率切 bundle 分開快取,而即時協定的選擇則要看資料流的形狀——LLM 的通訊是不對稱的,所以 SSE 而不是 WebSocket。整條鏈收束成一個循環:越懂底層,越能給 AI 正確的邊界;邊界越好,改動越少;改動越少,成本越低。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
2. Q1 hover 放大:reflow 還是 compositor
1:36
「Because CSS transitions they bypass the reflow and they go straight to what we call the compositor thread.」繞過 reflow 的是特定屬性,不是 transition 這個機制本身。只有可合成的屬性(實務上主要是 transform 與 opacity,以及 filter)能直接交給 compositor;`transition: width` 或 `transition: top` 一樣會每格觸發 layout 與 paint。他後面舉的例子(scale)剛好是對的,但把理由歸給「transitions」會讓人誤以為只要寫成 transition 就便宜。
依據: CSS Triggers / Chrome 渲染管線文件:layout → paint → composite,僅 transform、opacity 等屬性可跳到 composite
8. Q7 CSS box model 與 box-sizing
23:02
「However, the default of the browser is extrinsic width.」說反了。瀏覽器的預設是 box-sizing: content-box,而尺寸來源的預設是 intrinsic(由內容或可用空間決定),不是 extrinsic。extrinsic 指的正是你自己寫死 width 的那種情況。他後面描述的行為(瀏覽器先算內容寬、再把 padding 加上去)其實就是 content-box 的行為,講法與用詞在這裡對不上。
依據: CSS Box Sizing Level 3:初始值 box-sizing: content-box;intrinsic sizing 指 max-content / fit-content 等由內容決定的尺寸
11. Q10 event loop 的完整運作
32:55
「And we don't immediately pick the microtask.」他描述成「執行一個 microtask 後就停下來去做 rendering,下一輪才取下一個」。實際規格是:每個 macrotask 結束後,microtask queue 會被**整個排乾**(包含執行過程中新增的),然後才輪到 rendering。這個差別有實際後果——不斷產生 microtask 的迴圈會讓畫面永遠不更新,而同樣的工作排成 macrotask 則不會。
依據: HTML Living Standard, event loop processing model:perform a microtask checkpoint 在每個 task 之後、update the rendering 之前
10. Q9 響應式設計的三根支柱
28:50
「So, you never want to use pixels, for example.」太絕對。該用相對單位的是會隨使用者字級設定或容器變動的尺寸(字級、間距、元素寬度);border 寬度、細線、某些圖示尺寸用 px 反而正確,因為它們不該跟著字級放大。真正的問題不是 px 本身,而是把應該跟著容器或字級走的尺寸寫死。
依據: WCAG 1.4.4 要求文字可縮放至 200%,指向字級用相對單位;但並未要求所有長度都用相對單位

5. 推薦三個下一步

1. 往下挖深:瀏覽器渲染管線與效能量測

影片把 reflow、compositor thread、critical rendering path 都當成已知前提快速帶過,但沒有示範怎麼在 DevTools 裡實際看到它們。補上這一塊,前面關於 transform、re-render、載入速度的判斷才能被自己驗證,而不是背下來的結論。

YouTube 搜尋:browser rendering pipeline layout paint composite Chrome DevTools performance panel tutorial Core Web Vitals INP LCP debugging

2. 往旁邊對照:現代 CSS 的版面新工具

作者的響應式三支柱停在 rem / flex / breakpoint,還把 grid 說成需要大量 media query。但 container queries、subgrid 與 auto-fit 這些能力已經改變了「元件如何自我響應」的答案,跟他自己在 design system 那一段的目標其實更契合。

YouTube 搜尋:CSS container queries tutorial CSS grid auto-fit minmax responsive without media queries modern CSS layout 2026

3. 往上應用:把 LLM 串流真的接進前端

最後一題只講到「用 SSE」就結束,但實作上最卡的地方在後面——原生 EventSource 不能 POST、要用 fetch 自己解析串流、還要處理中止、重連與 token 的漸進渲染。實際做一次,前面關於 event loop 與 re-render 的知識會全部用上。

YouTube 搜尋:streaming LLM response fetch ReadableStream SSE AbortController cancel streaming request React Vercel AI SDK streaming chat tutorial

📄 全部影片 · 主題區: 前端底層與系統設計