WebAssembly Linear Memory: Why Your High-Performance App Needs Capacity Planning
WebAssembly memory grows by reallocating and copying all data. For real-time apps, pre-allocate based on worst-case needs rather than growing during execution.
30 Oct 2026, 16:58 UTC

The Problem: Memory Growth Janks Your Real-Time App
When a WebAssembly module needs more memory, memory.grow doesn't just expand the buffer—it reallocates and copies everything. For a 100MB buffer, this can take milliseconds, causing frame drops in games, audio glitches in DAWs, or missed deadlines in financial trading UIs. The linear memory model is powerful, but its growth behavior is a performance trap.
The Model: Contiguous Pages, Not Heap Fragmentation
WebAssembly memory is a single contiguous buffer that grows in 64KB pages. Unlike JavaScript's garbage-collected heap, there's no automatic compaction or virtual memory mapping. You get direct access via load/store instructions, but you also own the fragmentation problem. This makes initial sizing critical: allocate too little and pay the growth tax; allocate too much and waste memory.
Worked Example: Pre-allocating a Particle System
Consider a browser-based particle engine that spawns 10,000 particles per second. Each particle needs 32 bytes (position, velocity, lifetime). That's 320KB/s, or ~19MB per minute. Growing memory during simulation causes visible stutter:
;; particle_system.wat - pre-allocated memory approach
(module
(memory 1) ;; 64KB initial - will be exceeded!
(data (i32.const 0) "particle_data")
(func $spawn_particle (param $x f32) (param $y f32)
;; This will fail once we exceed 64KB!
i32.const 1000000 ;; out of bounds
i32.const 0
f32.store
)
)
The fix: calculate worst-case memory needs upfront. For 10,000 particles at 32 bytes each, we need 320KB. That's 5 pages (5 × 64KB = 320KB). In the module declaration:
(memory 5) ;; 320KB - enough for 10k particles
Now f32.store at offset 1,000,000 succeeds without growth. The key insight: memory.grow should be a configuration-time decision, not a runtime occurrence.
Shared Memory: Threads Need Headers
WebAssembly threads enable parallel computation via SharedArrayBuffer, but browsers require cross-origin isolation (COOP/COEP headers). Check support:
// In browser console
'SharedArrayBuffer' in window // true if available
fetch('/').then(r => r.headers.get('cross-origin-embedder-policy')) // should be 'require-corp'
If headers are missing, shared memory silently fails. This isn't a WebAssembly limitation—it's a deployment constraint. Hosting on platforms without custom header support (some CDNs, GitHub Pages) blocks parallel WebAssembly entirely.
The Trade-off: Predictable Performance vs. Flexibility
Pre-allocating memory guarantees consistent performance but risks over-allocation. A safer pattern: grow memory during loading screens or idle periods, never during active computation. Use memory.grow once at startup, then treat memory as fixed-size for the session.
Actionable Check: Measure Your Growth Cost
Before shipping, test memory growth impact:
// In browser console, test growth cost
const wasm = await WebAssembly.instantiateStreaming(fetch('module.wasm'));
const mem = new WebAssembly.Memory({initial: 1});
const start = performance.now();
mem.grow(1000); // grow by 1000 pages (64MB)
const elapsed = performance.now() - start;
console.log(`Growth took ${elapsed.toFixed(2)}ms`);
If this exceeds 1ms, pre-allocate instead. For production apps, memory growth should be measured in development and eliminated in critical paths.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.