Summary
When the terminal is resized or a pane split is created or closed, fern currently re-renders every open tab through the full glamour pipeline synchronously before drawing the next frame. Each tab re-render involves creating a new glamour.TermRenderer, running the full markdown-to-ANSI render of the document, post-processing links and tags, and recomputing fold regions. With 10+ tabs open this produces a noticeable stutter on any resize event.
A second compounding issue exists in split-pane layouts: the glamour renderer is cached in a single slot keyed by width. When two panes have different widths (e.g. 60 and 120 columns), every call to renderTab thrashes the cache -- pane A creates a renderer at width 60, pane B creates one at width 120, pane A creates again at 60. Each tab render in a split layout triggers an unnecessary renderer rebuild.
The fix has two parts:
- Replace the single-slot renderer cache with a small width-keyed map so split panes share renderers without thrashing.
- Introduce lazy re-rendering so only the active tab is glamour-rendered immediately on resize while background tabs are deferred until first focused.
Conditions of Satisfaction
- Replace the single
renderer / renderWidthfields onviewer.Modelwith arenderers map[int]*glamour.TermRenderercache keyed by render width- All calls to
getRenderer(width)look up the map first and only construct a newglamour.TermRendereron a cache miss - The map is capped at roughly 5 entries; when the cap is exceeded the oldest or least-recently-used(LRU) entry is evicted to prevent unbounded memory growth
- On theme change the entire
renderersmap is cleared, matching the current behavior of nulling the single renderer field
- All calls to
- Add
renderWidth intandrenderedContent stringfields totabSharedto track the state of the cached glamour outputrenderWidthrecords the pane width that was used for the currentrendered/linescacherenderedContentrecords the content string that was glamour-rendered, so a content change at a stable width still triggers a re-renderrenderTabsets both fields at the end of each successful render pass
- Change the resize handler to re-render only the active tab immediately and defer all background tabs
- The call site that iterates all tabs and calls
renderTabon each one should be narrowed to just the active tab - Background tabs have their
renderWidthleft stale (or explicitly set to -1) so the lazy guard triggers when they are next focused
- The call site that iterates all tabs and calls
- Add a lazy re-render guard at the top of the view-mode render path (
renderViewContentorrenderSinglePane)- Before consuming
t.lines, check whethert.renderWidth != expectedRenderWidth || t.renderedContent != t.content - If either condition is true, call
renderTabinline before proceeding, ensuring the tab is always rendered with current dimensions and content when it becomes visible
- Before consuming
- Verify that content-change re-renders (external file change, save-and-reload) still fire correctly under the new invalidation logic
renderedContentguard must not suppress re-renders triggered by content edits, only those where neither width nor content has changed
External Resources
- Goldmark is the markdown parser underlying glamour. Relevant to understanding rendering costs!