題幹與適用場景
瀏覽器需要壓縮上傳的大檔案並解壓下載的 gzip 資料,要求低記憶體、可取消且不因惡意輸入失控。請設計串流方案並說明格式、背壓、錯誤與安全邊界。
Compression Streams API 提供 CompressionStream 與 DecompressionStream,把二進位區塊接入 Web Streams 管線。規範定義 brotli、deflate、deflate-raw 與 gzip 格式;API 提供轉換流,卻不會替應用程式設定大小上限、驗證身分或執行業務完整性檢查。
面試官考察點
重點包括:Readable/Writable/TransformStream 的連接、背壓與佇列、結束 flush、壓縮格式邊界、解壓錯誤、取消傳播、記憶體上限、解壓炸彈與長度側信道,以及漸進增強與伺服器協商。
30 秒回答框架
「我會用 Blob.stream() 或 response body 接入 CompressionStream,透過 pipeThrough 保持背壓,使用 AbortSignal 傳播取消。下載端先限制壓縮大小、解壓位元組數與處理時間,再把 DecompressionStream 接入解析器;格式與校驗失敗立即終止並清理資源。瀏覽器能力不足時回退到伺服器壓縮,不能把 API 當作安全掃描器或完整性驗證器。」
分步驟深入解答
第一步:選擇輸入輸出串流
上傳可從 Blob.stream() 取得位元組流,下載可使用 Response.body。中間透過 CompressionStream 產生 ReadableStream,最後交給 fetch 請求體、檔案寫入或解析器;避免先把整個檔案讀入 ArrayBuffer。
第二步:連接轉換流
最小上傳管線如下:
async function uploadGzip(blob, signal) {
const compressed = blob.stream().pipeThrough(
new CompressionStream("gzip"),
{ signal },
);
return fetch("/upload", {
method: "POST",
body: compressed,
signal,
headers: { "Content-Encoding": "gzip" },
});
}實際服務還要約定請求長度、重試語意與伺服器是否接受串流請求。
第三步:理解背壓與 flush
Streams API 會根據下游佇列與寫入速度調節上游讀取。不要在 data 回呼中無限累積陣列;下游變慢時應讓管線自然暫停。關閉寫端時轉換流必須執行 flush,否則最後的壓縮區塊或校驗資訊可能沒有輸出。
第四步:處理格式與協商
CompressionStream 建構函式接收受支援的格式字串;不支援的格式會拋出錯誤。客戶端應依伺服器協定明確 Content-Encoding 或業務欄位,不能把 deflate、deflate-raw 與 gzip 視為同一種封裝,也不能未協商就傳送 Brotli。
第五步:設計安全解壓
壓縮資料可能以很小輸入展開成巨大輸出。解壓端要統計輸出位元組數、檔案項目、處理時間與並發任務,超過預算就呼叫 cancel 或 abort。解壓後內容仍需 MIME、路徑與內容安全檢查,不能因為來自 DecompressionStream 就信任。
第六步:傳播錯誤與取消
壓縮或解壓校驗失敗會使轉換流進入錯誤狀態。用 try/catch 處理 pipeTo、fetch 與 reader 錯誤,並把使用者取消傳播到請求、讀寫端與轉換流;清理暫存 Blob、鎖與 UI 進度狀態,避免懸掛讀取器。
第七步:處理隱私與完整性
壓縮長度可能洩露秘密與攻擊者可控文字的關係。不要把機密和使用者可控內容放入同一壓縮上下文;重要檔案使用獨立簽章或雜湊校驗,壓縮只負責編碼,不負責來源驗證或防竄改。
第八步:制定相容與降級
啟動時檢查建構函式與目標格式,在 Worker 中執行大檔案任務以隔離主執行緒。能力不足時交給伺服器壓縮或直接上傳,並保持相同的取消、大小限制與錯誤語意;不要只在現代瀏覽器驗證。
設計取捨與邊界
客戶端 CPU 還是網路節省
壓縮可降低上傳位元組數,但會消耗 CPU、電量與時間。行動裝置應依檔案類型、網路品質與電量制定策略,已壓縮格式通常不應再次壓縮。
串流處理還是重試簡單
串流管線低記憶體,卻要求伺服器支援分塊與冪等重試。需要斷點續傳時,應把壓縮區塊與上傳分片協定綁定,不能簡單重試已消費的 ReadableStream。
瀏覽器解壓還是伺服器解壓
瀏覽器解壓減少伺服器 CPU,卻把資源與安全預算放到使用者裝置。含敏感或高膨脹比資料時可由伺服器執行並回傳受控結果,前端只消費受限制的串流。
失敗演練與演進計畫
下游變慢導致記憶體增長
人為降低上傳速度,觀察佇列與記憶體曲線,確認沒有把所有 chunk 放入陣列;若佇列仍增長,降低並發並讓管線暫停。
截斷 gzip 輸入
截斷壓縮流,確認解壓在 flush 或校驗階段失敗,UI 顯示可重試狀態,並釋放 reader、請求與暫存物件。
解壓輸出超預算
使用高膨脹比樣本,驗證輸出計數達到上限後立即取消,不把完整結果寫入記憶體或持久化儲存。
常見誤區與追問
誤區一:串流 API 自動防止解壓炸彈
追問:還缺什麼?必須由應用程式設定輸出位元組、時間、檔案項目與並發預算,API 本身只執行格式轉換。
誤區二:deflate 等於 gzip
追問:為什麼不能混用?deflate、deflate-raw 與 gzip 封裝不同,伺服器必須依協定協商並使用匹配格式。
誤區三:直接重試已消費的流
追問:正確做法是什麼?從可重播的 Blob 或分片來源重新建立管線,並用冪等上傳識別碼協調伺服器。
延伸追問與參考答案
為什麼要關注 flush?
轉換流在輸入結束時需要輸出尾部壓縮資料與校驗資訊;沒有正常關閉寫端,消費者可能收到不完整串流。
如何降低長度側信道風險?
隔離機密與攻擊者可控文字,避免共享壓縮上下文,並在協定層加入固定填充或改用不暴露長度關係的設計。
哪些情況應優先由伺服器壓縮?
舊瀏覽器、低電量裝置、超大檔案或敏感資料可由伺服器承擔壓縮與解壓,但仍要限制輸出並向前端回傳可觀測狀態。