How We Measured
Space draws text with its own renderer, built into its fork of the GPUI framework. It draws each glyph on the GPU straight from its outline, using the Slug algorithm. GPUI’s original renderer, which the chart calls Native, has Core Graphics draw each glyph on the CPU at every new size, then keeps the result in a texture.
We timed both renderers on the same Mac, with the same text.
The setup
- Mac: Apple M3 Pro
- System: macOS 27
- Date: October 1, 2026
- Displays: each scene drawn at 1× and at 2× (Retina)
The scenes
- UI: app controls, a sidebar and 25 form rows in IBM Plex Sans at 12 to 20 points, about 2,000 glyphs in a 1440 × 900 window.
- Editor: two panes of Rust code with a file tree and a terminal, in Lilex at 12 and 13 points, about 3,800 glyphs in a 1440 × 900 window.
- Document: six columns of English, code and Chinese at 10 to 32 points, about 13,700 glyphs in a 2560 × 1440 window.
- Zoom: the Editor scene, made 1% larger in every frame for 200 frames, so no size repeats.
What we timed
Each number is the time to draw one frame: the CPU’s work and the GPU’s work, added together. It is the median frame of three passes, each of 30 frames (200 for Zoom).
First-frame latency is that time for a frame that draws text at sizes not drawn before, as when text first appears. In the UI, Editor and Document runs, every frame is such a frame. In the Zoom run, every frame is a new size, as when you zoom.
Zooming
| Scene | Display | Native | Space | Faster |
|---|---|---|---|---|
| Editor | 1× | 5.23 ms | 0.66 ms | 7.9 times |
| Editor | 2× | 7.16 ms | 0.85 ms | 8.4 times |
First-frame latency
| Scene | Display | Native | Space | Faster |
|---|---|---|---|---|
| UI | 1× | 5.28 ms | 0.84 ms | 6.3 times |
| UI | 2× | 7.45 ms | 0.95 ms | 7.9 times |
| Editor | 1× | 9.58 ms | 1.92 ms | 5.0 times |
| Editor | 2× | 12.88 ms | 1.98 ms | 6.5 times |
| Document | 1× | 100.23 ms | 38.03 ms | 2.6 times |
| Document | 2× | 110.89 ms | 37.83 ms | 2.9 times |
The UI scene at 1× varied more between runs than the others, by up to 36%.
Text already drawn
When the text on screen was already drawn at the same size, both renderers took about the same time. That is why the home page says “up to”: its number is the best Zooming run, where every frame draws text at a new size.
Editing
Space keeps a file’s text in a rope: a tree whose leaves are chunks of up to 1,024 bytes of text, and whose nodes have up to 16 children each. An edit changes one chunk and the counts on the path from that chunk up to the root. The tree gains a level only when the file grows many times larger, so the path stays short.
Inside a chunk, the rope counts characters, lines and UTF-16 units with the str_indices library. With its simd feature on, str_indices reads 16 bytes at a time with NEON, the SIMD instructions of Apple silicon. This is how fast a chunk is read and counted. It is a small part of an edit.
Levels of the tree
| File | Chunks | Levels |
|---|---|---|
| 1 KB | 1 | 1 |
| 1 MB | 1,024 | 4 |
| 100 MB | 102,400 | 6 |
| 1 GB | 1,048,576 | 7 |
These are the levels of a rope built from the file’s text, with the row of chunks counted as one level. We read them from a build of the rope that reports its depth. The drawing on the home page is a sketch: each node shows five of its up to 16 children, and the number of levels is the true one.
Editing setup
- Mac: Apple M3 Pro
- System: macOS 27.2
- Date: November 7, 2026
- Build: Rust 1.99.1
- Text: lines like
let value_7 = compute(7, "text") * 2;, all ASCII, cut to files of 1 KB, 1 MB, 100 MB and 1 GB
Editing method
An edit is one character or one line break, put in or taken out. At the cursor means 200 characters typed one after another in the middle of the file, then deleted again. At a random place means each edit lands at a new random offset in the file.
Each number is the time of one edit: the median of 401 batches of 200 edits (201 batches at 100 MB and 1 GB), after 21 batches of warm-up. The tables show the median of three runs. The runs of one number differed by up to 10% at the cursor and up to 19% at a random place.
The text buffer numbers are the median of 60 batches of 100 edits. The counting and NEON numbers are medians too, from three runs.
Rope edit at the cursor
| Edit | 1 KB | 1 MB | 100 MB | 1 GB |
|---|---|---|---|---|
| Insert a character | 64 ns | 133 ns | 178 ns | 211 ns |
| Delete a character | 187 ns | 264 ns | 330 ns | 375 ns |
| Insert a line break | 307 ns | 371 ns | 416 ns | 443 ns |
| Delete a line break | 318 ns | 405 ns | 470 ns | 522 ns |
The home page shows the first row. The time is not flat: the file grows a million times and the time about 3.3 times, as the tree gains levels.
Rope edit at a random place
| Edit | 1 KB | 1 MB | 100 MB | 1 GB |
|---|---|---|---|---|
| Insert a character | 64 ns | 146 ns | 886 ns | 1,282 ns |
| Delete a character | 147 ns | 299 ns | 921 ns | 1,271 ns |
| Find the row and column of an offset | 14 ns | 49 ns | 84 ns | 114 ns |
From 100 MB on, an edit at a random place takes several times as long as one at the cursor. In a 1 GB file it takes about 1.3 µs.
Text buffer edit
| Edit | 1 KB | 1 MB | 100 MB | 1 GB |
|---|---|---|---|---|
| Insert a character at the cursor | 9.7 µs | 10.1 µs | 9.9 µs | 10.0 µs |
| Delete a character at the cursor | 12.6 µs | 13.2 µs | 13.0 µs | 13.2 µs |
| Insert a line break at the cursor | 11.7 µs | 11.8 µs | 12.0 µs | 12.2 µs |
| Delete a line break at the cursor | 16.5 µs | 16.4 µs | 16.8 µs | 17.1 µs |
| Insert a character at a random place | 22.1 µs | 33.5 µs | 33.8 µs | 35.7 µs |
| Delete a character at a random place | 25.2 µs | 31.8 µs | 31.6 µs | 33.4 µs |
This is the editor’s text buffer, which edits the rope and also keeps the fragments, the undo history and the anchors. At the cursor it takes about 10 to 17 µs at every size, which is 40 to 60 times the rope’s own edit. The three runs of one number differed by up to 63%, and by under 10% in half of them, mostly in the 1 KB column.
A whole keystroke
| File | Time per character |
|---|---|
| 1,000 rows (about 48 KB) | 2.15 ms |
This types one character per frame into that file, and the time is the median of three runs. Each round is the key press, the edit, the layout, the drawing and a Metal draw, one after the other with no wait for the display. It is not the delay you see. At 120 Hz a display frame lasts 8.3 ms, so the frame, not the edit, sets what you see.
Counting in one chunk
| Count | NEON | Word at a time | Plain loop |
|---|---|---|---|
| Characters | 12 ns | 43 ns | 141 ns |
| Line breaks | 12 ns | 47 ns | 143 ns |
| UTF-16 units | 35 ns | 117 ns | 290 ns |
These are the times to count one 1 KB chunk of the text, through the rope’s own functions (len_chars, len_lines, len_utf16). Word at a time is the same rope with simd off, which reads 8 bytes at once. The plain loop is a simple loop over the bytes. rustc already turns it partly into SIMD code; with that switched off it takes 173 ns for the characters.
Counting in a long text
| Count | NEON | Word at a time | Plain loop |
|---|---|---|---|
| Characters | 63.6 GB/s | 25.2 GB/s | 7.2 GB/s |
| Line breaks | 61.1 GB/s | 22.3 GB/s | 7.3 GB/s |
| UTF-16 units | 23.5 GB/s | 13.7 GB/s | 3.7 GB/s |
The same counts over 64 MB of text. Over a single 1 KB chunk the speeds with NEON are 85.8, 85.8 and 29.2 GB/s.
What NEON adds to an edit
| Edit at the cursor | 1 KB | 1 MB | 100 MB |
|---|---|---|---|
| Insert a character | 2% faster | 2% slower | 1% slower |
| Delete a character | 13% faster | 10% faster | 8% faster |
| Insert a line break | 7% faster | 7% faster | 6% faster |
| Delete a line break | 7% faster | 6% faster | 5% faster |
These time the rope directly, with simd on and off. The simd feature controls str_indices; the memchr library has its own NEON code, which stays on in both. A delete and a line break read the whole chunk again, which NEON does faster. A plain insert only counts the new byte, so NEON changes nothing. Building a rope from 64 MB of text runs at 3.03 GB/s with NEON and 2.69 GB/s without, which is 13% faster (15 runs each).