on main help sign in
perf: reduce GC pressure in rendering pipeline (#687)
bdb0a4ctazjin
internal/tui/exp/list/list.go+46−9
@@ -790,15 +790,28 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
790 return tea.Batch(cmds...)
790 return tea.Batch(cmds...)
791}
791}
792
792
793+// renderFragment holds updated rendered view fragments
794+type renderFragment struct {
795+ view string
796+ gap int
797+}
798+
793−// render iterator renders items starting from the specific index and limits hight if limitHeight != -1
799+// renderIterator renders items starting from the specific index and limits height if limitHeight != -1
794// returns the last index and the rendered content so far
800// returns the last index and the rendered content so far
795// we pass the rendered content around and don't use l.rendered to prevent jumping of the content
801// we pass the rendered content around and don't use l.rendered to prevent jumping of the content
796func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string) (string, int) {
802func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string) (string, int) {
803+ var fragments []renderFragment
804+
797 currentContentHeight := lipgloss.Height(rendered) - 1
805 currentContentHeight := lipgloss.Height(rendered) - 1
798 itemsLen := l.items.Len()
806 itemsLen := l.items.Len()
807+ finalIndex := itemsLen
808+
809+ // first pass: accumulate all fragments to render until the height limit is
810+ // reached
799 for i := startInx; i < itemsLen; i++ {
811 for i := startInx; i < itemsLen; i++ {
800− if currentContentHeight >= l.height && limitHeight {
812+ if limitHeight && currentContentHeight >= l.height {
801− return rendered, i
813+ finalIndex = i
814+ break
802 }
815 }
803 // cool way to go through the list in both directions
816 // cool way to go through the list in both directions
804 inx := i
817 inx := i
@@ -811,6 +824,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string
811 if !ok {
824 if !ok {
812 continue
825 continue
813 }
826 }
827+
814 var rItem renderedItem
828 var rItem renderedItem
815 if cache, ok := l.renderedItems.Get(item.ID()); ok {
829 if cache, ok := l.renderedItems.Get(item.ID()); ok {
816 rItem = cache
830 rItem = cache
@@ -820,19 +834,42 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string
820 rItem.end = currentContentHeight + rItem.height - 1
834 rItem.end = currentContentHeight + rItem.height - 1
821 l.renderedItems.Set(item.ID(), rItem)
835 l.renderedItems.Set(item.ID(), rItem)
822 }
836 }
837+
823 gap := l.gap + 1
838 gap := l.gap + 1
824 if inx == itemsLen-1 {
839 if inx == itemsLen-1 {
825 gap = 0
840 gap = 0
826 }
841 }
827
842
828− if l.direction == DirectionForward {
843+ fragments = append(fragments, renderFragment{view: rItem.view, gap: gap})
829− rendered += rItem.view + strings.Repeat("\n", gap)
844+
830− } else {
831− rendered = rItem.view + strings.Repeat("\n", gap) + rendered
832− }
833 currentContentHeight = rItem.end + 1 + l.gap
845 currentContentHeight = rItem.end + 1 + l.gap
834 }
846 }
835− return rendered, itemsLen
847+
848+ // second pass: build rendered string efficiently
849+ var b strings.Builder
850+ if l.direction == DirectionForward {
851+ b.WriteString(rendered)
852+ for _, f := range fragments {
853+ b.WriteString(f.view)
854+ for range f.gap {
855+ b.WriteByte('\n')
856+ }
857+ }
858+
859+ return b.String(), finalIndex
860+ }
861+
862+ // iterate backwards as fragments are in reversed order
863+ for i := len(fragments) - 1; i >= 0; i-- {
864+ f := fragments[i]
865+ b.WriteString(f.view)
866+ for range f.gap {
867+ b.WriteByte('\n')
868+ }
869+ }
870+ b.WriteString(rendered)
871+
872+ return b.String(), finalIndex
836}
873}
837
874
838func (l *list[T]) renderItem(item Item) renderedItem {
875func (l *list[T]) renderItem(item Item) renderedItem {