Results
zgx against GPUI Fast, MyGo and Rune on one Mac, in one session, the same four scenes built from the same workload code.
zgx uses the least CPU in 10 of 10 tests and the least GPU power in 9, against GPUI Fast, MyGo and Rune, and none of its frames run long.
- CPU, heavy dashboard (both)
- 9.0%
- GPUI Fast 43% lessMyGo 48% lessRune 59% less
- GPU power, heavy dashboard (both)
- 154 mW
- GPUI Fast 7% lessMyGo 32% lessRune 68% less
- Long frames (over 25 ms)
- 0%
- GPUI Fast, MyGo and Rune: up to 20.6%
- CPU while idle
- 0.00%
- GPUI Fast 0.37%MyGo 0.00%Rune 0.00%
GPUI Fast is Zed’s GPUI with its fast-path optimizations, as apps ship it. MyGo is a Go desktop framework, measured through its GPU-drawn native UI. Rune is built on Rust, wgpu, Taffy and Parley. Every figure on this page is read at build time from the run that scripts/build_site.py accepted, after it recomputed each median and range from the trials and checked them against the run’s summaries.
Heavy dashboard
A 1280×800 dashboard: 6 tiles, 30 cards of 26 sparkline bars, a status table and a log. Most values change 60 times a second; the table scrolls. 50 trials. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 0.00 |
| GPUI Fast | 0.37 | |
| MyGo | 0.00 | |
| Rune | 0.00 | |
| Streaming | zgx | 8.5(trials 8.5 to 8.7) |
| GPUI Fast | 14.9(trials 14.5 to 15.8) | |
| MyGo | 16.2(trials 16.0 to 16.5) | |
| Rune | 19.9(trials 19.9 to 20.8) | |
| Scrolling | zgx | 2.9(trials 2.9 to 3.0) |
| GPUI Fast | 8.1(trials 7.1 to 9.1) | |
| MyGo | 13.5(trials 12.4 to 13.6) | |
| Rune | 19.8(trials 18.9 to 20.5) | |
| Both | zgx | 9.0(trials 8.0 to 9.3) |
| GPUI Fast | 15.9(trials 14.4 to 17.2) | |
| MyGo | 17.4(trials 17.2 to 17.8) | |
| Rune | 22.2(trials 21.8 to 22.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 128.6(trials 128.0 to 129.6) |
| GPUI Fast | 164.3(trials 163.8 to 165.3) | |
| MyGo | 228.2(trials 223.6 to 230.1) | |
| Rune | 485.1(trials 484.9 to 486.9) | |
| Scrolling | zgx | 80.4(trials 80.4 to 82.6) |
| GPUI Fast | 164.3(trials 163.6 to 165.0) | |
| MyGo | 111.7(trials 111.5 to 111.8) | |
| Rune | 484.5(trials 476.8 to 484.9) | |
| Both | zgx | 154.3(trials 152.4 to 158.5) |
| GPUI Fast | 165.1(trials 164.1 to 165.8) | |
| MyGo | 225.5(trials 225.1 to 226.5) | |
| Rune | 485.9(trials 484.1 to 486.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 18.3(trials 17.9 to 18.9) |
| GPUI Fast | 25.1(trials 24.9 to 25.3) | |
| MyGo | 18.9(trials 18.4 to 25.3) | |
| Rune | 24.1(trials 24.0 to 33.8) | |
| Scrolling | zgx | 17.8(trials 17.7 to 18.0) |
| GPUI Fast | 25.2(trials 25.2 to 25.4) | |
| MyGo | 17.8(trials 17.6 to 17.8) | |
| Rune | 33.7(trials 33.3 to 33.8) | |
| Both | zgx | 17.9(trials 17.8 to 18.0) |
| GPUI Fast | 25.1(trials 25.0 to 25.3) | |
| MyGo | 18.2(trials 17.6 to 24.9) | |
| Rune | 25.3(trials 23.8 to 33.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 0.0(trials 0.0 to 0.4) |
| GPUI Fast | 1.6(trials 0.8 to 2.0) | |
| MyGo | 0.6(trials 0.0 to 1.8) | |
| Rune | 0.4(trials 0.0 to 8.1) | |
| Scrolling | zgx | 0.0 |
| GPUI Fast | 2.2(trials 1.8 to 2.7) | |
| MyGo | 0.2 | |
| Rune | 8.4(trials 8.3 to 16.6) | |
| Both | zgx | 0.0 |
| GPUI Fast | 1.6(trials 1.2 to 2.1) | |
| MyGo | 0.2(trials 0.0 to 0.6) | |
| Rune | 1.9(trials 0.2 to 8.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 54(trials 54 to 55) |
| GPUI Fast | 71 | |
| MyGo | 75(trials 66 to 84) | |
| Rune | 76(trials 74 to 79) | |
| Streaming | zgx | 259(trials 257 to 261) |
| GPUI Fast | 270(trials 258 to 273) | |
| MyGo | 297(trials 295 to 297) | |
| Rune | 467(trials 464 to 467) | |
| Scrolling | zgx | 254(trials 252 to 254) |
| GPUI Fast | 267(trials 253 to 269) | |
| MyGo | 287(trials 286 to 288) | |
| Rune | 616(trials 612 to 616) | |
| Both | zgx | 259(trials 256 to 260) |
| GPUI Fast | 264(trials 256 to 272) | |
| MyGo | 296(trials 295 to 296) | |
| Rune | 613(trials 612 to 615) |
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 0.2 |
| GPUI Fast | 1.4(trials 0.2 to 2.6) | |
| MyGo | 0.2 | |
| Rune | 2.9(trials 0.2 to 5.7) | |
| Streaming | zgx | 10.0(trials 9.9 to 11.2) |
| GPUI Fast | 9.4(trials 9.3 to 9.6) | |
| MyGo | 10.6(trials 10.5 to 13.7) | |
| Rune | 9.7(trials 9.4 to 9.7) | |
| Scrolling | zgx | 9.7(trials 9.6 to 9.9) |
| GPUI Fast | 9.4(trials 9.3 to 9.7) | |
| MyGo | 10.4(trials 10.2 to 10.4) | |
| Rune | 9.6(trials 9.4 to 9.9) | |
| Both | zgx | 9.9(trials 9.8 to 13.3) |
| GPUI Fast | 9.4(trials 9.0 to 9.5) | |
| MyGo | 10.4(trials 10.1 to 10.5) | |
| Rune | 9.4(trials 9.1 to 10.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 18.5(trials 18.5 to 18.7) |
| GPUI Fast | 22.1(trials 22.0 to 22.1) | |
| MyGo | 26.2(trials 24.3 to 26.3) | |
| Rune | 46.4(trials 46.3 to 46.5) | |
| Scrolling | zgx | 12.6(trials 12.5 to 12.7) |
| GPUI Fast | 22.3(trials 22.2 to 22.5) | |
| MyGo | 15.7(trials 15.5 to 15.8) | |
| Rune | 46.1(trials 45.3 to 46.4) | |
| Both | zgx | 19.7(trials 18.9 to 19.9) |
| GPUI Fast | 22.1(trials 22.0 to 22.1) | |
| MyGo | 25.9(trials 25.5 to 26.1) | |
| Rune | 46.3(trials 46.2 to 46.4) |
Text and a 100,000-row list
A streaming transcript beside a 100,000-row virtual list, updated 60 times a second. 41 trials. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 2.8(trials 2.8 to 2.9) |
| GPUI Fast | 5.6(trials 4.8 to 5.8) | |
| MyGo | 8.6(trials 8.4 to 9.0) | |
| Rune | 7.3(trials 6.9 to 7.6) | |
| Scrolling | zgx | 2.4(trials 2.3 to 2.5) |
| GPUI Fast | 4.4(trials 4.1 to 4.5) | |
| MyGo | 6.6(trials 6.4 to 6.7) | |
| Rune | 6.5(trials 5.9 to 6.9) | |
| Both | zgx | 3.1(trials 3.0 to 3.1) |
| GPUI Fast | 5.6(trials 5.6 to 5.7) | |
| MyGo | 8.6(trials 8.5 to 9.1) | |
| Rune | 7.6(trials 7.2 to 7.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 57.9(trials 57.7 to 61.3) |
| GPUI Fast | 107.2(trials 106.3 to 107.8) | |
| MyGo | 64.5(trials 64.2 to 67.3) | |
| Rune | 312.1(trials 306.8 to 316.0) | |
| Scrolling | zgx | 62.5(trials 61.7 to 62.7) |
| GPUI Fast | 104.5(trials 104.0 to 105.7) | |
| MyGo | 98.9(trials 98.6 to 99.3) | |
| Rune | 305.4(trials 302.6 to 306.9) | |
| Both | zgx | 67.0(trials 65.7 to 67.1) |
| GPUI Fast | 107.6(trials 106.4 to 107.8) | |
| MyGo | 103.8(trials 103.0 to 105.0) | |
| Rune | 314.9(trials 311.4 to 316.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 18.1(trials 17.9 to 18.4) |
| GPUI Fast | 17.9(trials 17.8 to 18.2) | |
| MyGo | 18.2(trials 18.1 to 25.3) | |
| Rune | 33.8(trials 33.8 to 40.4) | |
| Scrolling | zgx | 18.1(trials 18.0 to 18.2) |
| GPUI Fast | 18.0(trials 17.8 to 18.8) | |
| MyGo | 25.3(trials 18.4 to 25.5) | |
| Rune | 33.8(trials 33.7 to 34.0) | |
| Both | zgx | 18.1(trials 17.9 to 18.2) |
| GPUI Fast | 18.0(trials 18.0 to 18.4) | |
| MyGo | 18.1(trials 18.0 to 19.0) | |
| Rune | 32.0(trials 26.1 to 41.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 0.0 |
| GPUI Fast | 0.2(trials 0.0 to 0.4) | |
| MyGo | 0.6(trials 0.2 to 2.1) | |
| Rune | 17.0(trials 15.3 to 20.0) | |
| Scrolling | zgx | 0.0 |
| GPUI Fast | 0.4(trials 0.0 to 0.4) | |
| MyGo | 2.7(trials 0.2 to 6.1) | |
| Rune | 14.5(trials 14.2 to 17.1) | |
| Both | zgx | 0.0 |
| GPUI Fast | 0.4(trials 0.2 to 0.6) | |
| MyGo | 0.0(trials 0.0 to 0.2) | |
| Rune | 8.4(trials 5.2 to 25.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 236(trials 234 to 243) |
| GPUI Fast | 247(trials 235 to 254) | |
| MyGo | 263(trials 262 to 270) | |
| Rune | 407(trials 405 to 432) | |
| Scrolling | zgx | 234(trials 232 to 236) |
| GPUI Fast | 236(trials 236 to 243) | |
| MyGo | 253(trials 251 to 254) | |
| Rune | 417(trials 416 to 419) | |
| Both | zgx | 235(trials 234 to 236) |
| GPUI Fast | 237(trials 236 to 237) | |
| MyGo | 262(trials 261 to 263) | |
| Rune | 418(trials 418 to 419) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 9.9(trials 9.6 to 10.7) |
| GPUI Fast | 9.1(trials 9.0 to 9.6) | |
| MyGo | 10.0(trials 9.7 to 10.2) | |
| Rune | 9.5(trials 9.4 to 9.5) | |
| Scrolling | zgx | 9.6(trials 9.3 to 9.7) |
| GPUI Fast | 9.0(trials 9.0 to 9.6) | |
| MyGo | 10.4(trials 10.4 to 10.5) | |
| Rune | 9.7(trials 9.5 to 10.0) | |
| Both | zgx | 9.8(trials 9.7 to 9.8) |
| GPUI Fast | 9.2(trials 9.1 to 9.2) | |
| MyGo | 10.3(trials 10.2 to 10.3) | |
| Rune | 10.0(trials 9.7 to 10.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 11.7(trials 11.7 to 11.8) |
| GPUI Fast | 18.7(trials 18.6 to 18.8) | |
| MyGo | 12.4(trials 12.3 to 12.5) | |
| Rune | 34.0(trials 33.9 to 34.4) | |
| Scrolling | zgx | 12.5(trials 12.5 to 12.7) |
| GPUI Fast | 18.5(trials 18.4 to 18.6) | |
| MyGo | 16.1(trials 16.0 to 16.1) | |
| Rune | 33.5(trials 33.4 to 33.7) | |
| Both | zgx | 13.4(trials 13.4 to 13.6) |
| GPUI Fast | 18.8 | |
| MyGo | 15.2(trials 15.2 to 16.7) | |
| Rune | 34.3(trials 34.0 to 34.4) |
Animation
48 blinking dots and a shimmering line of text, animated on every refresh of a 120 Hz display. 16 trials. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 4.7(trials 4.6 to 4.8) |
| GPUI Fast | 6.3(trials 5.8 to 6.7) | |
| MyGo | 8.1(trials 8.0 to 8.2) | |
| Rune | 7.3(trials 7.2 to 7.9) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 115.9(trials 115.7 to 122.0) |
| GPUI Fast | 140.2(trials 127.6 to 144.0) | |
| MyGo | 111.4(trials 110.6 to 117.2) | |
| Rune | 294.8(trials 293.1 to 303.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 18.2(trials 17.9 to 18.9) |
| GPUI Fast | 12.9(trials 12.8 to 17.4) | |
| MyGo | 17.4(trials 16.8 to 17.5) | |
| Rune | 17.3(trials 17.2 to 17.4) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 0.0 |
| GPUI Fast | 0.0 | |
| MyGo | 0.0 | |
| Rune | 0.0 |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 246(trials 243 to 254) |
| GPUI Fast | 247(trials 245 to 252) | |
| MyGo | 253(trials 253 to 260) | |
| Rune | 410(trials 410 to 423) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 19.5(trials 19.3 to 19.7) |
| GPUI Fast | 19.7(trials 19.2 to 20.0) | |
| MyGo | 21.8(trials 21.7 to 21.9) | |
| Rune | 20.0(trials 19.9 to 20.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 22.8(trials 22.6 to 23.0) |
| GPUI Fast | 17.7(trials 16.6 to 30.1) | |
| MyGo | 23.4(trials 23.4 to 23.5) | |
| Rune | 44.0(trials 43.8 to 44.2) |
Variable-height list
100,000 chat rows of wrapped text, 1 to 8 lines each, about one in four with an image, in each framework's own list widget, which measures the rows: scrolled steadily, flung, and scrolled with a jump to a far row every half second. 50 trials. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 2.6(trials 2.6 to 2.8) |
| zgx (Luau) | 2.7(trials 2.7 to 2.9) | |
| GPUI Fast | 5.1(trials 5.1 to 5.2) | |
| MyGo | 10.0(trials 9.2 to 10.5) | |
| Rune | 7.9(trials 7.9 to 8.5) | |
| Flinging | zgx | 3.2(trials 3.0 to 3.3) |
| zgx (Luau) | 3.2(trials 3.2 to 3.4) | |
| GPUI Fast | 5.2(trials 5.2 to 5.5) | |
| MyGo | 10.5(trials 10.1 to 11.1) | |
| Rune | 8.9(trials 8.7 to 9.0) | |
| Jumping | zgx | 2.8(trials 2.7 to 2.9) |
| zgx (Luau) | 2.9(trials 2.9 to 3.0) | |
| GPUI Fast | 5.4(trials 5.2 to 5.5) | |
| MyGo | 10.7(trials 10.3 to 10.8) | |
| Rune | 10.6(trials 10.4 to 10.7) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 60.5(trials 60.4 to 60.7) |
| zgx (Luau) | 60.3(trials 59.9 to 60.5) | |
| GPUI Fast | 89.1(trials 89.0 to 89.3) | |
| MyGo | 93.9(trials 93.8 to 95.0) | |
| Rune | 244.5(trials 244.5 to 245.5) | |
| Flinging | zgx | 67.2(trials 67.1 to 67.9) |
| zgx (Luau) | 67.0(trials 66.4 to 67.3) | |
| GPUI Fast | 89.0(trials 88.9 to 89.9) | |
| MyGo | 94.3(trials 93.5 to 96.0) | |
| Rune | 240.1(trials 237.6 to 241.7) | |
| Jumping | zgx | 60.5(trials 60.4 to 60.5) |
| zgx (Luau) | 60.8(trials 60.6 to 60.9) | |
| GPUI Fast | 89.2(trials 88.6 to 89.5) | |
| MyGo | 95.1(trials 94.2 to 96.7) | |
| Rune | 241.6(trials 241.3 to 242.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 17.9(trials 17.9 to 18.1) |
| zgx (Luau) | 18.0(trials 17.8 to 18.4) | |
| GPUI Fast | 17.9(trials 17.8 to 17.9) | |
| MyGo | 18.4(trials 17.9 to 19.8) | |
| Rune | 32.7(trials 32.6 to 41.2) | |
| Flinging | zgx | 18.1(trials 18.0 to 18.5) |
| zgx (Luau) | 17.9(trials 17.9 to 18.0) | |
| GPUI Fast | 17.7 | |
| MyGo | 18.3(trials 17.9 to 25.3) | |
| Rune | 41.3(trials 41.2 to 41.8) | |
| Jumping | zgx | 18.1(trials 18.0 to 18.9) |
| zgx (Luau) | 18.3(trials 18.1 to 18.4) | |
| GPUI Fast | 25.2(trials 24.9 to 25.5) | |
| MyGo | 18.7(trials 18.1 to 25.3) | |
| Rune | 33.5(trials 33.4 to 33.7) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 0.0 |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.2 | |
| MyGo | 0.4(trials 0.2 to 0.4) | |
| Rune | 7.1(trials 6.8 to 19.6) | |
| Flinging | zgx | 0.0(trials 0.0 to 0.2) |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.4(trials 0.2 to 0.4) | |
| MyGo | 0.2(trials 0.2 to 1.2) | |
| Rune | 20.6(trials 16.8 to 22.2) | |
| Jumping | zgx | 0.0 |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 1.6(trials 0.4 to 4.4) | |
| MyGo | 0.4(trials 0.4 to 4.0) | |
| Rune | 7.1(trials 6.5 to 8.7) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 0.0 |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.0 | |
| MyGo | 0.0 | |
| Rune | 0.0 | |
| Jumping | zgx | 0.0 |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.0 | |
| MyGo | 0.0 | |
| Rune | 0.0 |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 237(trials 237 to 238) |
| zgx (Luau) | 240(trials 238 to 241) | |
| GPUI Fast | 272(trials 270 to 272) | |
| MyGo | 254(trials 254 to 255) | |
| Rune | 436(trials 436 to 441) | |
| Flinging | zgx | 238(trials 238 to 239) |
| zgx (Luau) | 241(trials 240 to 242) | |
| GPUI Fast | 281(trials 274 to 282) | |
| MyGo | 254(trials 253 to 255) | |
| Rune | 438(trials 438 to 442) | |
| Jumping | zgx | 239(trials 238 to 239) |
| zgx (Luau) | 242(trials 240 to 242) | |
| GPUI Fast | 272(trials 272 to 281) | |
| MyGo | 258(trials 257 to 259) | |
| Rune | 440(trials 440 to 442) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 9.6(trials 9.6 to 10.0) |
| zgx (Luau) | 9.8(trials 9.6 to 10.6) | |
| GPUI Fast | 9.0(trials 8.9 to 9.2) | |
| MyGo | 10.8(trials 10.7 to 11.0) | |
| Rune | 9.3(trials 9.3 to 9.4) | |
| Flinging | zgx | 9.6(trials 9.4 to 9.7) |
| zgx (Luau) | 10.1(trials 9.6 to 15.4) | |
| GPUI Fast | 9.2(trials 9.0 to 9.2) | |
| MyGo | 10.7(trials 10.7 to 11.1) | |
| Rune | 9.1(trials 9.0 to 9.5) | |
| Jumping | zgx | 9.6(trials 9.5 to 9.7) |
| zgx (Luau) | 9.5(trials 9.5 to 9.7) | |
| GPUI Fast | 9.0(trials 8.9 to 9.1) | |
| MyGo | 10.7(trials 10.6 to 10.8) | |
| Rune | 9.2(trials 8.8 to 9.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 13.2(trials 13.1 to 13.5) |
| zgx (Luau) | 13.2(trials 13.0 to 13.2) | |
| GPUI Fast | 17.3(trials 17.1 to 17.3) | |
| MyGo | 15.4(trials 14.7 to 16.0) | |
| Rune | 29.5(trials 29.4 to 29.5) | |
| Flinging | zgx | 14.5(trials 14.5 to 14.7) |
| zgx (Luau) | 14.5(trials 14.1 to 14.8) | |
| GPUI Fast | 17.4 | |
| MyGo | 15.2(trials 14.8 to 16.2) | |
| Rune | 28.9(trials 28.6 to 28.9) | |
| Jumping | zgx | 13.2(trials 13.2 to 13.3) |
| zgx (Luau) | 13.3(trials 13.2 to 13.6) | |
| GPUI Fast | 17.3(trials 16.8 to 17.3) | |
| MyGo | 15.2(trials 15.1 to 16.1) | |
| Rune | 29.0(trials 29.0 to 29.2) |
How this was measured
- Machine: Apple M5 Max (Mac17,6, 18 cores, 128 GB), macOS 26.4 (25E246), AC Power.
- Displays: 2 × Studio Display XDR (3200 x 1800 @ 120.00Hz); the window on the main display, nothing covering it.
- Builds: zgx and GPUI Fast with their
distprofile (dist (release, lto = "fat", codegen-units = 1)), rustc 1.96.0 (ac68faa20 2026-05-25); MyGo v0.6.3 (1524290, MIT) built with go1.27.3 darwin/arm64,go build -trimpath -ldflags=-s -w, cgo off; Rune at 998bef58 (dev, MIT), profile dist (Rune's perf with fat LTO and codegen-units 1), and a 14-line patch exposing its wheel path. Every framework uses the system allocator. zgx atc3c0dc9. - Trials: each a fresh launch, the first 5 s discarded and the next 10 s sampled every 50 ms; 3 trials of each mode, in alternating order, and 1 more of any whose CPU or GPU spread past 10% of its median; idle 2 times in the dashboard, 20–30 s after launch. Every trial had to run as long as asked, at 60 updates a second. Medians are shown, the trials' range beside them. Frame timing is 3 separate 8 s captures of each mode.
How Rune is driven
- Ticks through
AppHandle::update_if. Rune's per-framepoll, woken bywake_after, ran on frame boundaries up to 16 ms late and dropped about 5% of ticks. A timer thread now hands each tick to the UI thread throughupdate_if, Rune's API for a timer or feed, and waits for it to run, as the other adapters' loops do. This changes the measurement: Rune now does the same 60 ticks a second of work as the others. - Updates apply at once (
Tx::instant). Rune animates every layout and colour change on a 0.3 s spring by default. Each tick is applied instantly, as the other frameworks apply live data. Leaving the springs on would add animation frames the other frameworks don't draw. - sRGB blending (
App::srgb_blend(true)). Rune blends translucency in linear light by default, so 10% opacity looked brighter than in the others. Its documented sRGB mode matches CSS and the other three. It is a surface format and a shader flag, with no extra pass, so it changes pixels, not cost. - Heavy's panel contents sit 1 pt in. A Rune border takes no layout space, while CSS, which the scene's geometry follows, places absolute children inside the border. The children are offset by the border's width so that everything lands on the same pixels. Geometry only; the cost doesn't change.
- vlist scrolls through a small patch exposing Rune's own wheel path (
rune-wheel.patch, 14 lines). Rune's public API can't set a vertical scroll offset, and only user wheel input moves a list. The patch addsTx::wheel, which feeds the sameRuntime::wheel_xya real wheel event goes through, at once, between frames. Nothing else in Rune changes. This is what makes Rune's scroll and fling measurable; it adds no work of its own. - Jump reveals the row (nearest), not at the top. Rune's
scroll_into_viewscrolls just far enough to show a row, where the other adapters put the jumped-to row at the top. A jump still mounts and lays out the far rows, but the row lands at a different place in the viewport. - Rune's window isn't colour-matched by the window server. Its Metal layer is untagged (wgpu 30's default, which Rune keeps), so sRGB values reach the display unconverted and look slightly more saturated. The window server converts the other three, whose layers are tagged sRGB. Rune may skip that colour-matching work, which could lower its WindowServer cost. Kept as Rune ships it.
- The vlist probe runs headless. Row heights are read from Rune's headless runtime, with the same font and 720 pt width, after revealing each row. A row's height doesn't depend on where it sits. Only the layout check uses this; it doesn't feed any measured figure.
What each figure is
- CPU: the app process’s CPU time over the sample, as a percentage of one core (it can exceed 100).
- WindowServer CPU: the CPU of macOS’s compositor, which puts every app’s frames on screen, over the same sample: what showing the app costs the system beside the app’s own CPU. It includes what WindowServer does anyway.
- GPU power: the whole GPU’s energy over the sample, from the system’s energy model (IOReport), in milliwatts, less the desktop alone (measured each repeat). It counts everything the GPU does for the app, the window server’s compositing of its window included, so a framework is charged for how it presents its frames as well as for drawing them.
- App GPU time (secondary): the GPU time macOS accounts to the app’s process, as cycles at the GPU’s mean clock. It includes the time from each submission to the GPU starting it, so an app that draws small frames often reads high. Kept for comparison with earlier runs.
- Frame time: the 99th percentile of the intervals between frames shown on screen; at 60 updates a second, 16.7 ms is on time (8.3 ms for the animation, drawn at 120 Hz). Long frames are intervals over 25 ms.
- Unfilled rows (the list): the window captured 10 times a second while scrolling and jumping; a capture counts when a stretch of the rows’ padding over 24 points shows the window’s background, where no row was drawn.
- Memory: the process’s physical footprint (what Activity Monitor shows: resident memory plus GPU memory it owns), averaged over the sample.
Caveats
- Compare within this run only: the same binaries measured hours apart read differently, so a cell is not comparable across runs, while the frameworks in one run share its session.
- Idle memory is two-level: an idle window settles at one of two levels about 25 MB apart, launch to launch, as the Metal driver returns its startup memory. Read idle memory with its range.
- MyGo is immediate mode: each frame it draws builds and draws its whole window, which is how MyGo apps work.
- Binary size is not compared.
- One machine, one session; a personal Mac in use, other applications kept running (the run waited until the machine was quiet). Windows and Linux are not compared here.
The full protocol, its noise and every other series are in the methodology. The numbers themselves: data.json, data.csv, and every trial under docs/results.