Skip to content

mixin_markdown_widget: measure text less and highlight code before the first frame - #525

Merged
YeungKC merged 9 commits into
mainfrom
mixin_markdown_widget/pretext-measure-cache
Oct 16, 2026
Merged

YeungKC merged 9 commits into
mainfrom
mixin_markdown_widget/pretext-measure-cache

Conversation

@YeungKC

@YeungKC YeungKC commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This makes mixin_markdown_widget lay out text with fewer measurements, and lets an app parse and highlight a document before the widget builds. It also prepares the 0.3.3 release.

Changes

  • Share pretext width, line height, and tight text metrics measurements, measure each segment once per run, and merge plain fragments of a line.
  • Add MarkdownController.parseDocument and a document argument to MarkdownController. parseDocument uses no Flutter bindings, so an app can parse in an isolate.
  • Add MarkdownController.highlightCodeBlocks and MarkdownController.cacheCodeHighlights. An app can highlight the code blocks of a parsed document in an isolate, then cache the results on the UI isolate.
  • Keep in-run widths in their own cache entry, apart from widths of segments measured alone.
  • Show cached code highlights in the first frame. Before, a code block first showed plain code, and the highlight then rebuilt the whole document.
  • Bump the version to 0.3.3 and add the changelog entry.

No existing API changes.

Performance

Measured in Mixin desktop on Linux (i7-8850H), profile build:

  • The first layout of a post preview with inline code: about 20 ms before, about 8 ms when new characters appear, and less after that.
  • The first layout of a chat post: about 1400 cache lookups and 235 text layouts before, about 390 lookups and 154 layouts after.
  • Scrolling the shared media post list while it loads a page: the worst frame went from 16.4–21.7 ms (8 document rebuilds and 78 paragraph relayouts) to 7.33 ms, with the app's background highlight.

The background highlight costs more time in the isolate: 20 posts take 27.8 ms instead of 16.8 ms.

To compare branches, run flutter test benchmark/pretext_layout_benchmark.dart in packages/mixin_markdown_widget.

Risks

  • A segment that misses the cache now gets its width from one layout of its whole run, not from a layout of the segment alone. The two widths differ only when a font shapes across a segment edge. Segments break only at whitespace and at CJK characters, so kerning, ligatures, and Arabic joining stay inside one segment. With Noto Sans, Noto Sans CJK, and Noto Naskh Arabic, 171 segments of Latin, CJK, mixed, and Arabic text had the same width both ways (difference 0.000 px), while kerning was active (AV is 0.64 px narrower than A plus V). In-run widths have their own cache entry, so a layout never depends on which width was measured first.
  • The measurement cache is shared by all documents and holds at most 20000 entries. It clears when it is full and when the system fonts change.
  • Code highlight results are cached by source and language. The theme is applied when the cached result is shown, so a theme change still restyles the code.

Testing

  • flutter analyze: no issues.
  • flutter test: 192 tests pass. New tests cover parsing into a controller, the first-frame highlight, and highlight preparation in an isolate.

Related

A pretext layout measured every word and CJK character with its own
text painter, and measured the fragments of each line again, up to
three times, while it fitted the line. Each measurement used a new
cache, and line heights had no cache. One post preview with inline code
took about 20 ms to lay out on a slow Linux desktop.

Segment widths and line heights depend only on the text, style, scale,
and padding, so layouts now share one cache. The cache clears when the
system fonts change and when it reaches 20000 entries. Layout results
do not change. A post preview now takes about 8 ms the first time its
characters appear, and less after that.

Add benchmark/pretext_layout_benchmark.dart.
Inline code fragments measured their tight text metrics with a new text
painter on every layout. On macOS the first measurement of new glyphs
parses their outlines, and this was the largest cost of a post preview
with inline code.

Tight text metrics depend only on the text, style, and scale, so they
use the shared measurement cache too. Layout results do not change.
A pretext layout measured every CJK character and word on its own, and
measured every fragment of each line again up to four times. Plain
fragments carried zero padding, so they missed the cache entries of
their segments, and each lookup hashed a TextStyle.

- Segments of a run take their widths from one layout of the run.
- A plain fragment of a whole segment keeps the segment width, and zero
  padding shares the cache entry of no padding.
- A line measures its width once, and fragments of one style share one
  line height lookup.

On a Linux desktop the first layout of a chat post made about 1400 cache
lookups and 235 text layouts in one frame; it now makes about 390
lookups and 154 layouts.
Each CJK character is a fragment of its line, and each fragment became
its own text span, so a line of Chinese text had dozens of style runs to
shape. Adjacent plain fragments with the same style, cursor and
recognizer now share one span. The text and its offsets are the same,
and a screenshot of a chat post is pixel-identical.
Add MarkdownController.parseDocument, which parses data as a new
controller does and uses no Flutter bindings, and a document argument
that skips the parse. An app can then parse in an isolate, before the
widget that shows the document builds.
A width measured inside a run can differ from a width of the segment alone when a font shapes across the segment edge. Both used one cache entry, so a layout used whichever width was measured first. In-run widths now have their own entry.
@YeungKC
YeungKC merged commit 21b60d1 into main Oct 16, 2026
9 checks passed
@YeungKC
YeungKC deleted the mixin_markdown_widget/pretext-measure-cache branch October 16, 2026 05:52
@YeungKC

YeungKC commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Mixin desktop uses this release in MixinNetwork/flutter-app#2070.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants