Skip to content

Excessive scroll events causing UI jitter in terminal multiplexers (4,000-6,700 scrolls/second) #9935

@rinadelph

Description

@rinadelph

Summary

Claude Code's streaming output causes 4,000-6,700 scroll events per second when running inside terminal multiplexers (tmux/smux), resulting in severe UI jitter and flickering. This issue was identified through comprehensive instrumentation of a tmux fork (smux) with microsecond-precision logging.

Problem Description

When Claude Code streams LLM responses, it causes excessive terminal scrolling that overwhelms the rendering capability of terminal multiplexers, creating a poor user experience with:

  • Visual jitter/flickering during streaming output
  • Scrollbar jumping erratically
  • Screen tearing effects
  • Degraded performance for 100+ second durations

Root Cause Analysis

Measured Metrics (106-second test session)

  • Total scroll events: 423,575 (logged) / 715,901 (counter)
  • Scroll rate: 4,002-6,764 scrolls/second
  • PTY read callbacks: 8,044 (avg 4,095 bytes each)
  • Linefeeds parsed: 400,930 (3,782/second)
  • Burst pattern: 94.7% of scrolls occur in sub-millisecond bursts

Timing Analysis

Scroll burst example:
- Scrolls #1-36: All within 144 microseconds
- Scroll #37: 1.26 second gap
- Scrolls #38-45: Burst of 8 in 1.7ms
- Scroll #46: 2.27 second gap
- Scrolls #47-50: Burst of 4 in microseconds

Sub-millisecond scroll timing (first 1,000 scrolls):

  • Same microsecond: 4.6%
  • 0-1ms apart: 94.7%
  • 1-10ms apart: 0.1%
  • 10ms apart: 0.6%

Data Pattern

Claude Code sends output in 4,095-byte chunks with a repeating pattern of newlines per chunk:

Pattern: 124, 36, 47, 58, 53, 46, 42, 23 newlines (then repeats)

This suggests full-screen redraws happening repeatedly, likely for:

  • Main content area
  • Status sections
  • Progress indicators
  • Syntax-highlighted code blocks

ANSI Overhead

Each line includes heavy ANSI formatting:

  • Background color: \e[48;2;55;55;55m (~20 bytes)
  • Foreground color: \e[38;2;255;255;255m (~22 bytes)
  • Content
  • Reset codes: \e[39m\e[49m (~8 bytes)

Estimated overhead: ~189 KB/second of ANSI codes alone (50 bytes/line × 3,782 lines/sec)

Technical Details

Three-Layer Instrumentation Results

We instrumented smux at three critical layers to trace data flow:

  1. PTY Raw Input (/tmp/smux_pty_raw.log - 14 MB)

    • Captured raw bytes from Claude Code's PTY
    • Hex dump + ASCII representation
    • Newline counts per chunk
  2. Linefeed Parser (/tmp/smux_linefeed.log - 11 MB)

    • Every LF/VT/FF character processed
    • 400,930 total linefeeds
  3. Scroll Trigger Detection (/tmp/smux_scroll_trigger.log - 25 MB)

    • Actual scroll events when cursor at bottom
    • 100% of scrolls had cy=24 (cursor always at screen bottom)
    • Every linefeed triggered a scroll

Why This Happens

Claude Code appears to use a full-screen redraw strategy for streaming:

// Suspected rendering loop
for await (const chunk of llmResponseStream) {
    responseBuffer += chunk;
    const renderedOutput = renderFullView(responseBuffer);  // ← Full redraw!
    process.stdout.write(renderedOutput);
}

Instead of:

// Optimal approach
for await (const chunk of llmResponseStream) {
    responseBuffer += chunk;
    const incrementalUpdate = renderOnlyNewContent(chunk);  // ← Incremental!
    process.stdout.write(incrementalUpdate);
}

Comparison to Normal Terminal Usage

vim editing:         10-50 scrolls/second
tail -f logfile:     1-100 scrolls/second
cat large file:      100-500 scrolls/second
npm install:         100-300 scrolls/second (bursts)
Claude Code:         4,000-6,700 scrolls/second (SUSTAINED)

Claude Code is 40-600x higher than typical terminal usage!

Proposed Solutions

Option 1: Incremental Updates (Recommended)

Switch from full-screen redraws to incremental updates:

  1. Only output new content to terminal
  2. Append to bottom instead of redrawing entire screen
  3. Batch UI updates at reasonable intervals (e.g., every 16ms)
  4. Optimize ANSI usage - don't reset colors on every line

Expected impact: Reduce scroll rate by 90%+ (from 4,000/sec to <400/sec)

Option 2: Use Alternative Screen Buffer

For TUI components (status bars, progress indicators):

\e[?1049h  # Enter alternative screen buffer
# Render TUI components here
\e[?1049l  # Exit back to main buffer

This would:

  • Isolate UI chrome from scrolling content
  • Prevent status updates from triggering scrolls
  • Reduce total scroll events significantly

Option 3: Batch Terminal Writes

Collect output chunks and flush at controlled intervals:

let outputBuffer = '';
let lastFlush = Date.now();

const FLUSH_INTERVAL_MS = 16;  // ~60 FPS

function writeOutput(chunk) {
    outputBuffer += chunk;
    
    if (Date.now() - lastFlush >= FLUSH_INTERVAL_MS) {
        process.stdout.write(outputBuffer);
        outputBuffer = '';
        lastFlush = Date.now();
    }
}

Temporary Workaround (User Side)

We're implementing scroll batching in smux to mitigate this issue, but this is a band-aid solution. The proper fix should be in Claude Code's rendering strategy.

Environment

  • OS: Arch Linux (kernel 6.17.2)
  • Terminal multiplexer: smux (tmux fork) with custom instrumentation
  • Terminal emulator: Various (issue occurs in all)
  • Claude Code version: Latest (2024-10-19)

Complete Analysis Document

Full technical analysis with all data and graphs available in the instrumented smux repository:

  • ROOT-CAUSE-ANALYSIS.md (complete breakdown)
  • DETECTION-READY.md (instrumentation setup)
  • Log files: 68 MB of captured data

Request

Please investigate Claude Code's terminal output strategy and consider implementing incremental updates or batched output to reduce scroll rate to reasonable levels (<100 scrolls/second).

This would significantly improve the experience for users running Claude Code in terminal multiplexers, which is a common development workflow.

Additional Context

  • Issue was detected using microsecond-precision instrumentation
  • Reproducible 100% of the time during streaming output
  • Affects all terminal multiplexers (tmux, screen, smux)
  • Native terminal emulators may also struggle with this rate
  • No data loss - all content reaches screen, just too fast

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions