Test Environment:
- Bun version: 1.2.13 (JavaScriptCore)
- Node.js version: v22.20.0 (V8)
- Platform: Linux (NixOS)
- Date: 2026-01-23
Testing reveals significant memory management differences between Bun (JSC) and Node.js (V8):
| Metric | Bun (JSC) | Node.js (V8) | Difference |
|---|---|---|---|
| SharedArrayBuffer leak | 6.4 KB/iter | 0 KB/iter | V8 has no leak |
| Table leak | 84 KB/iter | 6 KB/iter | 14x worse in JSC |
| Memory growth test (shared) | +38.76 MB | -56 KB | JSC leaks, V8 reclaims |
| Memory growth test (non-shared) | +14.50 MB | -1.82 MB | JSC leaks, V8 reclaims |
Conclusion: V8 (Node.js) properly reclaims WASM memory while JSC (Bun/Safari) shows persistent memory accumulation, confirming the Safari WebAssembly memory issues exist in the JSC engine itself.
| Runtime | All Tests Passed | RSS Change | Heap Change |
|---|---|---|---|
| Bun 1.2.13 | ✓ Yes | +17.18 MB | +112 MB |
| Node.js v22 | ✓ Yes | +0.55 MB | -1.22 MB |
Both runtimes successfully allocate all SharedArrayBuffer sizes including 2GB maximum. However, Node.js shows minimal memory impact while Bun retains significant memory after tests complete.
| Test Case | Bun Create | Node Create | Bun Grow | Node Grow |
|---|---|---|---|---|
| Small shared (16MB/256MB) | 0.06ms | 0.07ms | 10.23ms | 0.01ms |
| Very large shared (~200MB/2GB) | 0.97ms | 0.03ms | 9.20ms | 0.01ms |
Key Finding: Node.js memory growth operations are ~1000x faster for shared memory.
| Runtime | Initial RSS | Final RSS | Total Change | Per Iteration | Leak Status |
|---|---|---|---|---|---|
| Bun 1.2.13 | 33.48 MB | 47.98 MB | +14.50 MB | 297 KB | ⚠ LEAKING |
| Node.js v22 | 109.32 MB | 107.50 MB | -1.82 MB | -36 KB | ✓ OK |
| Runtime | Initial RSS | Final RSS | Total Change | Per Iteration | Leak Status |
|---|---|---|---|---|---|
| Bun 1.2.13 | 48.73 MB | 87.48 MB | +38.76 MB | 794 KB | ⚠ LEAKING |
| Node.js v22 | 107.50 MB | 107.45 MB | -56 KB | -1 KB | ✓ OK |
Key Finding: V8 properly garbage collects WASM memory (RSS actually decreases), while JSC accumulates memory with each cycle. This directly correlates with Safari's page-reload memory issues.
| Runtime | Total Growth | Leak/Iteration | Trend Slope | Status |
|---|---|---|---|---|
| Bun 1.2.13 | 8.20 MB | 84 KB | 52.59 KB/iter | ⚠ LEAKING |
| Node.js v22 | 600 KB | 6 KB | 708 bytes/iter | ⚠ Minor leak |
Ratio: JSC leaks 14x more memory per iteration than V8 for table operations.
| Runtime | Total Growth | Leak/Iteration | Trend Slope | Status |
|---|---|---|---|---|
| Bun 1.2.13 | 1.34 MB | 13.76 KB | 5.03 KB/iter | ⚠ LEAKING |
| Node.js v22 | 256 KB | 2.56 KB | 1.03 KB/iter | ⚠ Minor leak |
Ratio: JSC leaks 5x more memory than V8.
| Runtime | Total Growth | Leak/Iteration | Trend Slope | Status |
|---|---|---|---|---|
| Bun 1.2.13 | 640 KB | 6.40 KB | 8.45 KB/iter | ⚠ LEAKING |
| Node.js v22 | 0 bytes | 0 bytes | 0 bytes/iter | ✓ OK |
Key Finding: V8 shows zero memory leak for shared memory WASM, while JSC continues to accumulate. This is the most significant difference and explains why Safari has SharedArrayBuffer memory issues.
Based on the test results and WebKit bug reports:
- JSC's
JSWebAssemblyInstance::setTablestoresWasm::Tablereferences but doesn't properly release them - Test Result: 14x worse in JSC vs V8
- Fixed in Safari 18.3 but not backported
- JSC uses
MemoryMode::Signalingwhich allocates full maximum capacity upfront as protected pages - Memory cleanup relies on reference counting that appears to fail in certain scenarios
- Test Result: V8 shows zero leak, JSC shows consistent accumulation
- V8's garbage collector aggressively reclaims WASM memory after instances go out of scope
- JSC's GC appears to retain references longer, especially for SharedArrayBuffer-backed memory
- Test Result: V8 RSS decreased during stress tests, JSC RSS increased
Result: NOT REPRODUCED in either runtime
Both WASM and JS writes to SharedArrayBuffer are visible to workers in both Bun 1.2.13 and Node.js v22.20.0. This suggests the issue was either:
- Fixed in recent versions
- Platform-specific
- Timing-dependent
- Avoid repeated WASM instantiation - memory will accumulate
- Prefer non-shared memory when possible (lower leak rate)
- Minimize table usage in WASM modules (highest leak rate)
- Upgrade to Safari 18.3+ for the setTable fix
- Consider 256MB memory limits on iOS Safari
- V8 handles WASM memory correctly - no special precautions needed
- Even shared memory with large allocations is properly reclaimed
- Test WASM memory behavior on both engines
- Consider implementing memory limits for Safari/JSC environments
- Use non-shared memory fallback when SharedArrayBuffer causes issues
# Run with Bun (JSC)
bun run test-shared-memory.ts
bun run test-memory-growth.ts
bun run test-memory-leak.ts
# Run with Node.js (V8) - requires --expose-gc for accurate results
npx tsx --expose-gc test-shared-memory.ts
npx tsx --expose-gc test-memory-growth.ts
npx tsx --expose-gc test-memory-leak.ts- WebKit Bug #222097 - Memory not freed on reload (Fixed iOS 18)
- WebKit Bug #255103 - WASM OOM with shared=true (Open)
- WebKit Bug #285766 - setTable leak (Fixed Safari 18.3)
- Bun Issue #25677 - Worker visibility (Not reproduced)
- Emscripten Issue #20920 - mailbox_await leak
Bun: RSS +17.18 MB, Heap +112.10 MB
Node: RSS +0.55 MB, Heap -1.22 MB
Bun Non-shared: +14.50 MB (297 KB/iter) - LEAKING
Bun Shared: +38.76 MB (794 KB/iter) - LEAKING
Node Non-shared: -1.82 MB (-36 KB/iter) - OK
Node Shared: -56 KB (-1 KB/iter) - OK
Bun Table: +8.20 MB (84 KB/iter) - LEAKING
Bun Non-shared: +1.34 MB (13.76 KB/iter) - LEAKING
Bun Shared: +640 KB (6.40 KB/iter) - LEAKING
Node Table: +600 KB (6 KB/iter) - Minor leak
Node Non-shared: +256 KB (2.56 KB/iter) - Minor leak
Node Shared: 0 bytes (0 KB/iter) - OK