Search page titles, headings and text across the docs.

Guides

On this page

Performance

Read this page to see what canvist costs: the time each operation takes, how that grows with document size, and how to keep your app fast.

The short version

  • Typing costs the same in a 10-page document and a 10,000-page one. A keystroke changes one block, and only that block is laid out again.
  • A command costs under a microsecond in Rust, and a few microseconds from TypeScript or Dart.
  • Loading or exporting 10,000 paragraphs takes milliseconds.
  • Collaboration is cheap. Applying a collaborator’s keystroke costs a few microseconds.

Performance budgets

Every number below has a budget, and a test fails the build when an operation goes over it. “Today” is measured on Apple silicon. Rows marked “Coming soon” belong to editor hosts that haven’t shipped yet.

MetricBudgetToday
Apply one keystroke (model), 100k blocks< 2 µs0.29 µs
Enter / Backspace across blocks, 100k blocks< 100 µs45 µs
Keystroke in a 100 KB paragraph< 5 µs0.94 µs
Relayout one edited paragraph, 10k or 100k blocks< 100 µs2.5 µs
Relayout after a keystroke in a 100 KB paragraph< 100 µs25 µs
Relayout after Enter, 10k / 100k blocks ¹< 200 µs / < 2 ms83 µs / 1.4 ms
Hit test / caret rectangle< 5 µs / < 2 µs0.8 µs / 0.01 µs
Display list for one screen< 100 µs5 µs
Scroll one screen into unmeasured blocks< 1 ms53 µs
Relayout after a web font loads, 10k blocks< 200 µs43 µs
Measure cache hit rate: resize / typing≥ 99% / ≥ 85%100% / 98.5%
Measure cache memory per segment, per face≤ 160 B144 B
Mirror a keystroke into a Yjs update, 100k blocks< 20 µs1.9 µs
Apply a collaborator’s keystroke, 100k blocks< 20 µs2.7 µs
Mirror Enter into Yjs, 10k blocks²< 150 µs45 µs
Apply a collaborator’s Enter, 10k blocks²< 400 µs114 µs
Join a 10k-paragraph document from Yjs state< 50 ms24 ms
Headless: load 10k paragraphs (Markdown / JSON)< 50 ms / < 50 ms10 ms / 17 ms
Headless: export 10k paragraphs (JSON / MD / HTML)< 50 / 25 / 25 ms19 / 7 / 2.4 ms
Headless: run one command, 10k paragraphs< 2 µs0.21 µs
Headless: create a StarterKit document< 250 µs56 µs
Headless: apply one Yjs keystroke, 10k paragraphs< 20 µs2.3 µs
Headless TypeScript: one command / Yjs update< 5 µs / < 30 µs0.71 µs / 4.6 µs
Headless TypeScript wasm module, after wasm-opt< 1,200 KB raw1,058 KB (433 KB gz)
Headless Dart: one command / Yjs update< 10 µs / < 30 µs2.0 µs / 3.5 µs
Keystroke → paint on the web, p95 ⁴< 8 ms0.5 / 2 / 2 ms
Typing burst: 1,000 key events on the web ⁴no crash, no dropped input, ≤ 2 frames late1,000 of 1,000
Open a 10k-paragraph document → first paint< 50 mslayout 0.6 ms
Web bundle (core + StarterKit), gzipped< 350 KB349.8 KB ³

¹ Pressing Enter grows slowly with document size, about 10 ns per block, because every later line moves down one row. It stays well under a frame at 100,000 blocks.

² Structural edits, such as splitting or joining blocks, cost time in proportion to the number of blocks beside them, because Yjs walks the list of siblings. Budgets use 10,000 top-level blocks.

³ Measured as an app ships it: a Vite build of a page that creates an editor, minified and gzipped (336 KB of WebAssembly, 14 KB of script). CI fails above the budget. The module is optimized without inlining, which gzips smaller even though it is larger raw. Line breaking’s Unicode data is 13.6 KB of it, and browsers offer no line-break segmenter to use instead.

⁴ Chromium / Firefox / WebKit on an Apple silicon Mac, measured by editor.inputLatency() in canvist’s Playwright tests: from each input event’s timestamp until the change is painted on the canvas, in the same task. Firefox and WebKit round timestamps to 1 ms. In the burst, every key is painted in its own task, so none waits for a later frame; the 95th percentile is 0.4 / 3 / 1 ms. The browser’s Event Timing API also counts the wait for the next frame, and reports only events that took 16 ms or more, so it can’t resolve an 8 ms budget. The tests record its slow entries beside these numbers.

Keep your app fast

  • Build one engine and share it. Creating an engine registers its extensions. A document from an existing engine takes about 60 µs.
  • Reuse a Renderer per thread. It caches text measurements, so the second render of similar text is much cheaper.
  • Store JSON. It keeps block ids, so collaborators and comments don’t have to re-anchor after a reload.
  • Mind the WebAssembly size. The TypeScript package’s module is 433 KB gzipped. Collaboration is most of it: the core without it is 196 KB.
  • Batch updates on the server. takeUpdates() returns everything since the last call, so you can send one message per tick instead of one per keystroke.

Measure it yourself

Every number in the table comes from a benchmark in the repository:

cargo bench -p canvist_headless --all-features
Docs