Speed Up ArrayBuffer Memory Release in JavaScript
JavaScript's garbage collector handles memory automatically, but processing large ArrayBuffers continuously (several MB per second) causes lag. In browsers like Chrome, memory usage balloons to hundreds of MB, especially with video streaming. Firefox handles it better, but spikes still happen. This is critical for 32-bit processes and mobile devices with limited RAM.
ArrayBuffer is built for binary data but lacks manual release or resize methods. Proposals like ArrayBuffer.transfer() aren't widely supported. A buffer's detached state (byteLength=0, access throws TypeError) lets the GC reclaim it quickly.
MessageChannel Trick for Detaching ArrayBuffers
MessageChannel transfers data between contexts. Closing port2 detaches port1: the data goes straight to GC without waiting for the event loop.
const GarbageBin = (() => {
/** @type {MessageChannel | null} */
let messageChannel = null;
/**
* Speeds up memory release for ArrayBuffer, TypedArray, DataView.
* @param {any} junk
*/
function dispose(junk) {
if (typeof junk === 'object' && junk) {
const buffer = junk.buffer || junk;
if (buffer.byteLength) {
if (!messageChannel) {
messageChannel = new MessageChannel();
messageChannel.port2.close();
}
messageChannel.port1.postMessage(buffer, [buffer]);
}
}
}
return { dispose };
})();
Usage:
let buffer = new ArrayBuffer(1_000_000);
// Work with buffer...
GarbageBin.dispose(buffer);
buffer = null;
Compatibility: Works in Chrome 55+, Firefox 50+. Chrome 56 has issues with data sticking around. Result: 100 MB peak memory reduction in real scenarios.
Limitations: Not for asm.js heaps (Firefox 49). Unstable across versions.
Worker Trick for Batch Memory Disposal
Worker.postMessage detaches ArrayBuffer. Terminating the worker (self.close()) instantly frees all thread memory.
Optimization: Accumulate ~10 MB before shutdown to minimize create/destroy overhead.
const GarbageBin = (() => {
const BIN_CAPACITY = 10_000_000;
/** @type {Worker | null} */
let worker = null;
let workerURL = '';
let bytesInWorker = 0;
function dispose(junk) {
if (typeof junk === 'object' && junk) {
const buffer = junk.buffer || junk;
if (buffer.byteLength) {
if (!worker) {
if (!workerURL) {
workerURL = URL.createObjectURL(new Blob([
`'use strict'; self.onmessage = ({data}) => { if (!data) self.close(); };`
], {type: 'application/javascript'}));
}
worker = new Worker(workerURL, {name: 'GarbageBin'});
}
bytesInWorker += buffer.byteLength;
worker.postMessage(buffer, [buffer]);
if (bytesInWorker > BIN_CAPACITY) {
clear();
}
}
}
}
function clear() {
if (worker) {
worker.postMessage(null);
worker = null;
bytesInWorker = 0;
}
}
return { dispose, clear };
})();
Call GarbageBin.clear() on shutdown. Result: Chrome -310 MB, Firefox -70 MB peak memory.
Issues: Virtual address space leaks in Firefox 55+.
Performance Comparison and Recommendations
Tested over 40 minutes: CPU cycles within margin of error. MessageChannel slower than Worker in Firefox (synthetic tests).
| Trick | Chrome Effect | Firefox Effect | Compatibility |
|---------------|---------------|----------------|-------------------|
| MessageChannel | -100 MB | Stable | Low |
| Worker | -310 MB | -70 MB | High (exc. FF 55+)|
- Use Worker for production: more reliable, bigger impact in Chrome.
- Test in target browsers: GC algorithms evolve.
- Avoid for asm.js/WebAssembly heaps.
- Monitor with perfmon/process explorer for working set.
Worker shines under heavy loads (video, streams).
Key Takeaways
- Both tricks detach ArrayBuffer via postMessage, speeding up GC.
- Worker batches 10 MB to cut overhead.
- Impact: Up to 310 MB peak reduction in Chrome streaming.
- CPU overhead <1% in real scenarios.
- Test compatibility: GC changes break tricks across versions.
— Editorial Team
No comments yet.