on main help sign in
fix(ui): cache glamour renderers

The theme prep overhaul in 755f6fa made the main markdown renderer and thinking block markdown renderer heavier, which exasperated a gap in perf. Basically every token chunk would clear would clear the per-item render cache and force a re-render. This update memoizes renderers with markdown and should generally give us a nice increase in perf compared to what we had before the theme rendering.

commit19197e30864f22f1b7e29b9f005d3fe6d2bae728 authorChristian Rocha <christian@rocha.is> dateMon 4 May 2026, 11:38 (6 months ago) parent4010841
2 files changed+35−2
internal/ui/common/markdown.go+34−2 internal/ui/model/ui.go+1
internal/ui/common/markdown.go+34−2
@@ -2,6 +2,7 @@ package common
22
33import (
44 "image/color"
5+ "sync"
56
67 "charm.land/glamour/v2"
78 "github.com/alecthomas/chroma/v2/formatters"
@@ -18,24 +19,55 @@ func init() {
1819 formatters.Register(formatterName, xchroma.Formatter(zero, nil))
1920}
2021
22+var (
23+ mdCacheMu sync.Mutex
24+ mdCache = map[int]*glamour.TermRenderer{}
25+ quietMDCache = map[int]*glamour.TermRenderer{}
26+)
27+
2128// MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with
22−// the given styles and width.
29+// the given styles and width. Renderers are memoized per width and shared
30+// across callers; call InvalidateMarkdownRendererCache when the active
31+// styles change.
2332func MarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer {
33+ mdCacheMu.Lock()
34+ defer mdCacheMu.Unlock()
35+ if r, ok := mdCache[width]; ok {
36+ return r
37+ }
2438 r, _ := glamour.NewTermRenderer(
2539 glamour.WithStyles(sty.Markdown),
2640 glamour.WithWordWrap(width),
2741 glamour.WithChromaFormatter(formatterName),
2842 )
43+ mdCache[width] = r
2944 return r
3045}
3146
3247// QuietMarkdownRenderer returns a glamour [glamour.TermRenderer] with no colors
33−// (plain text with structure) and the given width.
48+// (plain text with structure) and the given width. Renderers are memoized per
49+// width and shared across callers.
3450func QuietMarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer {
51+ mdCacheMu.Lock()
52+ defer mdCacheMu.Unlock()
53+ if r, ok := quietMDCache[width]; ok {
54+ return r
55+ }
3556 r, _ := glamour.NewTermRenderer(
3657 glamour.WithStyles(sty.QuietMarkdown),
3758 glamour.WithWordWrap(width),
3859 glamour.WithChromaFormatter(formatterName),
3960 )
61+ quietMDCache[width] = r
4062 return r
4163}
64+
65+// InvalidateMarkdownRendererCache drops every cached renderer. Call this
66+// whenever the active styles change so subsequent renderers pick up the new
67+// ansi.StyleConfig.
68+func InvalidateMarkdownRendererCache() {
69+ mdCacheMu.Lock()
70+ defer mdCacheMu.Unlock()
71+ mdCache = map[int]*glamour.TermRenderer{}
72+ quietMDCache = map[int]*glamour.TermRenderer{}
73+}
internal/ui/model/ui.go+1
@@ -3116,6 +3116,7 @@ func (m *UI) refreshStyles() {
31163116 )
31173117 m.todoSpinner.Style = t.Pills.TodoSpinner
31183118 m.status.help.Styles = t.Help
3119+ common.InvalidateMarkdownRendererCache()
31193120 m.chat.InvalidateRenderCaches()
31203121}
31213122