Skip to main content

Overview

Bubble Tea provides several performance optimization features to ensure smooth rendering and efficient resource usage in terminal applications.

Frame Rate Control

WithFPS Option

Control the maximum frames per second (FPS) for your application using the WithFPS option:
package main

import (
    tea "github.com/charmbracelet/bubbletea"
)

func main() {
    p := tea.NewProgram(
        initialModel(),
        tea.WithFPS(30), // Set to 30 FPS
    )
    p.Run()
}

FPS Constraints

Bubble Tea enforces FPS limits to balance performance and responsiveness.
  • Default FPS: 60 frames per second
  • Minimum FPS: Values less than 1 default to 60 FPS
  • Maximum FPS: 120 frames per second (hard cap)
From renderer.go:13-14:
const (
    defaultFPS = 60
    maxFPS     = 120
)

How FPS Works

The FPS setting controls the rendering ticker interval:
// From tea.go:1372-1379
func (p *Program) startRenderer() {
    framerate := time.Second / time.Duration(p.fps)
    if p.ticker == nil {
        p.ticker = time.NewTicker(framerate)
    } else {
        p.ticker.Reset(framerate)
    }
    // ...
}
The renderer flushes updates at each ticker interval, ensuring consistent frame timing.

Choosing the Right FPS

30 FPS

Good for simple applications, status displays, and reduced CPU usage

60 FPS

Default - smooth for most interactive applications

120 FPS

Maximum smoothness for animations and high-frequency updates

Performance Considerations

package main

import (
    "time"
    tea "github.com/charmbracelet/bubbletea"
)

type model struct {
    animation int
    fps       int
}

func (m model) Init() tea.Cmd {
    return tick()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tickMsg:
        m.animation++
        return m, tick()
    case tea.KeyMsg:
        if msg.String() == "q" {
            return m, tea.Quit
        }
    }
    return m, nil
}

type tickMsg time.Time

func tick() tea.Cmd {
    return tea.Tick(time.Millisecond*50, func(t time.Time) tea.Msg {
        return tickMsg(t)
    })
}

func (m model) View() tea.View {
    return tea.NewView(fmt.Sprintf("Frame: %d", m.animation))
}

func main() {
    // For smooth animations, use higher FPS
    p := tea.NewProgram(
        model{fps: 60},
        tea.WithFPS(60),
    )
    p.Run()
}
Higher FPS increases CPU usage. Choose the lowest FPS that provides acceptable smoothness for your use case.

Cell-Based Rendering

Bubble Tea uses cell-based rendering for efficient terminal updates:

Screen Buffer

The cursedRenderer uses a screen buffer to track terminal cells:
type cursedRenderer struct {
    w             io.Writer
    buf           bytes.Buffer         // Updates buffer
    scr           *uv.TerminalRenderer
    cellbuf       uv.ScreenBuffer      // Cell-based screen buffer
    lastView      *View
    width, height int
    // ...
}

How It Works

  1. View generation: Your View() method returns content
  2. Cell parsing: Content is parsed into cells (characters + attributes)
  3. Diff calculation: New cells are compared with previous frame
  4. Minimal updates: Only changed cells are sent to the terminal
  5. Cursor optimization: Cursor movements use optimal escape sequences
Cell-based rendering means you only pay for what changes between frames, not the entire screen.

Rendering Optimizations

Synchronized Updates

Bubble Tea supports ANSI synchronized output mode (mode 2026) for tear-free rendering:
// From tea.go:1089-1095
if !p.disableRenderer && shouldQuerySynchronizedOutput(p.environ) {
    // Query for synchronized updates support
    p.execute(ansi.RequestModeSynchronizedOutput +
        ansi.RequestModeUnicodeCore)
}
When supported by the terminal, this prevents partial frames from being visible.

Cursor Movement Optimizations

The renderer can use optimized cursor movements:
type cursedRenderer struct {
    hardTabs  bool // Use hard tabs for horizontal movement
    backspace bool // Use backspace for backward movement
    // ...
}
These optimizations reduce the number of bytes sent for cursor positioning.

Performance Best Practices

1. Minimize View Complexity

func (m model) View() tea.View {
    // Pre-compute expensive operations
    content := m.cachedContent
    return tea.NewView(content)
}

2. Cache Rendered Content

type model struct {
    data         []string
    cachedView   string
    dataDirty    bool
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case dataUpdateMsg:
        m.data = msg.newData
        m.dataDirty = true // Mark cache as stale
    }
    return m, nil
}

func (m model) View() tea.View {
    if m.dataDirty {
        // Rebuild view only when data changes
        m.cachedView = renderData(m.data)
        m.dataDirty = false
    }
    return tea.NewView(m.cachedView)
}

3. Reduce Update Frequency

type model struct {
    lastUpdate time.Time
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case frequentMsg:
        // Throttle high-frequency updates
        if time.Since(m.lastUpdate) < 100*time.Millisecond {
            return m, nil // Skip this update
        }
        m.lastUpdate = time.Now()
        // Process update...
    }
    return m, nil
}

4. Use Appropriate FPS

// For mostly static displays
tea.NewProgram(model{}, tea.WithFPS(30))

Monitoring Performance

Enable Tracing

Bubble Tea supports tracing via the TEA_TRACE environment variable:
TEA_TRACE=/tmp/bubbletea.log ./myapp
This logs internal operations including render timing.

Measure View Performance

import (
    "time"
    "log"
)

func (m model) View() tea.View {
    start := time.Now()
    defer func() {
        duration := time.Since(start)
        if duration > 16*time.Millisecond { // 60 FPS threshold
            log.Printf("Slow view render: %v", duration)
        }
    }()
    
    content := buildView(m)
    return tea.NewView(content)
}

Performance Comparison

FPS vs CPU Usage

FPSFrame TimeCPU ImpactUse Case
30~33msLowStatus displays, logs
60~16msMediumInteractive apps (default)
120~8msHighSmooth animations

Terminal Update Costs

Sending the entire screen content every frame. Expensive and unnecessary with cell-based rendering.
Only updating changed cells. Used by Bubble Tea - efficient for most use cases.
Atomic frame updates prevent tearing. Slight overhead but better visual quality.

Build docs developers (and LLMs) love