AudioWorklet vs ScriptProcessorNode in Web Audio
The Web Audio API’s AudioWorklet replaces the deprecated
ScriptProcessorNode to provide high-performance,
low-latency custom audio processing in JavaScript. While the legacy
ScriptProcessorNode executed audio code on the browser’s
main execution thread—leading to audio stuttering, dropouts, and
latency—the AudioWorklet offloads audio computation to a
dedicated, low-latency audio rendering thread. This shift ensures
continuous, glitch-free audio processing that remains unaffected by
heavy main-thread operations like UI rendering, DOM manipulation, and
garbage collection.
The Flaws of ScriptProcessorNode
The ScriptProcessorNode relied on the
onaudioprocess event handler, which executed on the main UI
thread. This architectural limitation introduced several critical
problems:
- Audio Glitches (Dropouts): Because audio rendering competed with UI rendering, event listeners, and JavaScript execution, heavy tasks caused audible buffer underflows and clicks.
- Higher Latency: To compensate for main-thread bottlenecks, developers had to increase buffer sizes (often from 256 to 16,384 samples), significantly increasing audio latency.
- Synchronous Thread Blocking: Computationally intensive DSP (Digital Signal Processing) algorithms could freeze the user interface.
How AudioWorklet Solves Main-Thread Bottlenecks
AudioWorklet introduces a distinct separation between
UI-driven logic and actual DSP processing by splitting the
implementation across two components:
- AudioWorkletProcessor (Audio Thread): Executes
inside an
AudioWorkletGlobalScopeon a dedicated audio rendering thread. It directly handles the audio buffers synchronously within the render cycle. - AudioWorkletNode (Main Thread): Extends
AudioNodeand lives on the main thread, acting as the interface to connect the processor to the standard Web Audio graph and configure parameters.
By running the DSP logic in a dedicated audio thread,
AudioWorklet guarantees deterministic execution time,
allows for smaller buffer sizes (a default fixed quantum of 128 frames),
and delivers minimal audio latency without UI interference.
Key Differences
| Feature | ScriptProcessorNode | AudioWorklet |
|---|---|---|
| Execution Context | Main UI Thread | Dedicated Audio Worklet Thread |
| Thread Safety | Prone to UI-induced dropouts | Isolated from UI execution |
| Latency | High (large buffer requirements) | Ultra-low (fixed 128-sample block size) |
| Data Transfer | Event-based
(onaudioprocess) |
Shared memory / MessagePort
messaging |
| Custom AudioParams | Not supported | Fully supported with sample-accurate automation |
Architecture and Migration Flow
Migrating from ScriptProcessorNode to
AudioWorklet requires separating the audio algorithm into
an external script file and loading it asynchronously.
1. Define the Processor (Processor File)
The custom processing logic is defined in an isolated file by
extending AudioWorkletProcessor and implementing the
process() method:
// white-noise-processor.js
class WhiteNoiseProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const output = outputs[0];
output.forEach((channel) => {
for (let i = 0; i < channel.length; i++) {
channel[i] = Math.random() * 2 - 1;
}
});
return true; // Keeps the processor alive
}
}
registerProcessor('white-noise-processor', WhiteNoiseProcessor);2. Register and Connect (Main Thread)
The main thread loads the processor module into the audio context and
instantiates an AudioWorkletNode:
const audioContext = new AudioContext();
// Load the processor module asynchronously
await audioContext.audioWorklet.addModule('white-noise-processor.js');
// Create the AudioWorkletNode and connect it to the destination
const whiteNoiseNode = new AudioWorkletNode(audioContext, 'white-noise-processor');
whiteNoiseNode.connect(audioContext.destination);3. Thread Communication
To send control messages or pass data between the main thread and the
processor without blocking audio computation,
AudioWorkletNode and AudioWorkletProcessor use
the built-in bidirectional MessagePort via
node.port.postMessage() and
node.port.onmessage.