charmbracelet / charmbracelet/bubbletea
perf: cursedRenderer v2 scroll frames significantly slower than v1
- Dominant language
- Go
- Stars
- 44.9k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
**Is your feature request related to a problem? Please describe.**
bubbletea v2\x27s `cursedRenderer` processes scroll frames at ~300-800µs compared to
v1\x27s standard renderer at ~50-100µs. For TUI apps with continuous scrolling (log
viewers, code browsers, chat interfaces), this causes visible lag — the difference
is perceptible when scrolling through content.
The bottleneck is the full pipeline running on ALL lines every frame: ANSI parse →
cell buffer → `scrollOptimize` hashmap (10,000 cell hashes for 50×200) → cell diff →
emit. Most of this work is redundant during scroll: the majority of lines are simply
shifted, not changed.
**Describe the solution you\x27d like**
String-level scroll detection in `flush()` that:
1. Compares previous and current frame content lines to detect vertical shift
2. Strips identical leading/trailing chrome lines (padding, status bars, input area)
so apps with fixed UI regions benefit from the optimisation
3. Shifts the cell buffer in-place instead of clearing + full reparse
4. Calls `HardScroll` (from ultraviolet) for terminal scroll sequences, bypassing
the expensive `scrollOptimize` hashmap
5. Uses `DrawOver` (from ultraviolet) only on the N new/changed lines
6. Falls back to the original `Clear` + `Draw` path when no scroll is detected
**Describe alternatives you\x27ve considered**
- **Caching ANSI parse results**: high memory cost, complex invalidation across
style/resize changes
- **Line-level content hashing in scrollOptimize**: still O(n) per frame, and the
infrastructure already exists — the problem is it re-detects what we already know
- **This approach**: zero caching, pure algorithmic detection via string comparison.
O(height) comparison + O(shift) buffer manipulation + O(new_lines) ANSI parse
**Additional context**
Benchmarks (200-wide terminal, 50-line viewport):
| Benchmark | Before | After | Change |
|-----------|--------|-------|--------|
| FlushScroll | ~844µs | ~238µs | -72% |
| FlushScrollPartial | ~899µs | ~212µs | -76% |
| FlushScrollWithSuffix | ~895µs | ~157µs | -83% |
| FlushStatic | ~72ns | ~86ns | no regression |
| FlushFullChange | ~1.11ms | ~1.12ms | no regression |
Depends on charmbracelet/ultraviolet#129 / PR #130 (`DrawOver` + `HardScroll`).
**Setup**
- OS: Linux (AMD Ryzen 9 7940HS)
- Shell: bash
- Terminal: xterm-256color capable
- bubbletea: v2 main branch
Contributor guide
Research direction
Start at flush() and inspect the existing scrollOptimize, Clear, and Draw path. Verify ultraviolet#129/PR #130 provides HardScroll and DrawOver, then run or add the named scroll benchmarks to confirm faster scroll cases with no regression for static or fully changed frames.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100