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

SceneDisplayNativeSpaceFaster
Editor1×5.23 ms0.66 ms7.9 times
Editor2×7.16 ms0.85 ms8.4 times

First-frame latency

SceneDisplayNativeSpaceFaster
UI1×5.28 ms0.84 ms6.3 times
UI2×7.45 ms0.95 ms7.9 times
Editor1×9.58 ms1.92 ms5.0 times
Editor2×12.88 ms1.98 ms6.5 times
Document1×100.23 ms38.03 ms2.6 times
Document2×110.89 ms37.83 ms2.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

FileChunksLevels
1 KB11
1 MB1,0244
100 MB102,4006
1 GB1,048,5767

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

Edit1 KB1 MB100 MB1 GB
Insert a character64 ns133 ns178 ns211 ns
Delete a character187 ns264 ns330 ns375 ns
Insert a line break307 ns371 ns416 ns443 ns
Delete a line break318 ns405 ns470 ns522 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

Edit1 KB1 MB100 MB1 GB
Insert a character64 ns146 ns886 ns1,282 ns
Delete a character147 ns299 ns921 ns1,271 ns
Find the row and column of an offset14 ns49 ns84 ns114 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

Edit1 KB1 MB100 MB1 GB
Insert a character at the cursor9.7 µs10.1 µs9.9 µs10.0 µs
Delete a character at the cursor12.6 µs13.2 µs13.0 µs13.2 µs
Insert a line break at the cursor11.7 µs11.8 µs12.0 µs12.2 µs
Delete a line break at the cursor16.5 µs16.4 µs16.8 µs17.1 µs
Insert a character at a random place22.1 µs33.5 µs33.8 µs35.7 µs
Delete a character at a random place25.2 µs31.8 µs31.6 µs33.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

FileTime 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

CountNEONWord at a timePlain loop
Characters12 ns43 ns141 ns
Line breaks12 ns47 ns143 ns
UTF-16 units35 ns117 ns290 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

CountNEONWord at a timePlain loop
Characters63.6 GB/s25.2 GB/s7.2 GB/s
Line breaks61.1 GB/s22.3 GB/s7.3 GB/s
UTF-16 units23.5 GB/s13.7 GB/s3.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 cursor1 KB1 MB100 MB
Insert a character2% faster2% slower1% slower
Delete a character13% faster10% faster8% faster
Insert a line break7% faster7% faster6% faster
Delete a line break7% faster6% faster5% 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).