Need Game Art? Edit Game Trailers Launch Your Dev Site Sell Your Merch
Need Game Art? Sell Your Merch

Game Audio Performance and Memory

Updated July 2026
Audio processing in web games runs on a dedicated thread, which keeps it smooth even when the main thread struggles. But the audio thread has its own limits, and exceeding them causes clicks, pops, and dropouts. Managing node counts, decoded buffer memory, garbage collection, and processing overhead is essential for clean audio across desktop and mobile devices.

How Audio Processing Costs Add Up

Every active node in the Web Audio graph consumes processing time. A source node reads samples from a buffer. A gain node multiplies by a value. A biquad filter applies frequency-domain math. A panner node calculates spatial positioning. These costs are per-sample, running at the AudioContext's sample rate (typically 44,100 or 48,000 times per second). One gain node is trivial. Thirty panner nodes with HRTF processing is significant.

The audio thread must complete all processing for every audio frame within a strict deadline. Audio frames are typically 128 samples, giving the thread about 2.9 milliseconds per frame at 44.1kHz. If processing exceeds this budget, the audio thread misses a deadline and the output contains a glitch: a click, pop, or brief silence. These glitches are immediately audible and impossible to hide.

Desktop browsers on modern hardware can comfortably handle 64-128 simultaneous source nodes with moderate processing chains (gain + filter per source). Mobile browsers on mid-range phones start showing stress at 20-30 simultaneous sources, especially with spatial processing. Low-end phones may glitch above 10-15 sources.

Processing cost per node varies dramatically. A GainNode costs almost nothing. A BiquadFilterNode costs more. A PannerNode with "equalpower" costs moderate CPU. A PannerNode with "HRTF" costs roughly 10x what "equalpower" costs because it performs convolution with head-related impulse responses. A ConvolverNode with a 2-second impulse response is one of the most expensive single nodes available.

Polyphony Management

Polyphony (the number of sounds playing simultaneously) is the primary performance lever you control. Every explosion, footstep, gunshot, ambient bird call, and UI click that plays at the same time adds a source node and its processing chain to the graph. During intense gameplay, dozens of sounds may trigger within a single frame.

A polyphony manager tracks active sound count and enforces limits. When a new sound triggers and the active count exceeds the budget, the manager decides what to do: skip the new sound (simplest), stop the oldest low-priority sound to make room (more sophisticated), or reduce the quality of existing sounds (most complex).

Priority levels guide these decisions. Assign each sound type a priority: critical (player damage, game over jingle), high (weapon fire, explosions), medium (enemy sounds, environmental events), low (ambient detail, distant footsteps). When the budget is tight, low-priority sounds get dropped first. Critical sounds always play.

Per-type instance limits prevent any single sound from consuming too many slots. Even if total polyphony allows 32 sounds, you probably do not want 20 of them to be the same footstep. Limit each sound type to 2-4 simultaneous instances. Additional triggers of an already-maxed sound are silently dropped.

Track completion using the source node's ended event. When a sound finishes (or is stopped), the event fires and you decrement your active count. For sounds stopped via stop(), the ended event still fires, keeping your tracking accurate.

Decoded Buffer Memory

When decodeAudioData() converts a compressed audio file into an AudioBuffer, the result is uncompressed PCM data at the AudioContext's sample rate. The memory formula is straightforward: bytes = sampleRate * channels * bytesPerSample * durationSeconds. At 44.1kHz, stereo, 32-bit float (4 bytes per sample): a 1-second clip = 44100 * 2 * 4 = 352,800 bytes, roughly 345KB.

Scale that to a full game audio library: 100 sound effects at 1 second average = 34MB. 5 music tracks at 3 minutes each = 253MB. Those music tracks alone consume more memory than many mobile browsers allocate to a single tab. This is why streaming music through MediaElementAudioSourceNode instead of decoding fully into AudioBuffers is not optional on mobile.

Concrete memory reduction techniques with their approximate savings: convert all sound effects from stereo to mono (50% reduction, appropriate for any sound that gets positioned by a panner), stream music instead of buffering (eliminates 50-200MB per game depending on library size), unload sounds for inactive game zones (reduces active memory to current zone only), share AudioBuffers between Howl objects or source nodes (already the default behavior, just do not create duplicate buffers for the same file).

