Tom Rhodes

Lab · 2026

One drum machine, four front ends.

A playable TR-808 drum machine. The sound engine is written once, in Haxe. The front end is built four times: plain DOM code in Haxe, React, Vue and Angular. Then I tested all four, and here are the results.

Tap a key to add a step; drag it up or down for its level, sideways to nudge it early or late. Turn the knobs by dragging. Space plays and stops. The eight pattern buttons each hold a pattern: tap one to edit it while the sequence plays on; Select, Copy and Clear by the Sequence title set what plays, copy one or clear one. Switch the front end and your patterns come with you.

The idea

I wanted a Lab piece you can play with that also shows how I work. Most of my front ends are Haxe compiled to plain JavaScript, so I built one small app four ways to see what each mainstream framework really costs and gives.

Everything except the front end is shared. One engine, written in Haxe and compiled to JavaScript, does the pattern, the timing and the sound through Tone.js. All four builds use the same stylesheet and the same markup, and they pass the same 30 browser tests. That way, any difference you can measure comes from the framework.

Why Haxe

Haxe is an open-source language with one strict compiler and many targets: JavaScript, PHP, C++, Python, Lua, the JVM and more. If you write JavaScript or TypeScript you can already read it: classes, arrow functions, much the same syntax. What it adds is a compiler that checks everything before anything runs.

  • Types without the typing. Every value has a type, but the compiler works most of them out, so the code stays as short as JavaScript. A misspelt field or a wrong argument is a compile error, not a bug a visitor finds.
  • Features JavaScript doesn't have. Enums whose switch has to cover every case, pattern matching, abstract types that cost nothing at run time, and macros that run code while compiling.
  • Small output. Dead-code elimination drops everything the program never calls: the whole engine here is 11 kB, minified and gzipped.
  • Write once, use on both ends. One class can compile to a PHP or Node backend and to the JavaScript in the browser, so rules like form validation are written once and can't drift apart. In my own work, the same Haxe classes run on the server and in the client.

That's why it suited this project. The engine is written once in Haxe and compiled to one small JavaScript module. React, Vue and Angular install it like any npm package, and a short type file lets their TypeScript check every call into it. The Haxe front end goes further: it compiles together with the engine's source, so one compiler checks everything from the buttons down to the audio graph. The engine is also tested on its own, on Node with a stand-in for the audio library: 259 checks run before any front end is built.

The machine

The panel is modelled on my Korg volca beats, in the colours of the Norwegian rock on the rest of this site. The sounds are the TR-808: 68 recordings, and turning a part's 808 knobs picks the recording made at that setting.

  • Every step has its own level (drag the key up or down), nudge timing of the step, early or late (drag it sideways), and a flip tab that plays the sound backwards.
  • Drive is a warm, valve-style overdrive; reverb is a send, like on a mixing desk.
  • Eight patterns, played in any sequence (version 2, below).
  • The whole pattern lives in the page address, so the share button at the top right sends a beat as a link: whoever opens it gets this page with your beat in the machine, ready to play.

The rules

The same rules held in every build: functions of at most fifteen lines, names that tell the story, and the first version of each written the way that framework's own guides would write it. Everything was minified and gzipped the same way, then measured on the same machine.

What I measured

Headless Chrome, one build at a time. “Phone” is a phone-sized window with the processor slowed four times, which is Chrome's stand-in for a mid-range phone. Each figure is the middle of three or five runs. In every chart, shorter is better. These are the numbers for version 2 (below); version 1's were within the same run-to-run noise.

Code each front end adds

Minified and gzipped, leaving out what every build shares: Tone.js (81 kB) and the engine (11 kB).

  1. Haxe4.2 kB
  2. React65.6 kB
  3. Vue24.0 kB
  4. Angular47.8 kB

Plain DOM code adds almost nothing. Each framework brings its runtime with it: Vue is the lightest of the three, React the heaviest.

Panel on screen, slow 4G phone

Largest paint, from Lighthouse's mobile test.

  1. Haxe1.8 s
  2. React4.7 s
  3. Vue4.8 s
  4. Angular5.0 s

The Haxe panel is plain HTML, so it appears as soon as the page arrives. The frameworks draw it from JavaScript, which has to download and run first. Rendering on the server would close that gap.

Play works, slow 4G phone

From asking for the page until the Play button can be pressed: scripts run, kit downloaded and decoded.

  1. Haxe4.0 s
  2. React4.3 s
  3. Vue4.0 s
  4. Angular4.2 s

Close for all four. Downloading the drum kit takes most of the time, whichever framework draws the panel.

Script per step of the playhead, phone

At 200 BPM, beyond what a bare page with the same engine and no framework needs.

  1. Haxe0.0 ms
  2. React2.0 ms
  3. Vue0.0 ms
  4. Angular0.3 ms

All four keep up easily: a step lasts 75 ms. React re-renders all sixteen keys on every step; Vue and Angular re-check only what reads the playhead; the Haxe version moves one class by hand.

Work per pointer move while dragging, phone

Everything the browser does for one move of a knob or a key (the average of the two), as each move redraws the panel.

  1. Haxe, first version21.2 ms
  2. Haxe11.8 ms
  3. React13.8 ms
  4. Vue12.0 ms
  5. Angular11.5 ms

