Yesterday I got nerd sniped into missing a workout.

Frankenpenguin | Repository

[2:30 PM] I discovered a blog post about vibe coded performance benchmarks for drawing a large number of rectangles on the HTML canvas. It was an interesting read and I was nodding along until I saw that all the Typescript benchmarks were better than the Rust ones. That can’t be right?

[3:00 PM] My hunch was that most of the time was spent crossing the boundary between Rust and Javascript. This is something I’ve been burned by in the past.

[3:30 PM] I decoupled the work scheduling from the sync compute and rendering [1]. Deleted a few lines of code that were creating the render loop and recursively calling it from the request_animation_frame. And then moved the scheduling to Javascript.

BEFORE
// Rust updates the rectangle positions
//      renders to canvas
//      updates the fps counter
//      recursively schedules next render loop

┌─────────────────┐                       ┌───────────────────────────────────┐
│  Javascript     │                       │  Rust                             │
│                 │                       │                                   │
│                 │                       │  ┌─────────────────────────────┐  │
│                 │                       │  │ start                       │  │
│                 │                       │  │                             │  │
│                 │                       │  │ ┌──────────────────────┐    │  │
│                 │                       │  │ │ start_animation_loop │    │  │
│                 │                       │  │ │                      │◀┐  │  │
│                 │  wasm_bindgen(start)  │  │ └──────┬───────────────┘ │  │  │
│                 │  ──────────────────▶  │  │        │                 │  │  │
│                 │                       │  │        ▼                 │  │  │
│                 │                       │  │ ┌──────────────────────┐ │  │  │
│                 │                       │  │ │ render               │ │  │  │
│                 │                       │  │ │                      │ │  │  │
│                 │                       │  │ └──────┬───────────────┘ │  │  │
│                 │                       │  │        │                 │  │  │
│                 │                       │  │        └─────────────────┘  │  │
│                 │                       │  └─────────────────────────────┘  │
└─────────────────┘                       └───────────────────────────────────┘
AFTER
// JavaScript calls the `frankenpenguin.tick()`

// Rust       updates the rectangle positions
//            renders to canvas

// JavaScript updates the fps counter
//            recursively schedules the ticks

┌────────────────────────────────────┐                   ┌────────────────────┐
│  JavaScript                        │                   │ Rust               │
│                                    │                   │                    │
│  ┌────────────────────────────┐    │                   │                    │
│  │ render                     │◀─┐ │                   │                    │
│  │                            │  │ │                   │                    │
│  │ ┌───────────────────────┐  │  │ │                   │ ┌──────────────┐   │
│  │ │                       │  │  │ │                   │ │ tick         │   │
│  │ │                       │  │  │ │                   │ │              │   │
│  │ │                       │  │  │ │                   │ └──┬───────────┘   │
│  │ │                       │  │  │ │                   │    ▼               │
│  │ │ frankenPenguin.tick() │────────────────────────▶  │ ┌──────────────┐   │
│  │ │                       │  │  │ │                   │ │ render       │   │
│  │ │ requestAnimationFrame │─────┘ │                   │ │              │   │
│  │ │                       │  │    │                   │ └──────────────┘   │
│  │ └───────────────────────┘  │    │                   │                    │
│  └────────────────────────────┘    │                   │                    │
└────────────────────────────────────┘                   └────────────────────┘

[3:45 PM] I run the benchmarks. Hmmmm, it’s 2x faster, not bad. I was mistakenly comparing the updated Rust + WebGL implementation to the TS + WebGL version.

[4:00 PM] The train is unusually late.

[4:38 PM] I get to the gym, but the person turns me away as I’m too late [2].

[5:21 PM] I push the repository to GitHub.

[This morning] I realize that I was comparing the wrong benchmarks and the diff resulted in a 10x bump in perf.

Aside 1: To schedule async work in the browser you can pass a callback to the requestAnimationFrame API. As Rust doesn’t ship with a runtime, scheduling work requires using the closest one avaiable. In the browser it ends up being… the browser. So you have to use a crate that would provide the bindings to the requestAnimationFrame API.

Aside 2: I ended with playing Table Tennis afterwards so it’s all good.