Performance #

The design invariant is that re-presenting the tail is unconditionally cheap — cheap enough to do every frame with no dirty tracking. Measured on the library's benchmark scenario (a streaming chat app on a 100×40 terminal, release build):

scenariocost per frame
unchanged tail, 10KB of markdown showing~780µs, ~2.3K allocs
one streamed chunk arriving~900µs
a keystroke~790µs
a burst of 64 queued chunks~1.7ms total (one frame)
building the element tree0.5µs, 16 allocs

The pipeline already does the heavy lifting for you: expensive built-ins cache within the frame, the engine skips diffing rows that have scrolled past the reachable region, and the tokio driver coalesces queued messages into one frame per batch — a fast LLM stream costs one present per burst, not one per chunk.

What's yours to manage #

Per-frame cost is O(tail content). The frame costs above scale linearly with how much lives in the tail. The fix is architectural, not micro: push content when it's final. An app that seals each turn as it completes has a tail that stays small no matter how long the session runs. At ordinary sizes the cost is negligible — 10KB of live markdown at animation cadence is roughly 1% of a core — so this matters when tails get genuinely large, not as a habit of fear.

Cache expensive custom elements within the frame. Elements are rebuilt per frame, and height runs more than once per frame (measure, then container placement). If your element parses, wraps, or lays out anything non-trivial, share that work across calls with a RefCell<Option<…>> — no invalidation needed, the cache dies with the frame. The built-in markdown() is the reference implementation of this pattern.

View builders are not the place to optimize. Tree construction measured at half a microsecond and sixteen allocations for a realistic tail. Cloning a String into a text() is fine. (This is also why there's no arena allocator: the thing it would optimize is already 0.06% of the frame.)

Measuring #

The regression harness ships in the repo: benches/frame.rs (criterion) and examples/perf_report.rs, which prints exact, deterministic allocation counts per scenario via a counting global allocator. If you're changing render-path code — yours or the library's — run the report before and after; the numbers don't wiggle.