WYSIWYG surface: alternatives measured
Superseded (#247).
flutter_quill,re_editorand the old preview are gone: every mode is now Niman’s own surface (unified-surface.md). This page is kept as the record of what was measured before that decision; the files it links undereditor/wysiwyg/and the M2 benchmark it names no longer exist (they are in the git history).
The WYSIWYG surface is flutter_quill behind Niman’s own Markdown codec
(editor/wysiwyg/). A Delta document is not Markdown, so the codec pays
for the round trip: ~530 lines across
markdown_document_codec.dart
and markdown_blocks.dart,
plus the opaque embeds that keep tables, math, wikilinks, footnotes and
frontmatter read-only.
That cost makes replacements look attractive, so they get proposed. This
records what was measured, on 2026-09-18, so the next proposal starts
from numbers instead of from the README of the week. Verdict: none of
the six replaces flutter_quill today.
The bench
Everything measured here used
m2_spike_benchmark_test.dart’s
fixture — test/fixtures/markdown/fixture-200kb.md, 206 241 chars,
6289 lines — and the same shape of test: first frame after pumping the
widget, then the best of five mid-document single-character edits.
The spike branches (spike/smooth-markdown-editor,
spike/live-markdown-editor) were deleted after this was written; their
benches cannot live on main because the dependencies are gone. To redo
one: add the package, copy the fixture-loading and _firstFrame helpers
from the M2 benchmark, and print rather than assert.
Numbers are debug-mode, inside the test harness, on one Windows machine. They are inflated in absolute terms; only the ratios between rows carry.
Baseline: what the app already does
From T-M2-00, re-run on the same machine and day:
| Tokenize 200 KB from cold | 23.95 ms |
| Incremental edit, mid-buffer | 0.507 ms |
| Line layout + paint | 44.7 µs/line (only visible lines are paid for) |
| Whole buffer as a single span | 52.0 ms |
The 0.507 ms is the number every candidate has to be read against. It is
O(change), not O(document), because
highlighting.dart re-tokenizes
only what moved.
Measured, and rejected
flutter_smooth_markdown 0.10.0
Already a declared dependency — but dead: nothing in lib/ imports it,
only the M2 benchmark does. Its 0.10.0 added
flutter_smooth_markdown_editor, which keeps Markdown source as the
document of record (no Delta, no codec).
SmoothMarkdown eager preview, 200 KB |
2769.2 ms (0.8.1 measured 2781.5 ms) |
| Editor, formatted, 20 KB | 37.3 ms |
| Editor, formatted, 200 KB | 110.6 ms |
| Editor, source, 200 KB | 125.3 ms |
| Keystroke, 200 KB | 72.2 ms |
Two findings. The preview widget is still eager and has not improved
in two releases, which confirms the M2 rejection that
wysiwyg_editor.dart
refers to. The editor is not — its formatted mode builds block
segments through a ListView.separated, so ten times the document costs
three times the open, not ten.
Rejected anyway, on correctness: formatted mode throws RangeError out
of _sourceRangeForDocumentBlock at both 20 KB and 200 KB — indexOf
called with a start past the end of the source (20669 of 0..20624).
That is the block-to-source mapping the “source of record” promise rides
on. Source mode alone is clean. The 72 ms keystroke would rule it out
independently.
live_markdown_editor 0.11.0
Source-preserving by design, Raw/Live/Read modes, its own editable engine
(no Material in lib/), two dependencies, MIT, 160/160 pub points
against this exact toolchain.
| Live, 20 KB | 355.3 ms |
| Live, 200 KB | 1329.1 ms |
| Raw, 200 KB | 405.0 ms |
| Read, 200 KB | 862.0 ms |
| Source survives byte for byte | yes (206 241 = 206 241 chars) |
It is the only candidate that got the contract right: every mode renders, nothing throws, and the text leaves the controller identical character for character — where flutter_smooth_markdown broke on exactly that.
Rejected on the keystroke, which costs the whole document:
| 2 KB | 20 KB | 200 KB |
|---|---|---|
| 6.2 ms | 33.1 ms | 422.4 ms |
Clean O(n), about 2 ms per KB: the parse runs over everything on every
edit. Under ~5 KB it is comfortable; 20 KB — an ordinary note — already
lags. Against 0.507 ms this is a different complexity class, not a
constant factor to tune.
Worth revisiting if it ever parses incrementally. It was 26 days old and had 3 likes when measured, so it needs a track record regardless.
Rejected on inspection, without a bench
markdown_editor_live 0.6.0
A TextEditingController over a TextField, one file of substance,
6 likes. It hides syntax markers with fontSize: 0.0 and compensates
the caret nowhere: the markers stay in the text at their offsets, so
arrows walk invisible characters, backspace eats one * of a ** pair
(leaving text that re-parses as italic mid-correction), and selection
over a marker paints a zero-width rect. Its only offset mapping serves a
different hack — \n “virtual newlines” it injects into the
controller’s own text to make room for image widgets, which for an app
whose file is the truth is a way to write zero-width spaces into a note.
Rebuild is ~13 regexes over the whole document per keystroke, with an
O(n) offset map called per match. No hooks for the toolbar, find or
spell underlines; no links, blockquotes, task lists or tables.
appflowy_editor 6.2.0
The best model of the six — a tree of nodes with children, which maps
onto Markdown block structure far better than a flat Delta, plus
registrable NodeParsers that could make tables and math editable
instead of opaque. Rejected on everything around it:
- pub.dev reports
INCOMPLETE PACKAGE ANALYSIS, 60/160, platform support 0/20 and static analysis 0/50 against Flutter 3.47.4 / Dart 3.13.3 — this app’s toolchain. Last release was 9 months old;maintargets Flutter 3.38. Adopting it means pinning a git ref, with 117 open issues behind it. - Dual MPL-2.0 / AGPL-3.0 against this app’s MIT. MPL is the workable branch, but the choice would have to be deliberate and recorded.
- 21 direct dependencies, including
pdf,http,file_picker,universal_htmlandprovider— in an app on Riverpod with an encrypted sync story.
super_editor
Last stable release 0.2.7, two years old; only 0.3.0-dev.* since, and
the maintainers direct users to depend on the GitHub repo. Same git-pin
liability as appflowy_editor, without a published release to fall back
on.
Still open
fleather 1.28.0 is the one candidate not yet measured. It is the
Zefyr successor on the Parchment model — still Quill Delta underneath,
which is the point: the existing codec would port rather than be
rewritten, and parchment’s codecs library already ships a
ParchmentMarkdownCodec to compare against. MIT/BSD-3, 193 likes,
13.3k weekly downloads, 160/160 points, and a dependency list an order of
magnitude lighter than appflowy_editor’s.
flutter_quill itself was never put on this bench. So what is
established is that these candidates are slower than the source editor,
not that they are slower than what the app ships. The 200 KB cap in
wysiwyg_editor.dart
exists because Quill is not cheap either. Measuring the incumbent — mount
WysiwygEditor on this fixture, reusing the setup in
wysiwyg_editor_test.dart —
is what would turn “worse” into “a regression” or “a wash”, and it is the
first thing the next evaluation should do.