題目與適用情境
一個產品動態消息最多有 100,000 筆邏輯資料。游標 API 每次回傳 50 筆。文字換行、圖片載入或詳細內容展開後,單張卡片高度可能在 48 到 240 像素之間。使用者可以篩選動態消息、展開卡片並填寫行內回覆草稿。鍵盤和螢幕閱讀器使用者需要連續瀏覽,焦點與位置資訊都不能遺失。
請設計渲染區間、動態高度測量、增量載入、狀態歸屬、捲動錨定、焦點行為、無障礙語意和驗證方案,並說明何時應改用更簡單的分頁或一般清單。
100,000 筆、每頁 50 筆和高度範圍都是面試假設,不是通用產品指標。這題適合資深前端、UI 基礎設施和 Web 效能職位,核心考察瀏覽器渲染與互動設計,因此歸類為 frontend。
面試官考察重點
第一,候選人能否拆開四個容易混淆的職責:分頁決定用戶端已載入哪些紀錄;虛擬化決定哪些已載入紀錄擁有 DOM 節點;穩定識別決定狀態屬於哪筆業務紀錄;無障礙設計決定局部 DOM 如何表達完整邏輯集合。四者需要分別設計。
第二,候選人能否從列高限制推導區間演算法。等高列可以直接透過偏移量計算;動態高度需要估計值、實測值、累計偏移,以及估計修正後的補償策略。高品質回答應指出「視窗上方內容變高導致捲動跳動」,並保住一個邏輯錨點。
第三,候選人能否避免狀態串列。被重複使用的可視槽位不代表業務紀錄。草稿、選取、展開和待完成變更都需要穩定紀錄 ID,並由能跨越資料列卸載的狀態容器持有。排序、篩選和過期分頁回應也不能讓資料或狀態繼續綁定舊索引。
第四,候選人能否保住無障礙能力。只掛載可見卡片後,大部分邏輯集合都不在 DOM 中。高品質回答會保留原生或動態消息語意,公開邏輯位置與集合大小,正確維護忙碌狀態,保留含焦點的卡片,並提供可操作的載入方式。
第五,候選人能否驗證行為,而不是只承諾「捲動順暢」。回答應先定義目標裝置與情境,再測量掛載節點數、影格與長任務、版面配置位移或錨點誤差、重複請求、記憶體、焦點保留以及螢幕閱讀器輸出。
回答前需要釐清的問題
- 一般清單是否真的達不到效能目標? 如果產品最多顯示數十筆簡單資料,一般渲染或分頁更容易維護,也更容易保障無障礙。先測量,再增加視窗化。
- 列高是固定、有限幾種,還是任意變化? 固定高度允許常數時間區間計算;動態高度需要估計與測量。沒有限制的嵌入內容或延遲媒體會增加捲動修正難度,可能需要約束卡片版面。
- API 是否回傳總數,還是只有下一頁游標? 已知總數可以表達完整邏輯集合大小;未知總數會改變捲動條預期和無障礙中繼資料。游標分頁也代表未載入索引不能隨機存取。
- 使用者是否必須跳到任意資料? 大量資料列尚未測量時,按索引跳轉只能先到估計位置。按紀錄 ID 跳轉可能需要伺服器定位 API 或另一種分頁能力。
- 哪些資料列狀態必須跨卸載保留? 暫時懸停可以捨棄;回覆草稿、選取、展開、驗證錯誤和樂觀更新通常應放在資料列元件外,並以紀錄 ID 為鍵。
- 適用哪種鍵盤模型和語意? 閱讀型動態消息、可選取清單方塊和可編輯網格擁有不同角色與鍵盤約定。互動方式決定語意模式,視覺版面不能取代這個判斷。
- 資料變化時應遵循什麼捲動約定? 篩選後通常回到頂端;向前插入歷史資料時通常保留目前第一筆可見紀錄;即時附加時只有使用者原本就在尾端才自動跟隨。
- 效能驗收目標是什麼? 需要說清目標裝置、資料形態、捲動動作和預算。桌面環境中的短佔位列不能代表行動裝置上含圖片與控制項的動態消息。
30 秒回答框架
「我會把資料載入、虛擬化和資料列狀態分開。游標載入器每次取得 50 筆,並負責去重;虛擬化器只渲染可見區間和少量緩衝列,把這些列定位在代表已載入資料總高度的容器裡。固定列高直接用偏移量計算,動態列高先估計、版面完成後測量,再更新累計偏移。視窗上方高度變化時,我會保留第一筆可見紀錄及其列內偏移,防止畫面跳動。每列使用紀錄 ID,草稿和選取狀態放到資料列外。活動動態消息會使用 feed 與 article 語意,表達邏輯位置、集合大小和忙碌狀態,並且絕不卸載含焦點的卡片。我會測試快速捲動、圖片與展開高度變化、篩選競態、前插和附加、鍵盤與螢幕閱讀器導覽、節點數量、長任務、記憶體和錨點誤差。如果一般渲染已達標,我會保留更簡單的清單。」
分步深入解答
第一步:區分已載入紀錄與已掛載資料列
資料層維護明確狀態:有序紀錄 ID、按 ID 索引的紀錄、下一頁游標、請求狀態和是否還有更多資料。虛擬化器接收有序的已載入 ID 與捲動容器,不負責游標、重試、草稿或伺服器端變更。
游標 API 初始只允許存取 50 筆紀錄。虛擬化已載入前綴,並隨著新頁到達增加邏輯高度。除非 API 支援按索引存取,或回傳帶穩定位置的佔位資料,否則不能假設第 73,421 筆已在本機。可見區間尾端接近已載入數量時可以觸發請求,但載入器仍負責合併請求和識別資料尾端。
這層分離能防住兩類常見錯誤:無限載入持續保留舊頁 DOM 節點;虛擬化器每次重算小區間時重複發出同一請求。
第二步:先推導固定列高區間,再處理動態高度
等高列可以透過 scrollTop、視窗高度和列高得到第一筆與最後一筆可見索引。緩衝區向兩端擴展,再限制在已載入數量內。一個具有完整高度的內部畫布產生捲動條,掛載資料列根據邏輯偏移定位。
function getFixedRange({
scrollTop,
viewportHeight,
rowHeight,
loadedCount,
overscanRows,
}) {
const visibleStart = Math.floor(scrollTop / rowHeight);
const visibleEnd = Math.ceil(
(scrollTop + viewportHeight) / rowHeight,
);
return {
start: Math.max(0, visibleStart - overscanRows),
end: Math.min(loadedCount, visibleEnd + overscanRows),
totalHeight: loadedCount * rowHeight,
};
}渲染從 start 開始、到 end 之前結束的左閉右開區間。索引為 i 的資料列從 i * rowHeight 開始。根據捲動容器更新區間,讓框架批次渲染,並避免在一次捲動回呼中反覆交替讀取版面配置和寫入樣式。
設計能夠約束為統一列高或少量已知規格時,這個方案更合適。它可預測、成本低,也容易恢復捲動位置。不能因為元件庫提供動態測量,就預設引入動態測量。
第三步:測量動態高度並維護累計偏移
動態列高把乘法換成尺寸模型。未測量列先使用符合真實卡片設計的估計高度。實測高度按紀錄 ID 保存,不能綁定到被重複使用的 DOM 槽位。某筆紀錄的起始偏移等於此前尺寸總和,因此查找第一筆可見紀錄需要在累計偏移中搜尋。
簡單前綴陣列容易解釋,但一列高度變化後可能需要更新許多後續偏移。成熟虛擬化函式庫或帶索引的前綴和結構可以降低搜尋與更新成本。面試回答的關鍵不變量是:每個渲染列的邏輯起點都來自同一份尺寸儲存,畫布總高度等於所有實測或估計尺寸總和。
資料列完成版面配置後,用 ResizeObserver 或虛擬化函式庫的測量掛鉤讀取尺寸。能提前取得圖片寬高時先保留空間;否則圖片載入會產生一次新測量。忽略尺寸未變化的通知,批次套用修正,並在資料列卸載時解除觀察。容器寬度、字型指標、展開狀態或內容變化可能改變高度時,需要重新測量。
估計出現誤差是正常情況。更新偏移時缺少明確「使用者應繼續看到哪一處」的策略,才會造成失敗。
第四步:保留邏輯捲動錨點
套用尺寸變更前,記錄第一筆可見紀錄的 ID,以及其頂端到視窗頂端的距離。更新測量值與累計偏移後,找到這筆紀錄的新起點,再調整捲動偏移,讓同一個列內位置留在原本的視覺位置。
錨點上方某一列增加 30 像素時,捲動偏移通常也要增加 30 像素;錨點下方變化則不需補償。使用者正在拖動捲動條,或產品明確導覽到另一筆資料時,應根據互動約定延遲或略過補償。
瀏覽器會為一般版面變化執行捲動錨定。虛擬化器常使用合成畫布與絕對定位資料列,瀏覽器的原生行為未必能保住所需邏輯紀錄。必須實測兩者組合。一個容器裡的補償由瀏覽器或虛擬化器其中一方負責;手動錨定作為權威策略時,應在該容器關閉原生錨定,避免重複修正。
向前插入歷史紀錄也使用同一規則:先擷取目前錨點,插入並測量新紀錄,再恢復錨點。即時附加紀錄時,只有使用者原本處於產品定義的「最新位置」門檻內才跟隨尾端;其餘情況顯示「有新動態」操作,不主動移動視窗。
第五步:根據單列工作量和捲動行為調整緩衝區
緩衝區透過預先掛載視窗外的資料來隱藏渲染延遲。過少可能在快速捲動時露出空白;過多會重新引入虛擬化原本要消除的 DOM、版面配置、記憶體和副作用成本。動態高度情境中,按像素設定緩衝距離通常比固定列數穩定,因為五筆緊湊列和五筆展開列代表的距離與工作量不同。
從較小值開始,在目標裝置上記錄效能軌跡,再根據證據調整。需要考慮捲動方向與速度、單列渲染成本、圖片解碼和框架更新延遲。緩衝區不能取代昂貴資料列內容的快取、媒體尺寸保留、過期副作用取消或同步版面讀取削減。
維護嚴格的節點數不變量:掛載資料列只包括可見區間、緩衝區和少量明確固定的資料列,例如目前焦點列。記憶體分析應證明離開該集合的舊資料列元件、觀察器和事件監聽器能夠被回收。
第六步:載入分頁時處理競態與重複紀錄
從邏輯區間尾端觸發載入,避免依賴可能被回收的哨兵節點。每個游標最多同時存在一個請求。記錄每次請求對應的游標,按穩定 ID 對回傳紀錄去重,並在伺服器表示沒有下一游標時停止。
篩選或排序會建立新的查詢世代。能取消舊請求時就取消,並拒絕世代不符合的延遲回應。清空有序 ID 和游標,根據產品約定重設捲動位置,同時清理依賴版面或內容的測量值。新順序重複使用舊偏移會把正確高度貼到錯誤位置。
載入佔位需要穩定的邏輯含義。一個「下一頁載入中」佔位不能冒充 50 筆無障礙紀錄。如果 API 給出可按索引存取的已知總量,產品也決定使用逐列佔位,才能為它們分配穩定位置並在替換時保持識別。始終顯示重試與尾端狀態;請求失敗後留下永久空白尾端,會讓使用者誤以為已經到底。
第七步:讓紀錄識別和重要狀態脫離重複使用的資料列
框架 key 與虛擬化器 item key 都使用紀錄的穩定 ID。分頁前插、篩選重排或去重發生時,陣列索引不再安全。一個重複使用的槽位此刻顯示紀錄 A,之後可能顯示紀錄 B;綁定在槽位上的狀態會把草稿、勾選或樂觀結果帶到另一筆紀錄。
把需要保留的 UI 狀態存成以紀錄 ID 為鍵的映射:草稿文字、展開、選取、驗證狀態和待完成變更。資料列掛載後讀取自己的狀態片段,並透過能跨卸載存在的持有者寫回。只有明確允許捨棄的展示細節才適合放在資料列內區域狀態。
也不能永久保存所有暫時值。產品捨棄一個查詢時清理相關狀態,限制草稿保留範圍,並讓樂觀狀態與伺服器結果核對。目標是按業務識別歸屬狀態,不是建立無限成長的用戶端快取。
第八步:把無障礙設計納入區間演算法
根據互動選擇語意。閱讀型動態消息可以為容器使用 feed 語意,每張卡片使用帶可存取名稱的 article 語意。每個掛載 article 表達從 1 開始的邏輯位置;總數已知時表達邏輯總量,未知時使用該模式規定的未知集合大小。多步更新期間將動態消息標記為忙碌,完成或失敗後都必須清除該狀態。
不能卸載包含 DOM 焦點的元素。即使焦點紀錄落在一般緩衝區外,也將它加入掛載區間;或者在回收前透過明確導覽動作移動焦點。鍵盤命令前往下一筆邏輯紀錄時,先確保它已載入並掛載,捲入視窗後再聚焦。自訂按鍵需要清楚說明,並提供可靠方式移動到動態消息之前和之後。
虛擬化還會影響頁面內尋找、瀏覽器文字選取、列印和螢幕閱讀器瀏覽,因為未掛載內容不在 DOM 中。需要逐項確認產品需求。搜尋可能需要伺服器端控制項;列印可以使用獨立分頁檢視;明確「載入更多」按鈕可以作為自動載入之外的可操作路徑。
最後必須用純鍵盤和真實螢幕閱讀器測試。檢查屬性只能證明標記存在,不能證明焦點、播報、載入與導覽能夠協同運作。
第九步:驗證故障矩陣,不只看一次順暢捲動
準備確定性測試資料,涵蓋短長文字、帶尺寸的延遲圖片、展開、缺少媒體和重複 ID。執行慢速與快速滾輪捲動、類似觸控的慣性捲動、拖動捲動條、視窗縮放、頁面縮放、字型載入、請求進行中的篩選、前插、附加、載入失敗、重試和資料尾端。
記錄全程的掛載資料列與 DOM 節點數量。用效能軌跡檢查影格、長任務、指令碼、版面配置、繪製和意外的重複測量。捲動經過多頁後檢查峰值記憶體與保留記憶體。比較高度變化前後的頂端邏輯錨點,並斷言游標與紀錄 ID 沒有重複。
互動測試中,把焦點放入一列,再用指標和鍵盤捲動、展開其上方的資料列、載入下一頁並切換篩選條件。確認焦點落點,以及草稿是否仍屬於正確 ID。使用螢幕閱讀器確認卡片名稱、邏輯位置與集合大小、忙碌狀態切換、前後導覽、重試和尾端狀態。
驗收目標應來自產品與目標裝置。報告實測軌跡和不變量是否遭破壞,不能宣稱虛擬化天然保證某個影格率。
高品質示範回答
「我會把動態消息分成三層。資料層持有有序 ID、紀錄、下一游標、請求世代和去重;虛擬化器只接收已載入 ID,負責可見區間、測量和定位;草稿、展開、選取與變更狀態放在以紀錄 ID 為鍵的映射裡,因此資料列卸載不會刪除或移動它們。
固定列高時,可見區間直接透過偏移量計算,畫布高度等於已載入數量乘以列高。這些卡片會在 48 到 240 像素之間變化,所以我會先估計、再測量掛載卡片,並維護累計偏移。第一筆可見紀錄及其列內偏移構成捲動錨點。錨點上方測量值變化時,我按差值修正捲動偏移。一個容器只選擇一種補償責任方,避免原生與手動捲動錨定同時調整。
渲染區間包含少量經過測量的緩衝距離和目前焦點卡片。區間尾端接近已載入數量時,載入器只請求一次下一游標。篩選變化會開啟新世代,盡可能取消舊請求,拒絕延遲結果,並重設順序與測量值。紀錄 ID 作為 item key,索引不持有任何業務狀態。
無障礙方面,這個活動流使用 feed 與 article 語意。掛載 article 表達邏輯位置與已知總量,分頁插入期間動態消息表達忙碌狀態,隨後清除。我不會卸載含焦點的 article。鍵盤導覽會先載入並掛載下一筆邏輯紀錄,再移動焦點,同時保留明確的重試、尾端和載入更多路徑。
我會先確認一般渲染確實達不到目標預算,再用固定資料組合測試動態高度、延遲媒體、展開、快速捲動、縮放、篩選競態、前插、附加和網路失敗。記錄掛載節點數、長任務、版面配置與繪製、保留記憶體、錨點誤差、重複請求與 ID、草稿識別、鍵盤焦點和螢幕閱讀器輸出。如果分頁一般清單滿足相同要求,我會選擇它,因為正確性與無障礙故障面更小。」
常見錯誤
- 把無限載入當成虛擬化 → 舊分頁仍持續增加 DOM 節點,捲動越遠效能越差 → 讓掛載區間獨立於已載入資料,並保持有界。
- 使用陣列索引作為 item key → 前插、篩選和去重會把草稿與選取狀態移動到其他紀錄 → 按穩定紀錄 ID 管理資料列和外部狀態。
- 把平均高度當成精確高度 → 偏移誤差持續累積,最終出現空白區間或跳動 → 測量動態資料列,並只維護一份累計尺寸模型。
- 修改測量值時沒有錨點 → 視窗上方內容改變後,使用者正在閱讀的位置被推走 → 保留第一筆可見紀錄及其列內偏移。
- 讓原生與手動錨定同時補償 → 視窗可能移動兩次 → 實測容器行為,並只指定一個補償責任方。
- 固定多渲染大量緩衝列 → 展開卡片重新帶來沉重 DOM 與版面工作 → 按目標裝置上的像素距離和渲染成本調整。
- 每次區間更新都發出請求 → 重複渲染會請求同一游標並交錯合併結果 → 按游標合併請求,並拒絕過期查詢世代。
- 把草稿留在資料列元件中 → 卸載會刪除草稿,槽位重複使用還可能串到其他紀錄 → 把重要狀態提升到按 ID 管理的持有者。
- 卸載焦點所在資料列 → 鍵盤與輔助技術脈絡突然消失 → 固定焦點項目,或在回收前明確移動焦點。
- 只播報已掛載子集的位置 → 螢幕閱讀器使用者得到錯誤的集合大小和順序 → 表達邏輯位置與已知或未知的邏輯集合大小。
- 設定忙碌狀態後不清除 → 輔助技術可能一直看不到已完成更新 → 所有成功和失敗路徑都清除忙碌狀態。
- 只最佳化影格率 → 焦點、草稿、競態、記憶體與捲動位置仍可能錯誤 → 同時驗證效能與互動不變量。
追問與應對
追問一:如果每列都嚴格是 56 像素高,方案如何簡化?
刪除動態測量及其修正路徑。根據 56 像素直接計算區間與偏移,透過乘法得到已載入總高度,用索引加列內偏移恢復位置。穩定 ID、載入競態、焦點和無障礙處理仍需保留;固定高度只簡化幾何計算,不解決識別與互動。先驗證效能,無需引入限制已經不需要的前綴和結構。
追問二:游標 API 無法回傳總數時怎麼辦?
只虛擬化已載入前綴,新頁到達後增加畫布高度。不能虛構未載入索引或最終捲動條長度。使用動態消息模式規定的未知集合大小,播報載入與尾端狀態,並保留明確載入更多或重試控制項。伺服器沒有按索引查詢、紀錄定位或穩定估計約定時,任意按索引跳轉就不可用。
追問三:使用者正在中間閱讀,系統向前插入歷史紀錄,如何避免跳動?
插入前記錄第一筆可見紀錄 ID 及其相對視窗偏移。加入歷史 ID,估計或測量新資料列並重算累計偏移,然後修正捲動偏移,讓擷取紀錄回到原視覺位置。分頁邊界按 ID 去重。媒體稍後載入繼續改變高度時,對錨點上方差值套用同一規則。
追問四:使用者拖動捲動條後,焦點卡片距離目前位置已有 5,000 筆,還要一直掛載嗎?
先定義互動約定。固定一張焦點卡片的 DOM 成本有界,但焦點留在離視窗很遠的元素上也會讓使用者困惑。明確的捲動條跳轉過程中,可以把焦點移動到動態消息容器或新位置的可見卡片並給出播報,再解除舊卡片固定。一般指標捲動或測量變化不能悄悄銷毀焦點。最終行為需要與鍵盤和螢幕閱讀器使用者共同驗證。
追問五:產品要求瀏覽器頁面內尋找並列印全部 100,000 筆,單一虛擬 DOM 能滿足嗎?
局部 DOM 無法讓瀏覽器尋找尚未掛載的文字,也無法把它們作為一般頁面內容列印。把它們拆成獨立需求:提供伺服器端搜尋,找到紀錄後跳轉並掛載;提供帶明確上限的分頁匯出或列印檢視。暫時掛載 100,000 張互動卡片只會凍結頁面。
追問六:如何測試虛擬化器,避免只依賴截圖?
先用確定輸入驗證純固定區間和累計偏移不變量,再在真實瀏覽器做整合情境。斷言掛載 ID 涵蓋可見區間與緩衝區,總尺寸符合尺寸模型,測量後邏輯錨點誤差處於約定範圍,每個游標只請求一次,草稿在重排後仍跟隨 ID。還要加入鍵盤與螢幕閱讀器檢查,因為 DOM 幾何斷言無法驗證焦點和播報。