題幹與適用場景
一個同源資料分析後台有三項互相獨立的需求:
- 在本機解析使用者選擇的 200 MiB CSV,同時保持輸入與畫面繪製流暢;
- 多個分頁在至少一個分頁仍開啟時,共用一條 WebSocket 與記憶體中的訂閱狀態;
- 斷線時仍能開啟應用程式外殼與最近一次成功報表,並將一次儲存請求排入佇列,連線恢復後重試。
請分別在 Dedicated Worker、SharedWorker 與 Service Worker 中作出選擇,並說明擁有者、生命週期、通訊、持久化、取消與失敗處理、瀏覽器相容性降級、安全邊界及驗證方案。
200 MiB、單一 WebSocket 與離線儲存都是面試題設,不是通用產品指標。本題適合資深前端、Web 平台與前端架構職位。核心能力是瀏覽器執行模型與 Web 平台選型,因此歸類為 frontend。
面試官考察點
第一,能否依「誰擁有任務、需要存活多久、要影響哪些頁面」選型,而不是把所有背景執行緒都稱為 Web Worker。好的回答會明確說明:頁面擁有的運算交給 Dedicated Worker;同源多頁面共用的短期執行狀態可交給 SharedWorker;作用域內的網路代理、快取與事件驅動背景任務交給 Service Worker。
第二,能否區分執行緒與持久化。Worker 讓程式碼離開主執行緒,不會自動降低 CPU、記憶體或 I/O;SharedWorker 的記憶體狀態也不是持久資料;Service Worker 隨時可能在事件之間被瀏覽器終止,待重試操作必須寫入 IndexedDB 等持久儲存。
第三,能否量化訊息傳遞成本。結構化複製通常會複製資料;ArrayBuffer 可作為 transferable 轉移擁有權,避免再複製一份,但傳送端的 buffer 會被分離。大型 CSV 還要考慮分塊、進度、背壓與取消。
第四,能否提出可運作的降級路徑。SharedWorker 在目前最新瀏覽器已進入 Baseline 2026,但舊裝置或部分實作仍可能不支援;Background Sync 仍不是 Baseline。方案不能把正確性綁在單一選用 API 上。
第五,能否驗證生命週期故障。只在正常頁面點通一次不夠;還要涵蓋重新整理、關閉最後一個分頁、Service Worker 更新、離線重啟、重複同步、快取污染與舊瀏覽器降級。
回答前需要釐清的問題
- CSV 解析是一次性還是持續發生? 一次性任務可按需建立一個 Dedicated Worker;持續並行任務要限制 worker pool 大小並建立佇列,避免每個檔案啟動一個執行緒。
- 輸入必須整體讀入記憶體嗎? 若解析器支援串流或分塊輸入,應優先逐塊處理;若必須一次轉移完整
ArrayBuffer,要分別為原始位元組、解碼字串、解析結果與索引估算尖峰記憶體。 - 多個分頁是否嚴格同源? SharedWorker 要求協定、主機與連接埠完全相同。跨子網域、嵌入第三方 iframe 或不同連接埠時,必須重新設計通訊邊界。
- 「共用一條 WebSocket」是最佳化還是正確性要求? 若只是減少連線數,可降級為每頁一條連線;若伺服器只允許單一工作階段,就需要 BroadcastChannel 加上領導者選舉、租約與腦裂復原等更嚴格的備援方案。
- 離線儲存允許背景自動重試嗎? 若操作敏感或可能衝突,應在頁面恢復後讓使用者確認;若允許自動重試,請求必須有冪等鍵、持久狀態與有界退避。
- 目標瀏覽器包含哪些舊版本與隱私模式? 這會決定 SharedWorker、Background Sync、儲存配額與 Service Worker 的能力偵測和降級範圍。
30 秒回答框架
「我先依擁有者與生命週期拆解三項需求。200 MiB CSV 是目前頁面擁有的 CPU 密集任務,使用 Dedicated Worker;分塊解析並回報進度,能轉移 ArrayBuffer 時使用 transferable,取消時傳訊息或終止 worker。跨同源分頁的一條 WebSocket 使用 SharedWorker,各頁面透過 MessagePort 訂閱;但先做能力偵測,降級為 BroadcastChannel 加領導者選舉,必要時允許每頁連線。離線外殼、報表快取與稍後重試使用 Service Worker,因為它能攔截作用域內請求並回應背景事件。佇列寫入 IndexedDB,請求帶冪等鍵;Background Sync 不可用時,在頁面恢復連線或啟動時清空佇列。最後專門測試關閉最後分頁、離線重啟、重複同步與 Service Worker 更新。」
分步驟深入解答
第一步:先用擁有者、作用域與生命週期做選擇
| 需求 | 選擇 | 擁有者與通訊 | 關鍵失效點 | |---|---|---|---| | 目前頁面解析大型 CSV | Dedicated Worker | 建立它的頁面;postMessage | 頁面關閉、取消、記憶體尖峰 | | 同源多分頁共用連線 | SharedWorker | 同源頁面;每頁一個 MessagePort | 最後引用關閉、舊瀏覽器不支援 | | 離線外殼、網路代理、稍後同步 | Service Worker | 註冊的 origin 與 path scope;事件和 client 訊息 | 更新等待、事件間被終止、選用 API 不支援 |
三種 Worker 都不能直接操作 DOM。Dedicated Worker 與 SharedWorker 適合執行頁面所需的 JavaScript 工作;Service Worker 是註冊在來源與路徑上的事件驅動網路代理。它能處理 fetch、快取、Push 和支援瀏覽器中的背景同步,卻不適合承載必須連續執行數分鐘的 CSV 解析。
第二步:用 Dedicated Worker 隔離 200 MiB CSV 解析
頁面按需建立 Dedicated Worker,因為任務只服務目前頁面。主執行緒負責選擇檔案、顯示進度與繪製結果;worker 負責解碼、詞法解析、型別推斷與彙總。它沒有 DOM 存取權,結果必須透過訊息傳回。
以下是介面草圖,省略了解析器實作和錯誤型別。chunkBytes 表示 worker 內部以 4 MiB 批次推進,不表示主執行緒又複製一次資料:
const parser = new Worker(
new URL("./csv-parser.worker.js", import.meta.url),
{ type: "module" },
);
const jobId = crypto.randomUUID();
const buffer = await file.arrayBuffer();
parser.postMessage(
{ type: "parse", jobId, buffer, chunkBytes: 4 * 1024 * 1024 },
[buffer],
);
parser.onmessage = ({ data }) => {
if (data.jobId !== jobId) return;
if (data.type === "progress") renderProgress(data.rows);
if (data.type === "done") renderReport(data.summary);
};
cancelButton.onclick = () => {
parser.postMessage({ type: "cancel", jobId });
};取消分為協作式與強制式。解析迴圈在分塊邊界檢查取消標記,可以釋放暫存物件並回傳狀態;worker 卡死或解析器無法中斷時呼叫 terminate(),代價是失去該 worker 中的所有任務。因此持續處理多個檔案時,不應盲目共用一個沒有任務隔離的 worker。
第三步:把訊息複製與尖峰記憶體納入設計
一般 postMessage 使用結構化複製。依簡化假設,200 MiB ArrayBuffer 在傳送端與接收端同時保留一份時,僅原始位元組就約占 400 MiB;這還不包含 File 的底層儲存、字串解碼、資料列物件、索引與垃圾回收餘量。這個數字是題設推導,不是瀏覽器記憶體承諾。
把 ArrayBuffer 放入 transfer list 後,記憶體資源會移到 worker,傳送端 buffer 被分離,可避免這份位元組複製。若頁面還需要原始資料,就不能在未規劃擁有權時直接轉移。更穩健的方案是從 Blob.stream() 或切片持續傳送小區塊,worker 回傳增量統計;主執行緒限制傳輸中的區塊數量,收到消費確認後再傳送下一塊,形成背壓。
共享記憶體不是預設捷徑。SharedArrayBuffer 需要 cross-origin isolation,並引入原子操作、資料競爭與更嚴格的安全部署要求。CSV 解析尚未證明一般訊息與 transferable 已成為瓶頸前,不應增加這層複雜度。
第四步:用 SharedWorker 維護同源多分頁連線
SharedWorker 適合「至少一個同源頁面仍連線時,保留一個共用執行個體」的要求。每個分頁連接同名、同 URL 的 worker,並透過自己的 MessagePort 註冊訂閱。worker 持有 WebSocket、port 集合與記憶體訂閱表,再把伺服器訊息路由給相關 port。
const socketHub = new SharedWorker(
new URL("./socket-hub.shared-worker.js", import.meta.url),
{ type: "module", name: "analytics-socket-hub" },
);
socketHub.port.start();
socketHub.port.postMessage({ type: "subscribe", reportId });
socketHub.port.onmessage = ({ data }) => applyLiveUpdate(data);
window.addEventListener("pagehide", () => {
socketHub.port.postMessage({ type: "disconnect", reportId });
socketHub.port.close();
});SharedWorker 只允許精確同源的環境存取。只要仍有開啟頁面持有引用,它可以繼續存活;最後一個引用關閉後,不能把它視為永久背景服務。連線中斷、瀏覽器結束或 worker 當機後,記憶體訂閱表可以消失,因此伺服器游標或必須復原的訂閱要寫入持久儲存,或由頁面重新宣告。
第五步:為 SharedWorker 設計真實降級
先偵測 SharedWorker。不支援時,可讓各分頁透過 BroadcastChannel 交換心跳與事件,並用 Web Locks 或帶到期時間的儲存租約選出一位領導者持有 WebSocket。領導者關閉後,其他頁面重新選舉;伺服器以工作階段與事件 ID 去重,用戶端用單調 epoch 拒絕舊領導者訊息,降低腦裂影響。
這套備援方案比 SharedWorker 複雜。如果「單一連線」只是成本最佳化,最簡單且正確的降級是每個分頁各開一條 WebSocket,並由伺服器處理冪等與連線限制。只有伺服器硬性約束確實要求單一連線,才值得實作選舉、租約續期、失聯偵測與故障注入。
第六步:用 Service Worker 處理離線與重試
Service Worker 註冊在 origin 與 path scope 上,可攔截受控頁面的請求。應用程式外殼適合 cache-first 或帶版本的預先快取;最近報表通常適合 network-first:連線時取得新回應並更新 Cache Storage,失敗時回傳最近成功快取。使用者儲存的資料佇列應放 IndexedDB,而不是 Cache Storage 或 service worker 全域變數。
以下仍是介面草圖。正式實作還要限定可快取 URL、驗證回應、管理快取版本,並在把儲存請求寫入 IndexedDB 後,才向使用者顯示「已排入佇列」:
navigator.serviceWorker.register("/sw.js", { scope: "/" });
// sw.js
self.addEventListener("install", (event) => {
event.waitUntil(cacheAppShell());
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/reports/")) {
event.respondWith(networkFirstReport(event.request));
}
});
self.addEventListener("sync", (event) => {
if (event.tag === "retry-report-saves") {
event.waitUntil(flushIndexedDbQueue());
}
});Background Sync 只在部分瀏覽器可用。註冊失敗或 API 不存在時,頁面在 online、啟動與回到前景時呼叫同一套佇列清理邏輯。網路狀態事件只能作為重試觸發器,成功仍以伺服器回應為準。每筆儲存帶冪等鍵、嘗試次數與下次重試時間;衝突回應進入人工處理,不能無限自動覆寫。
第七步:處理 Service Worker 生命週期與更新
Service Worker 經過下載、安裝、等待與啟用後才控制頁面;首次註冊後,目前文件通常要重新導覽才進入受控狀態。新版本可能完成安裝後等待舊頁面退出。直接 skipWaiting() 與 clients.claim() 會更快接管,但若舊頁面協定與新 worker 不相容,可能造成版本錯配,所以訊息與快取 schema 要帶版本。
瀏覽器可在事件之間終止 worker,再於下一個事件到來時重新啟動。event.waitUntil() 只把目前事件關聯的 Promise 延長到完成,不提供常駐程序保證。佇列、重試次數與冪等鍵必須持久化;同步處理應能從任一筆記錄重新開始,並能安全處理同一請求執行兩次。
Service Worker 只能在安全環境執行,HTTPS 是正式環境前提,localhost 是本機開發例外。預設 scope 受指令碼路徑限制。還要檢查 CSP 的 worker-src、快取是否包含使用者敏感回應、登出時的快取隔離,以及跨來源請求的 CORS 與憑證策略。
第八步:用故障矩陣驗證選擇
Dedicated Worker 測試 1 MiB、200 MiB、格式錯誤與取消;記錄主執行緒 long task、輸入延遲、解析吞吐、尖峰記憶體與取消後釋放時間。比較結構化複製、transferable 與分塊版本,選擇實際瓶頸最小的方案。
SharedWorker 測試兩個分頁同時訂閱、關閉領導頁面、關閉最後一個頁面、網路抖動、worker 例外和不支援 SharedWorker 的瀏覽器。驗收條件包含伺服器實際連線數、訊息不重不漏、重新連線後游標連續,以及降級模式不破壞正確性。
Service Worker 測試首次造訪、已受控造訪、離線重新整理、快取過期、新版本等待、強制關閉瀏覽器、重複 sync、儲存配額不足與 Background Sync 缺失。儲存端點用冪等鍵驗證「至少一次嘗試」不會變成重複寫入;快取測試要確認不同使用者的資料不會互相取用。
高品質示範回答
「我會先看任務由誰擁有、需要存活多久、是否要代理網路。200 MiB CSV 只服務目前頁面,而且是 CPU 密集解析,所以使用 Dedicated Worker。主執行緒只處理檔案選擇與 UI,worker 分塊解析、回報進度並在區塊邊界回應取消。一般訊息會結構化複製;如果完整 200 MiB buffer 在兩端各一份,題設下僅原始位元組就可能約 400 MiB,所以我會測試 transferable,把 ArrayBuffer 擁有權移交給 worker,或直接串流分塊並限制傳輸中數量。
跨分頁的一條 WebSocket 使用 SharedWorker。所有頁面必須精確同源,每頁透過 MessagePort 重新宣告訂閱,worker 只保存可重建的連線與路由狀態。它不負責持久化;最後一個頁面關閉、瀏覽器結束或 worker 當機後,狀態允許消失。上線前做能力偵測。若單一連線只是最佳化,降級為每頁連線;若是硬性約束,才使用 BroadcastChannel 加租約選主,並用 epoch 與事件 ID 處理腦裂和重複。
離線外殼、最近報表與稍後儲存使用 Service Worker。它在 scope 內攔截請求:應用程式外殼做帶版本的預先快取,報表採 network-first 與快取回退。儲存先用冪等鍵寫入 IndexedDB,再註冊背景同步。Background Sync 不可用時,在頁面啟動、回到前景或連線恢復時呼叫同一個清理器。Service Worker 可能在事件間被終止,因此不能把佇列放在全域變數;每次處理都從持久狀態復原,並用 waitUntil() 關聯目前事件。
最後我會分別驗收主執行緒 long task 與記憶體、真實 WebSocket 連線數與斷線復原、離線重啟與重複同步,還會測試 Service Worker 更新等待、舊瀏覽器降級、快取使用者隔離和 HTTPS/CSP 邊界。」
常見錯誤
- 把 Service Worker 當成長時間運算執行緒 → 瀏覽器可在事件之間或長任務期間終止它 → 頁面運算使用 Dedicated Worker,Service Worker 只處理事件驅動網路工作。
- 把 200 MiB 物件直接
postMessage後不測記憶體 → 結構化複製與解析結果可能同時占用大量記憶體 → 比較 transferable、分塊與串流方案,並測量尖峰。 - 認為開了 worker 就不會卡 → CPU、垃圾回收與訊息序列化仍有成本 → 測量主執行緒 long task、吞吐與訊息批次大小。
- 把 SharedWorker 記憶體當成可靠狀態 → 最後引用關閉或瀏覽器結束後狀態可消失 → 只存可重建執行狀態,關鍵游標與佇列持久化。
- 忽略精確同源限制 → 協定、主機或連接埠不同就不能共用 → 先畫清 origin 邊界,再決定是否需要伺服器協調。
- SharedWorker 不支援時直接複製複雜選主邏輯 → 業務可能只需要正確而非單一連線 → 先判斷單一連線是否硬性約束,可接受時降級為每頁連線。
- 把離線佇列放在 Service Worker 全域變數 → worker 重新啟動後任務遺失 → 先寫入 IndexedDB,再觸發可重複執行的佇列清理。
- 只依賴 Background Sync → 它仍不是所有主流瀏覽器都支援的 Baseline 能力 → 增加啟動、回到前景與連線恢復時的前景重試。
- 更新時無條件強制接管 → 舊頁面與新 worker 協定可能不相容 → 版本化訊息、快取與遷移,再決定是否跳過等待。
- 快取所有成功回應 → 私有資料可能跨帳號殘留或被錯誤重用 → 建立 URL 白名單、使用者隔離、登出清理與回應驗證。
追問及應對
追問一:CSV 成長到 2 GiB,行動裝置記憶體不足怎麼辦?
不再呼叫 file.arrayBuffer() 整體讀入。用 Blob.stream() 或 slice() 分塊,增量解碼時保留跨塊的半行,worker 只回傳彙總結果或欄式批次。主執行緒設定最大傳輸中區塊和確認機制形成背壓;解析器提供取消點。若業務必須保留所有資料列,寫入 IndexedDB 或分段檔案並按需分頁,不把完整物件圖留在 JS heap 中。還要設定裝置級上限與明確的拒絕提示。
追問二:產品要求不同子網域的頁面也只能有一條 WebSocket 怎麼辦?
SharedWorker 不能跨精確 origin 共用。可把即時連線集中到一個受控 origin,透過經過稽核的 iframe 與 postMessage 建立跨來源訊息橋,並嚴格驗證 targetOrigin、來源和訊息 schema;或把單一連線協調移到伺服器,讓各頁面保持獨立用戶端連線。前者增加安全與可用性耦合,後者增加伺服器連線數,應依硬性約束選擇,不能繞過同源政策。
追問三:SharedWorker 已是 Baseline 2026,還需要降級嗎?
需要依產品瀏覽器矩陣判斷。Baseline 2026 表示最新瀏覽器版本達到新的共同可用範圍,不代表舊裝置、企業凍結版本、隱私模式與嵌入環境全部支援。能力偵測成本很低;降級可先選擇每頁連線,只有連線數硬性約束成立才實作複雜選主。
追問四:Background Sync 可能重複執行儲存,如何保證不重複寫入?
用戶端為業務操作產生穩定冪等鍵,佇列狀態至少包含 payload、鍵、嘗試次數與下次重試時間。伺服器以使用者與冪等鍵建立唯一限制或結果記錄:相同請求再次到達時回傳原結果。用戶端只有收到確認才刪除佇列項目;逾時視為結果未知並重試,不能產生新鍵。業務版本衝突進入待處理狀態,不做靜默覆寫。
追問五:新 Service Worker 已安裝,但使用者一直不關舊頁面怎麼辦?
先把新版本狀態通知受控頁面,讓使用者在安全點重新整理。若安全修正必須盡快接管,可以使用 skipWaiting() 與 clients.claim(),前提是新舊頁面與 worker 的訊息協定向後相容,快取和 IndexedDB 遷移可重入。發布驗證要同時執行舊頁面與新 worker 的組合;無法保證相容時就保留等待階段,不能只追求立即啟用。
追問六:為什麼不用 Service Worker 同時共用多分頁 WebSocket?
Service Worker 的生命週期由事件驅動,瀏覽器可在閒置時終止它;WebSocket 需要持續連線與記憶體工作階段,不符合這種執行模型。SharedWorker 在仍有頁面引用時更適合持有共用連線。若目標瀏覽器不支援 SharedWorker 且單一連線是硬性要求,應採用跨頁選主或改變伺服器架構,不能依賴 Service Worker 常駐。