This is the surprise, below.

The surprise

The first Haxe version was the slowest to drag: about 21 ms per pointer move on the slowed phone, against about 13 ms for the frameworks.

It was redrawing two bits of text, the status line and the names of the 808 knobs, on every change, even when they had not changed. Writing the same text into a page still replaces it, and the browser then works out the layout of the whole page again. React, Vue and Angular skip unchanged writes for you without being asked. Plain DOM code has to do it itself.

The fix was three lines: only write text that differs. After it, the Haxe version became the cheapest of the four to drag. Both versions are in the chart above, because finding that is the reason to measure at all.

Conclusions

Lightest: plain DOM code, by a long way. The Haxe version adds 4.2 kB to what every build shares. Vue adds 24 kB, Angular 48 kB and React 66 kB. A framework has to send its own runtime before it can do anything, while Haxe compiles away and leaves only the JavaScript the page actually runs.

Quickest to appear: plain DOM again. Its panel is ordinary HTML, so a phone on a slow connection shows it in under 2 seconds. The frameworks build the panel from JavaScript, so it can't appear until their code has downloaded and run: about 5 seconds. Rendering on the server would close most of that gap, at the cost of a server.

Most responsive: a tie, once plain DOM is written with care. After the fix, the Haxe version does as little work per drag as the best of the frameworks, and next to nothing per step of the playhead, because each change goes straight to the one thing on the page that changed. Vue and Angular's fine-grained updates do about as well, within the noise of the measurements; React's re-rendering does a little more work on every step and every drag. The first Haxe version shows the catch: without a framework, avoiding wasted work is your job, and it's easy to miss until you measure.

What doesn't change: the sound. The engine schedules every hit on the audio clock, so all four play in exactly the same time, and on a desktop computer you can't feel any difference between them.

So which is best? For a small, very interactive piece like this one, where every kilobyte and millisecond counts on a phone, plain DOM code is the lightest and the quickest to appear, as long as someone careful writes it. In a larger app with a team, I'd pick Vue: the lightest framework here, the least code, and efficient updates without having to think about them. Angular brings the most structure and by far the most tooling. React's huge ecosystem comes with the heaviest runtime and the most work per update.

These are the results for one small app, measured in a headless browser with a slowed-down processor standing in for a phone. They're a guide, not a law.

All the code, the tests and the measuring scripts are on GitHub, so you can check every number yourself.

Version 2: one new feature, four times

Real apps change, so for version 2 I gave bümbap a feature from my volca: eight pattern buttons and a sequence to play them in. Each button holds a whole pattern, every step with its level, nudge and flip, and every part's sound. Tap one to edit it, even while the sequence plays on. Three small buttons by the title do the rest: Select (tap patterns in the order they should play, repeats allowed, and each shows its place; a new sequence takes over at the end of the bar), Copy and Clear. Shortcuts too: shift-click to add to the sequence, on a phone hold one and tap others, and drag one onto another to copy it.

Then I measured what the feature cost each front end, against version 1; the numbers are in the table below.

Most of the work went where it belongs: into the engine. Which pattern plays next, the switch at the end of the bar, what Select, Copy and Clear do with a tap, a link that carries all eight patterns (and still opens version 1 links), and even the buttons' gestures are written once, in Haxe. So each front end only had to draw the buttons and pass on their taps.

With that, the four finished close together. Vue needed the fewest lines again, then Angular and React. The Haxe version needed the most, because its markup lives in its own HTML page, and it got a small helper that skips unchanged writes: the lesson from version 1. Each build grew by a few hundred bytes.

Nothing broke in any framework. The one bug the shared browser tests found was in the engine (a shared link asked for a clap recording that doesn't exist), so one fix mended all four. That's the case for keeping the logic in one place: a feature costs most once, and only a little per front end.

What the patterns cost each front end

Front-end linesCode added
Haxe+99 / −0+0.4 kB
React+76 / −1+0.5 kB
Vue+53 / −1+0.5 kB
Angular+60 / −2+0.7 kB
Shared by all four: 603 lines in the engine, and 2.8 kB more engine code.

Building each one

Haxe. The quickest to write, and the only one compiled together with the engine's own source. When I rename something in the engine, the front end fails to compile instead of failing in the browser. The compiler caught two mistakes before anything ran. The price: performance details like the one above are yours to find.

React. The engine hands out frozen snapshots, which is exactly what React's useSyncExternalStore wants, so no glue code was needed. JSX grows quickly, and the fifteen-line rule split it into many small components. React's own wheel events can't stop the page from scrolling, so every knob adds a listener of its own.

Vue. The least code of the four. A shallowRef is the whole adapter to the engine, and templates carry the markup directly. Its type checker doesn't run on the new TypeScript 7 yet.

Angular. The most setup and by far the biggest install, but signals made the state code as short as Vue's. It was the only build to fail tests on its first run: line breaks in a template had put spaces into the part names, and without zone.js, knobs read a stale value between two quick key presses.

Code and tools

UI code linesClean buildPackages installed
Haxe6710.4 s7
React5671.0 s32
Vue4473.7 s53
Angular5256.4 s218

Sounds: Michael Fischer's free TR-808 sample set (1994). Engine and front ends written with Claude, an AI coding assistant, to my design, spec and code rules.