深拷貝利用 structuredClone + Transferable 把 100 MB JSON 復制時間砍到 15 ms
1. 先拋問題:為什么老辦法會翻車?
前端世界里「深拷貝」是剛需:
- 防副作用,Redux 要 immutable
- 防競態,彈窗表單先拍個快照
- 防回滾,撤銷/重做棧必須深復制
最順手的就是:
const copy = JSON.parse(JSON.stringify(data));但它有三宗罪:
- 丟類型:Date → 字符串、Map/Set → 空對象、undefined → 消失
- 爆內存:100 MB JSON 字符串會瞬間 double
- 循環引用直接拋錯:Uncaught TypeError: cyclic object value
社區方案(lodash cloneDeep、ramda clone)能解決問題 1、3,但大對象依舊慢——時間復雜度 O(n)、內存峰值 2n。Web 環境早就有「零拷貝」黑科技:structuredClone + Transferable。只是很多人把它當成「WebWorker 專用」——今天咱們把它薅出來做「史上最快深拷貝」。
2. structuredClone 是什么?
W3C 官方定義:
一種瀏覽器內置的、結構化克隆算法實現,支持 Map/Set/Date/RegExp/ArrayBuffer/Blob/File/ImageData 等 30+ 類型,自動處理循環引用。
一句話:比 JSON 方法全能,比遞歸算法快,還能“轉移”內存。
兼容性(2025-10):
- Chrome 98+、Edge 98+、Firefox 94+、Safari 15.4+
- Node.js 17+ 實驗引入,v18.0.0 正式暴露為全局 API
檢測代碼:
const isSupported = typeof globalThis.structuredClone === 'function';3. Transferable:把「復制」變「搬家」
ArrayBuffer 這類底層二進制,默認會被 克隆 —— 內存翻倍。標記為 Transferable 后,原對象失效,內存直接“搬家”到新上下文,0 復制成本。
類比:
- 克隆:復印機再印一份,兩份紙
- 轉移:把原件遞給你,手里還是一份,只是換了人
關鍵接口:
structuredClone(value, { transfer })- transfer 是一個數組,列出你想“搬家”的 buffer
- 搬家后,原 buffer 被 detach,.byteLength === 0
4. 實戰:手寫一個「零依賴」極速深拷貝函數
目標:
- 通用:支持循環引用、支持任意結構化克隆可用類型
- 大文件友好:自動識別 ArrayBuffer 并轉移
- 零三方包:瀏覽器 & Node 通殺
- 可回退:老環境降級到 lodash 或 JSON
/**
* zero-deps deep clone
* @param {*} value 任意值
* @returns {*} 深拷貝結果
*/
exportfunction fastClone(value) {
// 1. 檢測 structuredClone 是否可用
if (typeof structuredClone === 'function') {
const transfers = [];
// 2. 遞歸收集所有 ArrayBuffer(含嵌套)
(function collectBuffer(val) {
if (val instanceofArrayBuffer) {
transfers.push(val);
return;
}
if (Array.isArray(val)) {
val.forEach(collectBuffer);
return;
}
if (val && typeof val === 'object') {
if (val instanceofMap) {
val.forEach((v) => collectBuffer(v));
} elseif (val instanceofSet) {
val.forEach((v) => collectBuffer(v));
} else {
Object.values(val).forEach(collectBuffer);
}
}
})(value);
// 3. 執行克隆 + 轉移
return structuredClone(value, { transfer: transfers });
}
// 4. 降級方案(可選:引入 lodash/cloneDeep 或 JSON 粗略拷貝)
try {
returnJSON.parse(JSON.stringify(value));
} catch (_) {
thrownewError('fastClone: 當前環境不支持 structuredClone,且對象不可序列化');
}
}代碼說明:
- 遞歸收集所有 ArrayBuffer,包括 TypedArray.buffer、嵌套在對象/Map/Set 里的
- 一次性傳入 transfer,避免多次克隆帶來的內存抖動
- 降級分支保證“老瀏覽器”不白屏,但會提示性能損耗
5. 性能對比:真實跑一遍
測試機:MacBook Air M2 / 16 GB數據:
- 普通對象 200 k 鍵
- 內含 1 個 100 MB 的 Float64Array
- 存在循環引用(雙向鏈表)
方案 | 耗時 | 峰值內存 | 備注 |
| 拋錯 | — | 循環引用直接掛 |
| 1.28 s | +210 MB | 深復制完成,但雙倍內存 |
| 380 ms | +105 MB | 克隆 buffer,內存翻倍 |
| 15 ms | +0 MB | 原 buffer 被清空 |
結論:
- 速度提升 25 倍
- 內存 0 增長(原 buffer 被 detach,GC 立即回收)
6. 常見坑 & 規避清單
- detachable 后原對象失效如果業務后續還要用原 buffer,先復制再轉移:
const cloned = fastClone(buffer);
// 原 buffer 此時 byteLength === 0,不可再訪問- Node < 17 沒有全局 API加一段 polyfill:
import { structuredClone } from 'node:util';
globalThis.structuredClone ??= structuredClone;- structuredClone 不能克隆函數、DOM 節點、Error 對象需要函數序列化請改用 eval + toString() 的臟套路,或干脆禁止業務把函數塞進狀態樹。
- MessageChannel 異步轉移?不需要!早年有人用 postMessage 模擬深拷貝,但那是 異步 且代碼啰嗦;structuredClone 是 同步 調用,無需任何 Channel。
7. 在真實項目里落地
場景 1:表格快照
- 用戶點擊「歷史版本」時,把 50 MB 數據拍下來做對比
- 老方案卡頓 600 ms,UI 掉幀;換 fastClone 后 20 ms 內完成,體驗絲滑
場景 2:WebAssembly 內存鏡像
- WASM 線性內存 Module.memory.buffer 需要備份做「重置」
- 轉移后原內存立即釋放,避免瀏覽器 OOM
場景 3:撤銷/重做棧
- 圖形編輯器每步都要深復制 10 萬節點
- 用 Map 存節點屬性,structuredClone 天然支持,無需手動序列化
8. 總結
瀏覽器自帶 structuredClone,同步、全能、循環引用安全;再配 Transferable,大文件深拷貝 15 ms 搞定,內存 0 上漲;今天 copy 這段 fastClone,明早把 lodash.cloneDeep 從 bundle 里刪掉,包體積 -21 kB,性能 ×25,OKR有著落啦。































