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. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 0.00 |
| GPUI Fast | 0.42 (0.41–0.42) | |
| MyGo | 0.00 | |
| Rune | 0.00 | |
| Streaming | zgx | 6.3 (6.1–6.4) |
| GPUI Fast | 13.3 (12.5–13.6) | |
| MyGo | 14.8 (14.8–15.4) | |
| Rune | 18.3 (17.5–18.7) | |
| Scrolling | zgx | 2.7 (2.4–2.8) |
| GPUI Fast | 7.2 (6.6–7.8) | |
| MyGo | 12.7 (11.6–13.1) | |
| Rune | 17.9 (17.6–18.6) | |
| Both | zgx | 6.6 (5.8–7.0) |
| GPUI Fast | 13.3 (11.8–14.6) | |
| MyGo | 15.1 (14.8–15.4) | |
| Rune | 21.2 (20.9–21.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 4.3 (3.4–5.2) |
| GPUI Fast | 0.3 | |
| MyGo | 2.0 (0.3–3.7) | |
| Rune | 3.2 (0.2–6.1) | |
| Streaming | zgx | 10.4 (10.3–10.8) |
| GPUI Fast | 10.2 (10.0–14.2) | |
| MyGo | 11.3 (11.0–12.3) | |
| Rune | 9.9 (9.9–10.0) | |
| Scrolling | zgx | 10.7 (10.1–11.4) |
| GPUI Fast | 9.9 (9.8–10.0) | |
| MyGo | 11.1 (10.6–11.5) | |
| Rune | 10.0 (9.9–10.0) | |
| Both | zgx | 10.6 (10.5–10.7) |
| GPUI Fast | 10.0 (9.8–11.4) | |
| MyGo | 11.1 (11.1–11.2) | |
| Rune | 9.8 (9.8–10.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 128.3 (127.5–129.5) |
| GPUI Fast | 166.9 (166.0–178.4) | |
| MyGo | 225.8 (224.9–228.6) | |
| Rune | 488.0 (480.7–491.0) | |
| Scrolling | zgx | 82.9 (81.6–87.4) |
| GPUI Fast | 166.9 (164.2–168.9) | |
| MyGo | 115.5 (113.9–116.4) | |
| Rune | 490.1 (487.4–491.6) | |
| Both | zgx | 154.5 (153.4–154.7) |
| GPUI Fast | 167.5 (165.9–171.5) | |
| MyGo | 226.7 (226.1–228.7) | |
| Rune | 489.3 (479.3–489.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 17.2 (17.2–17.5) |
| GPUI Fast | 22.0 (21.9–22.1) | |
| MyGo | 25.8 (24.9–26.1) | |
| Rune | 46.1 (45.5–46.3) | |
| Scrolling | zgx | 12.6 (12.3–12.7) |
| GPUI Fast | 22.4 (22.3–22.5) | |
| MyGo | 17.0 (16.3–17.5) | |
| Rune | 46.0 (46.0–46.2) | |
| Both | zgx | 19.2 (19.1–19.6) |
| GPUI Fast | 22.0 (21.6–22.3) | |
| MyGo | 26.0 (25.4–26.2) | |
| Rune | 46.2 (44.9–46.3) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 18.6 (17.9–21.1) |
| GPUI Fast | 25.3 (25.2–25.3) | |
| MyGo | 21.3 (17.8–25.6) | |
| Rune | 25.3 (18.5–35.4) | |
| Scrolling | zgx | 17.9 (17.8–20.3) |
| GPUI Fast | 25.0 (24.9–25.4) | |
| MyGo | 20.1 (18.3–25.5) | |
| Rune | 19.0 (18.8–24.1) | |
| Both | zgx | 17.8 (17.7–18.1) |
| GPUI Fast | 25.4 (25.3–25.6) | |
| MyGo | 20.9 (20.6–25.2) | |
| Rune | 22.0 (19.5–33.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 0.0 |
| GPUI Fast | 2.9 (2.4–3.8) | |
| MyGo | 0.4 (0.0–3.5) | |
| Rune | 2.8 (0.0–11.0) | |
| Scrolling | zgx | 0.0 (0.0–0.2) |
| GPUI Fast | 0.6 (0.6–2.9) | |
| MyGo | 0.2 (0.0–3.5) | |
| Rune | 0.0 (0.0–0.2) | |
| Both | zgx | 0.0 |
| GPUI Fast | 3.3 (2.9–3.4) | |
| MyGo | 0.2 (0.2–1.6) | |
| Rune | 0.4 (0.2–11.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Idle | zgx | 53 |
| GPUI Fast | 71 | |
| MyGo | 69 (66–73) | |
| Rune | 75 | |
| Streaming | zgx | 264 |
| GPUI Fast | 264 (264–281) | |
| MyGo | 308 (307–308) | |
| Rune | 478 (477–479) | |
| Scrolling | zgx | 260 |
| GPUI Fast | 263 (263–274) | |
| MyGo | 297 (297–298) | |
| Rune | 632 (631–634) | |
| Both | zgx | 265 (265–266) |
| GPUI Fast | 265 (265–278) | |
| MyGo | 308 (307–308) | |
| Rune | 630 (630–631) |
Text and a 100,000-row list
A streaming transcript beside a 100,000-row virtual list, updated 60 times a second. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 2.5 (2.2–2.5) |
| GPUI Fast | 5.0 (4.7–5.2) | |
| MyGo | 7.2 (7.0–7.3) | |
| Rune | 6.0 (6.0–6.4) | |
| Scrolling | zgx | 2.2 (2.2–2.3) |
| GPUI Fast | 4.0 (3.5–4.2) | |
| MyGo | 5.3 (5.3–5.8) | |
| Rune | 5.8 (5.6–6.4) | |
| Both | zgx | 2.8 (2.6–2.8) |
| GPUI Fast | 5.3 (4.8–5.5) | |
| MyGo | 7.7 (7.3–7.8) | |
| Rune | 6.8 (6.3–7.2) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 10.5 (10.4–13.8) |
| GPUI Fast | 10.3 (9.7–12.5) | |
| MyGo | 11.5 (10.8–14.1) | |
| Rune | 10.3 (9.6–12.6) | |
| Scrolling | zgx | 10.6 (10.3–13.8) |
| GPUI Fast | 10.3 (9.4–13.3) | |
| MyGo | 11.0 (11.0–13.2) | |
| Rune | 10.4 (9.6–10.5) | |
| Both | zgx | 10.5 (10.4–10.6) |
| GPUI Fast | 9.6 (9.4–10.0) | |
| MyGo | 10.9 (10.9–11.0) | |
| Rune | 9.7 (9.6–10.7) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 58.5 (57.0–58.6) |
| GPUI Fast | 108.2 (107.1–109.7) | |
| MyGo | 68.9 (68.8–69.1) | |
| Rune | 309.8 (303.0–315.2) | |
| Scrolling | zgx | 61.6 (61.4–62.4) |
| GPUI Fast | 106.1 (105.7–107.4) | |
| MyGo | 98.0 (97.9–99.3) | |
| Rune | 306.1 (301.3–307.0) | |
| Both | zgx | 67.1 (66.6–67.5) |
| GPUI Fast | 108.3 (107.4–108.6) | |
| MyGo | 105.2 (104.1–105.6) | |
| Rune | 315.4 (309.5–319.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 11.8 (11.5–11.9) |
| GPUI Fast | 18.4 (18.1–18.7) | |
| MyGo | 12.4 (12.4–12.5) | |
| Rune | 33.7 (33.0–34.3) | |
| Scrolling | zgx | 12.7 (12.3–12.9) |
| GPUI Fast | 18.4 (17.4–18.6) | |
| MyGo | 15.8 (15.6–15.8) | |
| Rune | 33.4 (32.3–33.6) | |
| Both | zgx | 13.2 (13.1–13.7) |
| GPUI Fast | 18.8 (18.7–18.9) | |
| MyGo | 16.7 (16.5–16.7) | |
| Rune | 33.9 (33.8–34.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 18.3 (17.8–18.3) |
| GPUI Fast | 23.3 (18.4–25.0) | |
| MyGo | 25.0 (17.7–25.1) | |
| Rune | 33.5 (31.1–34.1) | |
| Scrolling | zgx | 18.2 (18.1–18.5) |
| GPUI Fast | 18.3 (17.9–18.4) | |
| MyGo | 25.1 (17.6–25.2) | |
| Rune | 34.0 (33.8–35.2) | |
| Both | zgx | 18.9 (18.3–21.4) |
| GPUI Fast | 18.5 (18.2–19.6) | |
| MyGo | 18.0 (17.5–25.2) | |
| Rune | 35.0 (26.6–39.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 0.0 |
| GPUI Fast | 0.8 (0.2–1.0) | |
| MyGo | 0.8 (0.0–1.6) | |
| Rune | 10.9 (10.4–12.8) | |
| Scrolling | zgx | 0.0 (0.0–0.2) |
| GPUI Fast | 0.2 (0.0–0.2) | |
| MyGo | 1.2 (0.0–1.6) | |
| Rune | 16.2 (9.6–19.7) | |
| Both | zgx | 0.0 (0.0–0.2) |
| GPUI Fast | 0.4 (0.2–0.4) | |
| MyGo | 0.0 (0.0–2.8) | |
| Rune | 20.3 (5.5–23.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Streaming | zgx | 243 (242–244) |
| GPUI Fast | 246 (246–256) | |
| MyGo | 272 | |
| Rune | 424 (421–432) | |
| Scrolling | zgx | 241 |
| GPUI Fast | 248 (243–255) | |
| MyGo | 262 | |
| Rune | 428 (419–430) | |
| Both | zgx | 243 (243–244) |
| GPUI Fast | 246 (246–247) | |
| MyGo | 272 (271–272) | |
| Rune | 432 (432–433) |
Animation
48 blinking dots and a shimmering line of text, animated on every refresh of a 120 Hz display. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 3.7 (3.7–3.8) |
| GPUI Fast | 5.9 (5.5–5.9) | |
| MyGo | 7.2 (7.1–7.2) | |
| Rune | 7.0 (6.9–7.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 18.1 (18.1–18.3) |
| GPUI Fast | 21.1 (18.3–21.2) | |
| MyGo | 23.4 (22.9–26.5) | |
| Rune | 20.4 (17.8–21.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 122.1 (117.9–125.4) |
| GPUI Fast | 146.5 (144.6–146.9) | |
| MyGo | 116.0 (111.8–121.7) | |
| Rune | 300.7 (294.6–304.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 23.0 (22.9–23.0) |
| GPUI Fast | 30.1 (29.8–30.1) | |
| MyGo | 23.4 (23.1–23.5) | |
| Rune | 43.8 (43.8–43.9) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 14.5 (13.8–15.3) |
| GPUI Fast | 17.3 (13.0–17.8) | |
| MyGo | 17.7 (12.7–17.8) | |
| Rune | 17.4 (17.4–27.0) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 0.0 (0.0–0.1) |
| GPUI Fast | 0.0 | |
| MyGo | 0.0 | |
| Rune | 0.0 (0.0–1.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Animating | zgx | 241 (240–241) |
| GPUI Fast | 253 | |
| MyGo | 263 (263–264) | |
| Rune | 426 (423–426) |
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. Raw data
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 2.5 (2.4–2.5) |
| zgx (Luau) | 2.6 (2.5–2.6) | |
| GPUI Fast | 4.7 (4.3–4.9) | |
| MyGo | 8.9 (8.1–9.2) | |
| Rune | 7.4 (6.9–7.7) | |
| Flinging | zgx | 2.7 (2.3–2.8) |
| zgx (Luau) | 2.7 (2.6–2.8) | |
| GPUI Fast | 4.8 (4.7–5.0) | |
| MyGo | 9.4 (8.0–9.9) | |
| Rune | 8.3 (7.6–8.3) | |
| Jumping | zgx | 2.7 (2.7–2.9) |
| zgx (Luau) | 2.8 (2.7–2.8) | |
| GPUI Fast | 5.2 (4.8–5.3) | |
| MyGo | 10.0 (9.4–10.4) | |
| Rune | 9.6 (9.5–10.1) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 10.3 (10.2–10.3) |
| zgx (Luau) | 10.4 (10.3–10.5) | |
| GPUI Fast | 9.6 (9.4–9.9) | |
| MyGo | 11.3 (11.2–11.3) | |
| Rune | 9.8 (9.7–9.9) | |
| Flinging | zgx | 10.4 (10.1–10.8) |
| zgx (Luau) | 10.4 (10.3–10.9) | |
| GPUI Fast | 10.8 (10.5–11.2) | |
| MyGo | 11.4 (11.1–13.0) | |
| Rune | 9.9 (9.9–10.0) | |
| Jumping | zgx | 10.3 (10.0–10.5) |
| zgx (Luau) | 10.2 (10.1–10.3) | |
| GPUI Fast | 9.6 (9.4–9.9) | |
| MyGo | 11.2 (11.1–11.3) | |
| Rune | 9.5 (9.5–9.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 60.5 (58.8–60.9) |
| zgx (Luau) | 61.3 (59.1–61.5) | |
| GPUI Fast | 89.7 (87.7–90.3) | |
| MyGo | 94.9 (94.5–95.1) | |
| Rune | 246.1 (243.3–247.0) | |
| Flinging | zgx | 60.7 (59.5–62.6) |
| zgx (Luau) | 61.4 (59.2–62.3) | |
| GPUI Fast | 90.6 (89.6–92.7) | |
| MyGo | 95.8 (93.3–98.0) | |
| Rune | 248.7 (241.9–249.0) | |
| Jumping | zgx | 62.1 (60.3–62.3) |
| zgx (Luau) | 61.3 (59.8–61.9) | |
| GPUI Fast | 89.6 (88.8–91.0) | |
| MyGo | 95.7 (94.4–98.4) | |
| Rune | 244.6 (244.0–248.6) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 13.2 (12.9–13.4) |
| zgx (Luau) | 13.2 (13.2–13.3) | |
| GPUI Fast | 17.2 (17.0–17.4) | |
| MyGo | 15.9 (15.9–16.0) | |
| Rune | 29.4 (29.3–29.5) | |
| Flinging | zgx | 13.4 (13.2–13.5) |
| zgx (Luau) | 13.2 (13.1–13.4) | |
| GPUI Fast | 17.1 (16.9–17.1) | |
| MyGo | 15.9 (15.0–16.1) | |
| Rune | 29.3 (29.2–29.3) | |
| Jumping | zgx | 13.2 (13.0–13.5) |
| zgx (Luau) | 13.2 (13.1–13.3) | |
| GPUI Fast | 17.2 (17.2–17.3) | |
| MyGo | 15.8 (15.3–16.1) | |
| Rune | 29.4 (29.3–29.5) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 18.1 (17.9–18.4) |
| zgx (Luau) | 18.2 (17.7–18.8) | |
| GPUI Fast | 17.7 (17.5–18.2) | |
| MyGo | 18.1 (18.0–19.4) | |
| Rune | 25.4 (25.4–34.3) | |
| Flinging | zgx | 18.2 (18.1–20.8) |
| zgx (Luau) | 18.4 (18.2–18.5) | |
| GPUI Fast | 19.0 (18.6–23.5) | |
| MyGo | 18.5 (17.8–25.2) | |
| Rune | 35.3 (25.6–41.1) | |
| Jumping | zgx | 18.4 (17.9–18.4) |
| zgx (Luau) | 18.8 (18.5–19.0) | |
| GPUI Fast | 18.6 (17.7–18.8) | |
| MyGo | 20.6 (18.2–25.4) | |
| Rune | 34.0 (33.3–40.8) |
| Mode | Framework | Median (range) |
|---|---|---|
| Scrolling | zgx | 0.0 |
| zgx (Luau) | 0.0 (0.0–0.2) | |
| GPUI Fast | 0.0 (0.0–0.2) | |
| MyGo | 0.0 (0.0–0.2) | |
| Rune | 2.7 (2.0–21.3) | |
| Flinging | zgx | 0.2 (0.0–0.2) |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.2 (0.0–0.6) | |
| MyGo | 0.4 (0.2–3.4) | |
| Rune | 17.6 (4.6–19.1) | |
| Jumping | zgx | 0.0 (0.0–0.2) |
| zgx (Luau) | 0.0 | |
| GPUI Fast | 0.2 (0.0–0.2) | |
| MyGo | 0.4 (0.0–2.1) | |
| Rune | 15.9 (7.4–18.3) |
| 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 | 244 |
| zgx (Luau) | 247 (246–247) | |
| GPUI Fast | 281 (281–290) | |
| MyGo | 263 (262–263) | |
| Rune | 450 | |
| Flinging | zgx | 245 (244–245) |
| zgx (Luau) | 248 | |
| GPUI Fast | 288 (285–293) | |
| MyGo | 264 (263–264) | |
| Rune | 452 (451–452) | |
| Jumping | zgx | 245 |
| zgx (Luau) | 248 | |
| GPUI Fast | 282 | |
| MyGo | 268 (267–269) | |
| Rune | 454 |
Microbenchmarks
One state change, and one scroll step of 10,000 rows, timed headless in process CPU per operation (the median over batches after a warm-up) from the change through to a frame ready to present. Each framework's frame is compared with the frames that do the same work: GPUI Fast's headless frame stops before the GPU encode, so it sits beside zgx's frame without one; Rune's and MyGo's include it (MyGo's also rasterizes on the CPU), beside zgx's with it. How they are defined
| Frame | Framework | Median (p10–p90) |
|---|---|---|
| Without the GPU encode | zgx | 1.9 (1.7–2.2) |
| zgx (Luau) | 2.4 (2.2–2.5) | |
| GPUI Fast | 675 (660–685) | |
| With the GPU encode | zgx | 81.6 (75.6–123.0) |
| zgx (Luau) | 60.7 (56.2–81.3) | |
| Rune | 1,424 (1,384–1,454) | |
| MyGo | 482 (461–495) |
| Frame | Framework | Median (p10–p90) |
|---|---|---|
| Without the GPU encode | zgx | 3.5 (3.1–4.1) |
| zgx (Luau) | 4.2 (3.8–4.9) | |
| GPUI Fast | 26.5 (24.5–30.1) | |
| With the GPU encode | zgx | 91.2 (79.3–100.7) |
| zgx (Luau) | 113 (100–191) | |
| Rune | 210 (126–231) | |
| MyGo | 208 (200–304) |
| Frame | Framework | Median (p10–p90) |
|---|---|---|
| Without the GPU encode | zgx | 6.9 (0.8–11.5) |
| zgx (Luau) | 9.7 (0.8–14.9) | |
| GPUI Fast | 119 (98–137) | |
| With the GPU encode | zgx | 112 (87–137) |
| zgx (Luau) | 109 (86–143) | |
| Rune | 209 (174–278) | |
| MyGo | 431 (356–509) |
- The change alone ("write") is not compared: GPUI Fast, Rune and MyGo store the value and mark the window to draw (0.02–0.14 µs), leaving all their work to the frame, where zgx's bindings update their nodes as the value is set (about 1–8 µs). The frame is the figure that counts, and it includes the write.
- MyGo's frame rasterizes its scene on the CPU, as its headless tester does, where a window would draw on the GPU; it is shown at a raster scale of 0.05× (the charts' MyGo), which leaves its layout and paint and makes the raster almost free (at 1×: state update 468 µs, scroll step, fixed rows 481 µs, scroll step, variable rows 976 µs).
- Most of a frame with the GPU encode is what any Metal frame costs: a bare Metal frame (a command buffer, one empty render pass, commit) takes 44–94 µs of process CPU back to back on this Mac, most of it in Metal's own threads; zgx's work over it is the few microseconds of its frame without the encode and about 33,000 instructions of drawing and presenting.
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 (release, fat LTO, one codegen unit), 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 at998bef58(dev, MIT), profile dist (Rune's perf with fat LTO and codegen-units 1) (Rune's own bench scripts use perf), and a 14-line patch exposing its wheel path (see below). Every framework uses the system allocator. zgx at1cd5211. - Trials: each a fresh launch, the first 5 s after its window was ready (frontmost and uncovered) 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 its window was ready. 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.
- 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 (the desktop alone, measured each repeat, is beside each chart's title).
- 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; beside each chart's title). 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, about half a millisecond after the GPU idles, so an app that draws small frames often reads high: measured with Metal System Trace, zgx's animation frames execute in 46 µs against GPUI Fast's 318 µs, yet this counter charges zgx more. It also moves with whatever else keeps the GPU awake. 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 for 8 s in 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.
- The list widgets: zgx's measured virtual list, the same list from a Luau script (
ui.list { measured = true }), and GPUI Fast'slistmeasure each row as they lay it out; MyGo'sui.Listkeeps its place by an anchor row and estimates unmeasured rows at the average height; Rune's virtual rows estimate unmeasured rows (28 pt fixed, a three-line row variable) and measure as they mount.
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.
Caveats
- Since 2026-10-26 a trial’s sample starts from its window being ready (frontmost and uncovered, polled up to 2.5 s), not from its launch, and each app runs 3 s longer; this run is therefore not ranked against earlier ones, which measured from launch.
- Compare within this run only: the same binaries measured hours apart read differently (the machine's state changes), 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. Which level a launch lands on is the driver's timing, not the app's, so 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.
- WindowServer spends more CPU on zgx’s window than on the cheapest rival’s in 9 of 10 tests, by at most 0.8 points of a core (its chart follows CPU in each scene).
- GPU power is within the trials’ spread, or higher for zgx, in: animation (animating) against MyGo.
- Until 2026-10-25 these pages' GPU figure was app GPU time; it is now GPU power, which measures what the GPU spends rather than how long it was held (see How this was measured). In the animation, zgx uses 24% less than GPUI Fast, 2% less than MyGo, 48% less than Rune on app GPU time and 17% less than GPUI Fast, 5% more than MyGo, 59% less than Rune on power.
- No app exited abnormally during the run.
- 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.
Data
- This page's numbers: data.csv, data.json, and the microbenchmarks’ micro.jsonl.
- Every trial, raw:
docs/results/suite-macos-2026-10-26-1cd5211/heavy(52 trials, 36 pacing captures)docs/results/suite-macos-2026-10-26-1cd5211/lab(45 trials, 36 pacing captures)docs/results/suite-macos-2026-10-26-1cd5211/animate(15 trials, 12 pacing captures)docs/results/suite-macos-2026-10-26-1cd5211/vlist(54 trials, 45 pacing captures)
- How the runs are made: benchmarking; the adapters: comparisons; the code: github.com/zeronsh/zgx.