Node.js Buffer 進階操作:切片、複製與記憶體管理

在掌握了 Buffer 的基礎建立與轉換後,本章將帶你深入探討 Buffer 的運算邏輯,特別是它們在記憶體中的表現形式與對效能的影響。

切片與視圖:subarray() 的真相

開發者最容易感到困惑的是 Buffer 的「切片」行為。與 JS 陣列的 slice() 不同,Buffer 的 subarray()(或舊稱 slice()不會複製資料,而是建立一個指向原始記憶體區塊的「映射/視圖」(View)。

const original = Buffer.from('Antigravity');

// 切割出前 4 個位元組
const sub = original.subarray(0, 4);
console.log(sub.toString()); // Anti

// 修改子 Buffer,會連帶修改原始數據!
sub.write('XXXX');
console.log(original.toString()); // XXXXgravity
如果你需要獨立的副本:請使用 Buffer.from(original.subarray(0, 4))Buffer.alloc() 建立新空間後再用 copy() 複製進去。

手動複製內容:copy()

當你需要從一個 Buffer 部分移動資料到另一個 Buffer 時,copy() 是最精確的工具。

const source = Buffer.from('NodeJS-is-Cool');
const target = Buffer.alloc(4);

// source.copy(目標, 目標起始, 來源開始, 來源結束)
source.copy(target, 0, 0, 4);
console.log(target.toString()); // Node

內容比較與搜尋

在處理通訊協定或檔案格式時,比對二進位內容是日常任務:

  • buf.equals(otherBuf):快速檢查兩個 Buffer 的內容是否完全相同(二進位層級)。
  • buf.indexOf(value):尋找特定內容(字串或另一個 Buffer)在檔案中的起始位置。
  • Buffer.compare(buf1, buf2):用於排序 Buffer 陣列。
const b1 = Buffer.from('abc');
const b2 = Buffer.from('abcd');
console.log(b1.equals(b2)); // false

效能與記憶體管理建議

由於 Buffer 涉及到手動記憶體操作,有些性能上的最佳實務需要留意:

  1. 預分配大記憶體:如果你在迴圈中頻繁使用 Buffer.concat(),會導致大量的記憶體重新分配。建議預先 alloc() 一塊大的 Buffer,再用 copy() 依序填入。
  2. Buffer 池機制:Node.js 內部會維護一個小的 Buffer 池,用於處理較小的 Buffer.allocUnsafe 請求,以減少系統頻繁向作業系統申請記憶體的開銷。
  3. 外部記憶體:Buffer 是在 V8 Heap 之外分配的。雖然這能避開 JS 的垃圾回收壓力,但如果建立了海量的 Buffer 而不釋放,依然會耗盡系統記憶體。
小知識:你可以用 process.memoryUsage() 來查看你的 Node.js 進程除了 Heap 之外,還佔用了多少系統記憶體 (rss) 以及 Buffer (external)。

總結

Buffer 是 Node.js 處理二進位底層的基石。在下一章學習 Stream (串流) 時,你會更強烈地感受到:數據傳輸的本質其實就是一連串的「Buffer 片段在管道中流動」。