An audio memory budget helps you catch problems during development. Set a limit (e.g., 40MB for mobile, 150MB for desktop), track total decoded buffer size as sounds load, and log a warning when the budget is exceeded. This prevents the gradual accumulation of audio assets that works fine on your development machine but crashes on a player's phone.

Garbage Collection Pressure

Each sound playback creates at least one AudioBufferSourceNode (which is single-use and discarded after playing) and connects it to existing nodes in the graph. During intense gameplay, hundreds of source nodes may be created and discarded per minute. Each is a JavaScript object that eventually needs garbage collection.

While individual source nodes are small, high creation rates during gameplay can trigger GC pauses at inopportune moments. A 10-20ms GC pause during a frame is invisible visually but can cause an audible audio glitch if it coincides with an audio processing deadline.

Mitigation: pre-create reusable processing chains. GainNodes, BiquadFilterNodes, and PannerNodes are all reusable. Create a pool of gain nodes during initialization and connect new source nodes to available pool members rather than creating new gain nodes for every sound. The source node itself cannot be pooled (it is single-use by API design), but every other node type can.

Disconnect nodes explicitly after use. When a source node finishes playing, disconnect it from the graph in the ended event handler: source.disconnect(). While modern browsers garbage-collect disconnected audio nodes, explicit disconnection ensures the node stops any processing immediately and is eligible for GC sooner.

Avoid creating closures or complex objects in hot audio paths. The play function that runs hundreds of times per minute should be lean: look up the buffer, grab a pool gain node, create a source, connect, start, attach the ended handler, return. No object allocations beyond the source node itself.

Processing Chain Optimization

Not every sound needs every processing stage. A UI click needs a source node and a gain node, nothing more. A spatialized enemy growl needs source, gain, and panner. A reverb-processed footstep needs source, gain, panner, and a reverb send. Tailor the processing chain to the sound's needs.

Shared processing is cheaper than per-source processing. Instead of attaching a reverb ConvolverNode to every sound source, create one reverb bus (a ConvolverNode connected to the destination through a wet gain node). Sounds that need reverb connect to the reverb bus's input in addition to the dry output. The convolver processes all reverb-routed sounds as a single mixed input, which is dramatically cheaper than convolving each source independently.

Sub-mixing reduces downstream processing. If you have 20 enemy sound sources, each with its own gain node, connect all 20 gains to a single enemy sub-mix gain node. Apply any group-level processing (filtering, compression) to the sub-mix node rather than to each individual source. This changes a per-source cost into a per-group cost.

Disconnect sources from expensive processing when they are far from the listener. A sound beyond audible range does not need spatial processing, reverb, or even playback. Stop the source entirely when it exceeds the maximum audible distance, and restart it if the listener moves close again. Calculating distance is cheap; running a full audio processing chain for an inaudible sound is waste.

Profiling Audio Performance

Chrome DevTools' Performance panel shows audio thread activity as a separate thread in the timeline. Record a performance profile during gameplay and look for audio thread frames that approach or exceed their deadline. Long audio frames correlate with glitches.

The chrome://media-internals page shows detailed information about active audio contexts, including the number of live audio nodes, the rendering quantum (how often the audio thread runs), and any underflow events (missed deadlines that caused glitches).

The Web Audio API's AudioContext has a baseLatency property that reports the processing latency of the audio pipeline. Unusually high values suggest the audio thread is under stress. Monitor this during gameplay to detect performance degradation.

Build a runtime audio stats overlay for development. Track and display: active source count, total decoded buffer memory, current AudioContext state, and a glitch counter (increment whenever you hear or detect a dropout). This overlay catches problems during playtesting that might not trigger in targeted performance profiles.

Test on your lowest-target device. If you plan to support 3-year-old budget Android phones, profile audio on one. The gap between a development laptop and a budget phone is often 10x or more in audio processing capacity. Features that run effortlessly on your machine may be completely impractical on the target device.

Key Takeaway

Limit simultaneous sounds to 20-30 on mobile and 50-60 on desktop. Stream music instead of buffering. Convert effects to mono. Use priority-based polyphony management. Pool reusable gain/filter nodes. Route reverb through a shared bus. Disconnect nodes in the ended handler. Profile on your lowest-spec target device.