Performance & memory

Keep wavesurfer.js fast and lean with these practical techniques.

Large files

Decoding a long audio file with the Web Audio API allocates the entire file as raw PCM in memory — a 60-minute stereo track can easily exceed 500 MB. Two options help here.

Use the MediaElement backend. Pass a plain <audio> or <video> element as the media option. The browser streams the file without fully decoding it, and memory stays low regardless of duration.

const audio = new Audio()
audio.src = '/long-podcast.mp3'

const ws = WaveSurfer.create({
  container: '#waveform',
  media: audio,
})

Supply pre-decoded peaks. Pair the MediaElement backend with a pre-computed peaks array so the waveform still renders immediately. See Pre-decoded peaks for how to generate and serve peak data.

const ws = WaveSurfer.create({
  container: '#waveform',
  media: audio,
  peaks: [[-0.5, 0.5, -0.3, 0.8 /* … */]],
  duration: 3600,
})

Canvas-width limit. The renderer splits the waveform across multiple canvas elements, each at most 8 000 px wide (the MAX_CANVAS_WIDTH constant in renderer-utils.ts). Each individual canvas is also capped at the scroll container’s clientWidth. When the container is extremely wide, or minPxPerSec produces a very long total waveform, the renderer may produce a large number of canvas elements. To keep DOM node counts in check, the renderer automatically clears off-screen canvases when scrolling (the MAX_NODES = 10 threshold in renderer-utils.ts). If the container width is 0 or the layout has not settled when wavesurfer initialises, the canvas width calculation returns 0 and nothing renders — make sure the container has a measurable size before calling WaveSurfer.create.

Avoid extremely large minPxPerSec values in non-scrollable containers. A total waveform width in the tens of thousands of pixels forces the browser to allocate many large canvases at once and can crash the tab.

Many regions

The Regions plugin lets you add, remove, and update regions dynamically. Creating and removing thousands of regions in rapid succession can cause visual glitches because each region is an absolutely positioned DOM element inside the waveform container.

Guidelines:

  • Keep the total number of visible regions small — remove regions you no longer need with region.remove() rather than rebuilding every region on each update.
  • If you need to bulk-update regions (e.g. after a re-analyse pass), call regions.clearRegions() once and re-add only what is visible, rather than removing them one by one in a loop.
  • For very dense data (thousands of segments), consider rendering a custom canvas overlay instead of individual region elements.
// Efficient bulk update
regions.clearRegions()
newSegments.forEach(({ start, end }) => regions.addRegion({ start, end }))

Cleanup

Failing to tear down a wavesurfer instance before navigating away or unmounting a component is the most common source of memory leaks.

Always call ws.destroy(). It emits the destroy event, calls destroy() on every registered plugin, unsubscribes all internal event listeners, and destroys the renderer and timer.

// React / Vue / Svelte teardown
onUnmounted(() => {
  ws.destroy()
})

Do not re-register plugins on the same instance. Registering a plugin a second time (e.g. inside a re-render function) adds a second set of listeners without removing the first, doubling event callbacks on every subsequent action. Create a fresh WaveSurfer instance instead, or gate registration so it only runs once.

Re-registering plugins on a live instance is a known source of duplicate callbacks and ghost regions. Always destroy() the old instance before creating a new one.

Unsubscribe your own event listeners. The .on() method returns an unsubscribe function — hold on to it and call it when you are done.

const unsub = ws.on('timeupdate', (currentTime) => {
  updateUI(currentTime)
})

// Later, on teardown:
unsub()

See Frameworks integration for component-lifecycle patterns in React, Vue, and Svelte.

Reducing memory

A few option-level tunings lower both memory use and CPU load:

Fewer peaks points. The peaks array resolution controls how many data points are stored in memory and iterated during every render pass. For most music players a peaks array with 1 000–2 000 points per channel is plenty. Only go higher if you need per-sample accuracy for editing tools.

Lower fftSamples for the Spectrogram plugin. The Spectrogram plugin computes an FFT for every column of pixels. The default fftSamples value is 512; halving it to 256 cuts both the FFT work and the frequency-bin storage roughly in half with a modest loss of vertical detail.

SpectrogramPlugin.create({
  fftSamples: 256, // default 512 — lower = faster & less memory
})

Avoid high height values when not needed. Canvas memory scales with width × height × devicePixelRatio². On high-DPI screens a 256 px tall waveform uses four times the canvas memory of the same waveform on a 1× display.