How to Prevent Memory Leaks in Tone.js
Dynamically creating and destroying synth instances in Tone.js can quickly degrade browser performance and cause audio glitching if memory is not managed correctly. Because Tone.js operates on the Web Audio API, simply dereferencing a variable is not enough to trigger garbage collection. This article provides a direct, actionable guide on how to properly clean up synth instances, disconnect audio nodes, and free up system memory when dynamically managing audio in Tone.js.
Why Tone.js Synths Cause Memory Leaks
In standard JavaScript, setting a variable to null makes
it eligible for garbage collection. However, Web Audio API nodes (which
power Tone.js) are tied to the browser’s audio graph. If a synth is
still connected to the master output (toDestination()) or
another effect, the browser maintains a reference to it in the audio
thread.
To completely remove a synth from memory, you must explicitly disconnect it from the audio graph, release its internal sub-nodes, and then delete your JavaScript references.
The Correct Disposal Workflow
To prevent memory leaks when destroying a dynamically created synth, you must follow a three-step process: stop the audio, dispose of the node, and nullify the variable.
1. Trigger Release (Stop Active Audio)
If the synth is currently playing a note, call
triggerRelease() to let the envelope finish. Destroying a
synth mid-oscillation can cause audible clicks or pops.
synth.triggerRelease();2. Dispose of the Synth
Tone.js provides a built-in .dispose() method on almost
all of its classes. Calling .dispose() is the most critical
step. It automatically disconnects the synth from any output nodes,
stops internal oscillators, and cleans up internal Web Audio nodes.
synth.dispose();3. Clear JavaScript References
After disposing of the synth, clear any references to it in your code so the JavaScript engine’s garbage collector can reclaim the memory.
synth = null;Code Example: Safe Dynamic Creation and Destruction
Below is a practical implementation of creating a synth on demand and destroying it safely after use.
let activeSynths = [];
// Function to dynamically create and play a synth
function playDynamicSound() {
const synth = new Tone.Synth().toDestination();
activeSynths.push(synth);
// Play a note
synth.triggerAttackRelease("C4", "8n");
// Schedule cleanup after the note has finished playing (e.g., after 1 second)
setTimeout(() => {
cleanupSynth(synth);
}, 1000);
}
// Function to safely destroy the synth
function cleanupSynth(synth) {
// 1. Disconnect from the audio graph
synth.disconnect();
// 2. Dispose of internal Tone.js Web Audio nodes
synth.dispose();
// 3. Remove from local reference arrays
activeSynths = activeSynths.filter(s => s !== synth);
}Best Practices for Dynamic Audio in Tone.js
- Avoid Excessive Creation: Instead of constantly creating and destroying synths, consider using a Synth Pool. Reuse a set number of pre-created synth instances by dynamically reassigning their parameters.
- Dispose of Effect Chains: If you attach a dynamic
synth to a private effect chain (e.g.,
synth.connect(reverb)), you must dispose of the effect (reverb.dispose()) as well as the synth. - Monitor Memory: Use the browser’s developer tools (such as the Performance or Memory tab in Chrome) to take heap snapshots and monitor the number of active Web Audio nodes to ensure your cleanup routines are working effectively.