Skip to content

Ink Rendering And Flicker in Qwen Code: An Introduction #1778

@tanzhenxin

Description

@tanzhenxin

Note

This issue serves as an introduction to the flickering problem space in Qwen Code's terminal UI. It explains the core concepts, design considerations, and environments where flicker may appear. The goal is to establish shared understanding before exploring potential improvements.

Part 1: The Few Concepts That Matter

Qwen Code's terminal UI is built on Ink. Ink does not render "screens" — it renders text into the terminal using multiple write operations per update. Flicker happens when those writes are large, frequent, or visible as intermediate states.

Static Output vs. Dynamic Output

Ink separates output into two streams.

Static output comes from <Static>. It is written above the interactive UI and then kept as terminal history. It is not re-rendered every frame unless new static items are appended.

Dynamic output is everything outside <Static>. It is re-rendered on each update cycle, even if some of its content never changes.

If you remember only one rule: dynamic output is what Ink continuously redraws. Static output is what Ink writes once and keeps.

Dynamic Output Height vs. Terminal Height

Ink computes the height of dynamic output as "how many terminal rows the interactive output occupies after layout and wrapping." This height does not include <Static> content.

That height is compared to the terminal's row count, and the comparison decides the rendering path:

  1. If dynamic output height is less than terminal height, Ink uses a normal partial update path.
  2. If dynamic output height is greater than or equal to terminal height, Ink falls back to a fullscreen path that clears the entire terminal and redraws everything.

This threshold is the biggest flicker trigger. The fullscreen path is correct but visually aggressive, especially on Windows terminals.

Block-Based vs. Line-Based Updates

In normal mode (non-incremental), Ink uses block-based updates. It clears the previous dynamic output block and then writes the new dynamic output block. The "block" is the exact region occupied by the previous dynamic output, not the entire screen.

In incremental mode, Ink switches to line-based updates. It compares the output line by line and only rewrites lines that changed. This may reduce visible redraw when most lines are stable, though results vary by terminal.

Synchronized Output

Ink automatically wraps updates with ANSI sequences that request synchronized output. If the terminal supports the feature, the update is displayed atomically, so intermediate steps may not be visible. If the terminal does not support it, the sequences are ignored and the update is shown step by step.

Support cannot be forced from Ink — the terminal decides whether it works.

Part 2: Design Considerations for Reducing Flicker

This section outlines approaches that may help reduce flicker. These are not mandates but starting points for exploration.

1) Keep Dynamic Output Below Terminal Height

If dynamic output height stays below the terminal height, Ink may avoid the fullscreen clear-and-redraw path. This could remove a major source of flicker.

Ways to explore this include limiting the height of live dashboards, moving long histories into <Static>, and avoiding layouts that always fill the entire terminal.

2) Use <Static> For Long, Append-Only History

If there are logs, completed steps, or output that never changes once printed, moving them into <Static> may help. That content becomes terminal history and no longer increases dynamic output height or redraw cost.

3) Segment Streaming Output And Move History To <Static>

In streaming output scenarios, splitting output into segments may be more effective than letting a single dynamic block grow past one screen. A pattern to consider:

  1. Split on newlines and commit completed lines to <Static>.
  2. Keep only the current, in-progress line(s) in dynamic output.
  3. Treat the dynamic area as a small "live window" rather than a full log.

This may keep dynamic output height bounded and avoid the fullscreen fallback. It works best for append-only streams. If previously printed lines need updates, this pattern may be less suitable.

4) Batch Streaming Updates

Streaming data often arrives in tiny chunks. Rendering each chunk immediately may cause frequent rerenders and visible jitter. Instead, buffering a short interval (for example 50–100ms or one logical line) and updating once per batch may help. This could pair with the split strategy: finalize complete lines into <Static> in batches, and keep only the live tail in dynamic output.

5) Avoid Unnecessary Rerenders (React-Level)

Unnecessary rerenders may be a hidden source of flicker. Keeping rapidly changing data localized, and avoiding updates that cause large parts of the tree to re-render when only a small sub-region needs to change, may help. This could be especially relevant in streaming output UIs.

6) Evaluate Incremental Rendering

If only a few lines update each frame, incremental rendering may reduce redraw volume. If nearly every line changes each frame, incremental rendering may offer less benefit — in that case, reducing update frequency or limiting how much the UI changes per update might be more useful.

Incremental rendering is a relatively recent addition in Ink. It may be worth validating in real environments before relying on it, as terminals vary significantly.

7) Design for Variable Terminal Support

Synchronized output helps only when the terminal supports it. Many terminals, especially on Windows, may ignore the sync sequences. Designing as if sync is unavailable and treating it as a bonus rather than a requirement may lead to more robust results.

Part 3: Environments Where Flicker May Appear

Even if the approaches above are followed, flicker may still appear in some environments. These scenarios may be worth understanding and testing.

1) Windows Terminal

Windows terminals may make clear-and-redraw operations more visible. Full-screen redraws or large block updates may be more likely to show flicker than on many Unix terminals.

2) Remote VM (Cloud VM With Poor Network)

Network latency and packet bursts can make otherwise clean updates arrive in clumps. Reducing output volume and frequency may help, but jitter and occasional flicker might still happen when the connection stalls or catches up.

3) TMUX Terminal

tmux adds another layer of screen management. It can change how updates are applied and can interfere with terminal-level behavior. Results may vary across tmux versions and terminal combinations.

4) Very Narrow Screen Height

A narrow terminal makes it easy for dynamic output to reach the fullscreen threshold. If dynamic output cannot be kept strictly below the terminal height, Ink may fall back to full-screen redraws and flicker may become difficult to avoid.

Next Steps

This issue is intended to establish shared understanding of the flicker problem space. Once there is alignment on the concepts and considerations above, specific areas for exploration can be identified and tracked separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No 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