💜 Voice AI 3.0 is live · See what's new →
Ag irl
BUILD

Real-time waveforms with WebGL — a postmortem

SR
S. Romero · Engineering April 9, 2026 · 11 min read

Three iterations to get a 60fps voice waveform that doesn't melt phones. The path involved more pain than expected.

When you say something to AGirl, the bar that animates while you’re speaking is doing more work than it looks. It’s reading PCM frames at 48kHz, computing a windowed FFT, downsampling to 60 visual frames per second, and pushing geometry to the GPU — all while the rest of the app is keeping a 200ms voice latency budget.

We shipped three versions before we were happy. Here’s what we tried and what landed.

V1: Canvas2D

The first version drew 64 vertical bars per frame in a 2D canvas. It worked. On a MacBook M2, it was 60fps with 0.4% CPU. On a 2019 Android mid-range phone, it was 22fps and drained the battery in 90 minutes.

The bottleneck was rasterization. Every frame, the browser was filling 64 rounded rectangles. That’s fine on a discrete GPU. It’s catastrophic on a mobile GPU sharing memory with the rest of the system.

V2: SVG

We tried SVG out of curiosity. Same architecture, declarative geometry, hopefully the browser’s path renderer would batch better. It didn’t. Mobile SVG performance is worse than Canvas2D in every browser we tested.

V3: WebGL

We rewrote the waveform in WebGL with a fixed-vertex instanced quad shader. 64 quads, one draw call per frame. The GPU does the vertex transforms. The CPU does the FFT only.

Performance: 60fps everywhere, including the 2019 Android phone. Battery cost dropped 80% on mobile. CPU cost dropped 92% on desktop.

The catch

WebGL is brittle. Context loss on tab switch, on backgrounding, on certain memory pressure events on Android. We had to write a context-restore handler that gracefully redraws the last visible state from a CPU-side copy of the bar heights. That handler is 40 lines and we hate it but it stays.

The other catch: WebGL requires shipping shaders. Our pipeline strips comments, minifies, and inlines them at build time. The first shipping version had a typo that made the waveform render upside-down on iOS Safari only. We learned to write integration tests against real Safari before we shipped.

What we’d do differently

Skip V1 and V2. Go straight to WebGL. The premise — “Canvas2D is fast enough for 64 bars” — was correct on hardware that doesn’t represent our users. We learned the hard way that the test devices in the office are not the test devices for the product.

More like this