daw-ui

Performance

How the components stay out of the way of audio and animation, and how that is measured on every change.

In a DAW, the interface competes with audio and with dozens of animated meters. Performance is a requirement of the library, and it is measured, not assumed.

How the components work

  • Styling runs no code per render. className and style take plain values; state reaches CSS through data attributes and CSS variables, and thresholds through zones. A drag commits on every pointer event and automation on every frame, so anything computed per render is multiplied by the channel count and the frame rate.
  • Values never render through React. When the value of a knob, fader or number box changes, by a drag, a key, or automation, each part writes what it shows (an attribute, a CSS variable, its text) straight to the DOM, and only what differs. Dragging a knob renders nothing, not even the knob.
  • Values that change on their own are read, not pushed. Meters, read on knobs and faders, and Knob.Modulation are called once per animation frame by one shared requestAnimationFrame loop, instead of flowing through props and renders. The accessible value of a meter updates four times a second.
  • An interaction renders only what it changes. Toggles in a group subscribe to their own state: painting one step of a 16 × 64 sequencer renders that step, not the grid.
  • Playback touches only what follows the playhead. The playhead's position is written as a number into the playhead and the played parts of waveforms, not as a CSS variable: a variable on the timeline would recalculate the style of every element under it on every frame. Scrolling and zooming do change the variables every element on the timeline uses, so they cost more than playback, in proportion to the number of elements.
  • Drawing happens when what is drawn changes, not when it moves. Waveforms draw canvas tiles once and let CSS place them in time: playback and scrolling draw nothing already drawn. What must be drawn goes through one queue that spends at most 4 ms per frame, visible tiles first.
  • Layout is read once per gesture. A paint stroke reads the boxes of the toggles, or of a bar graph's items, when it starts, then tests the pointer's path against them.
  • Formatters are created once, not per frame.

What is measured

Every change to the repository runs stress pages, in a production build, in Chromium with the CPU slowed down 4×: 64 channel strips with running meters, pan knobs, faders and mute and solo buttons, a 16 × 64 step sequencer, 64 channels whose knobs and faders follow automation on every frame, a timeline of 16 tracks with 32 four-minute clips under one playhead, a ruler and a grid, a take being recorded, a MIDI track and an automation lane, a keyboard of 88 keys, a bar graph of 64 values, and eight live spectra.

ScenarioMain thread per frame (ms)Of which JavaScript (ms)Dropped framesInput → frame p50 / p95 (ms)React commits
64 meters running5.00.20 of 181–0
Fader drag, meters running8.30.40 of 919.8 / 11.20
Paint 64 steps, meters running7.41.11 of 8910.1 / 11.832 for 30 moves
Automation on 128 knobs and faders, through read12.22.71 of 180–0
The same automation through React state (value)19.49.924 of 157–one per frame
32 waveforms on a timeline with a ruler and grid, playback2.80.10 of 181–0, and no tile drawn
The same, the view paging along with playback4.60.20 of 181–0, 4 tiles drawn in 3 s
The same, recording a 33rd take3.50.30 of 181–0, one tile drawn per frame
The same, zooming without pause8.90.84 of 177–0
The same, paging along, with a MIDI track of 3760 notes4.90.20 of 181–0, 4 tiles drawn in 3 s
The same, zooming, with an automation lane of 2000 bent points9.10.84 of 177–0
The same, playing, dragging a point of that lane, editable4.00.50 of 9010.4 / 13.52: the press, and keeping the points
88 keys lit by playback through read0.40.00 of 181–0
A glissando across 88 keys1.10.10 of 9014.2 / 15.40
A stroke across 64 values of a bar graph1.00.31 of 9014.8 / 15.70
8 spectra of 2048 bins, read every frame5.10.70 of 181–0

A 60 Hz frame has 16.7 ms. The two automation rows are the same page driven two ways: controlled value props updated on every frame spend more than the whole budget, read leaves room.

The numbers that do not depend on the machine fail the build when they get worse: React commits per interaction (zero for drags, for automation through read, for running meters, for waveforms and for keys), canvas tiles drawn (none during playback), renders per painted step, and the bundle size, minified and gzipped: 40.6 kB for everything, and what an application adds when it imports only some components, such as 7.8 kB for a knob alone or 4.8 kB for a keyboard alone, which checks that the rest is left out. Timings are reported in the CI job summary, because shared CI machines are too noisy to fail on them.

In your application

  • Pass values that change on their own with read, not with value or level: a meter's level, automation playback, a parameter moved by a control surface. read is called once per frame and renders nothing; a prop renders its parent.
  • Controlled components render when their parent does. For a mixer with many channels, keep each channel strip in a memoized component, or leave the controls uncontrolled and send onValueChange straight to your audio engine, as the playground does.

On this page