到這裡,我們已經把資料流關在 Wasm,互動操作幾乎不再跨界拷貝。接下來的現實問題是:這些像素會住多久、住在哪裡、什麼時候要請它們搬家。如果不把「生命週期」講清楚,你的 demo 會跑得很快,但用戶一換大圖、切十次濾鏡或開新分頁,就開始看到莫名其妙的記憶體暴衝與畫面錯亂。
這一篇談的不是演算法,而是空間治理:Wasm 線性記憶體的成長、常駐緩衝的重用/釋放、Worker 的生死、以及 JS 視圖的壽命管理。
Wasm 的 memory.grow()
只能成長,無法縮小;更重要的是,成長後底層 ArrayBuffer
會被替換。任何之前透過
new Uint8Array(memory.buffer, ptr, len)
建立的視圖,都會指向舊房子。你貼圖前若還在用舊的 view
,就會貼出錯亂畫面或拋錯。
準則
每次要把結果貼回畫布之前,重新讀一次 memory.buffer
再建視圖。不要把 view
或 memory.buffer
存在模組全域。
對外 API 層面,直接提供不暴露視圖的貼圖函式,由套件內部重抓 buffer
:
export function blitToImageData(imgData: ImageData) {
const ptr = buffer_ptr(), len = buffer_len()
const view = new Uint8Array((memory as WebAssembly.Memory).buffer, ptr, len)
imgData.data.set(view)
}
使用者只要呼叫 blitToImageData(imgData)
,不用管視圖是否失效。
我們用 BUF/SCR
兩塊 thread-local Vec<u8>
當乒乓緩衝。策略是「只長不縮」,以避免頻繁配置。這帶來兩個問題:
做法:在對外 API 提供兩個維運入口:
#[wasm_bindgen]
pub fn ensure_buffer(capacity: usize) { /* 已有 */ }
#[wasm_bindgen]
pub fn shrink_buffers() { // 主動縮到「目前最後一次使用大小」
BUF.with(|b| b.borrow_mut().shrink_to_fit());
SCR.with(|s| s.borrow_mut().shrink_to_fit());
}
#[wasm_bindgen]
pub fn buffers_capacity() -> usize {
BUF.with(|b| b.borrow().len())
}
shrink_buffers()
,把高峰記憶體還給系統。buffers_capacity()
,讓操作者知道現在吃了多少。畫布尺寸變了(或載入新圖)→ 應該做:
ensure_buffer(w*h*4)
:容量夠再搬。load_pixels(bytes)
:把新圖灌進 Wasm。run_pipeline_inplace(w, h, /* 可給空陣列代表不做事 */)
:同步尺寸語境(有時你會在 in-place 裡快取寬高)。不要保留任何上一張圖的 ImageData
視圖或 Uint8Array
指標;只保留數字(w/h)。
Day 14/15 我們把任務丟進 Worker 里跑。生命週期該怎麼訂?
terminate()
。優點是乾淨;缺點是初始化成本。ensure_buffer
+ load_pixels
,中間只傳參數(Transferable 只在首次載入時用得到)。傳輸資料記得用 Transferable(postMessage(data, [data.buffer])
),讓來源端的 ArrayBuffer
變成 detached,真正做到了「搬家不是複製是過戶」。但注意:Canvas 的 ImageData.data
不是可轉移,載入時請用 createImageBitmap()
+ OffscreenCanvas
畫進 Worker 端的畫布,再 getImageData()
抓 bytes;或主緒先 getImageData()
,複製一份普通 Uint8Array
再轉移。
閉包捕捉
你在事件處理器外層宣告 const big = new Uint8Array(w*h*4)
,結果每個 listener 都抓住它,換頁也沒釋放。
解法:把大陣列的生命週期縮到函式內;或把它藏在 Wasm 裡(我們已經做了)。
FinalizationRegistry 不是萬靈丹
可以用它在 JS 物件 GC 時呼叫 shrink_buffers()
或 dispose()
,但時間點不可預期。關鍵路徑仍要顯式地在「離開此圖」時呼叫釋放/縮容。
給使用者一個明確的按鈕或程式 API 可以「收工」:
// JS wrapper(同步版本)
export function dispose(): void {
// 可選:把 Wasm 端緩衝縮容
shrink_buffers()
// Worker 版:terminate()
// UI 版:清掉畫布/事件
}
如果你有 Worker 版本,則提供 await worker.dispose()
:
terminate()
Worker把「收工」變成顯式動作,不要指望 GC 幫你決定何時該退房。