[BUG] Fullscreen: full-viewport repaints degrade whole-window latency in the VS Code integrated terminal (root-caused; scrolling not required)

Status Open
Reported on v2.1.224
Maintainer reply None cached
Activity 3 comments · opened Aug 7, 2026
Update — root-caused, and two corrections to this report. The cause is xterm.js screenReaderMode (driven by VS Code's editor.accessibilitySupport) doing a forced synchronous reflow per terminal row per render, amplified by fullscreen's full-viewport repaint. Details in this comment. Two claims in the original report below are retracted and struck through: that scrolling is required to trigger it, and that sending a message reliably clears it. Corrections in this comment. Everything else stands.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

In fullscreen rendering ("tui": "fullscreen"), a sustained stream of repaints progressively degrades the entire UI. Scrolling up through a long conversation with the mouse wheel is the fastest way to trigger it, but it is not required — see below. After enough repaint volume, a single wheel notch takes 1–3 seconds to register. The degradation then persists after all input stops — it is not a transient scroll-feel problem.

While degraded:

  • Text selection inside Claude Code lags by the same 1–3 seconds.
  • Hovering VS Code's own terminal buttons (split, close) lags identically, so the host editor's UI is affected too.
  • ~~Sending a message instantly restores full responsiveness, every time. This is the most reliable signal in the whole repro.~~ Retracted — this is not reliable. On repeated observation it sometimes did nothing.

Scrolling is not required. It also degrades with no mouse input at all, during ordinary streaming output where the view auto-scrolls on its own. The animated spinner glyph makes it obvious — its frame rate visibly drops and stutters while nothing is being scrolled, selected, or clicked. The common factor is repaint volume, not scroll input.

Continuous scrolling in one direction is enough to trigger it quickly. Scrolling up and down is not required — I originally reported it that way only because it was a convenient way to stay inside the scrollback.

This is distinct from #56546 and #80033, which describe jank during scrolling. Here the lag outlives the input and degrades unrelated interactions.

Steps to Reproduce

  1. "tui": "fullscreen" in settings.
  2. Open Claude Code in the VS Code integrated terminal.
  3. Build up a conversation long enough to scroll through (a few screens is enough; a longer one degrades faster).
  4. Scroll up continuously with the mouse wheel.
  5. After enough scrolling, stop. Latency per notch is now 1–3 seconds and stays there.
  6. Try selecting text — same lag. Hover a VS Code terminal button — same lag.
  7. ~~Send any message. Responsiveness returns immediately.~~ Retracted, see above.

Alternative trigger with no mouse input: leave a long-running request streaming output in a tall terminal and watch the spinner glyph stutter.

Environment

  • Claude Code: 2.1.224 (native installer)
  • VS Code: 1.132.0 (also reproduced on 1.127.0)
  • OS: Windows 11 (10.0.26200)
  • Shell: PowerShell 7
  • GPU: NVIDIA RTX 4080 SUPER, driver 32.0.16.1088, 3840×2160 @ 143 Hz, DPR 2

What I ruled out

Each of these was tested individually; the bug reproduces unchanged in every case.

| Eliminated | How |
|---|---|
| Mouse-wheel acceleration | "wheelScrollAccelerationEnabled": false, restarted |
| ~~VS Code GPU acceleration~~ | ~~"terminal.integrated.gpuAcceleration": "off", new terminal (DOM renderer confirmed active by appearance change)~~ — this test was not conclusive. The real cost is DOM layout, which neither renderer avoids. See the root-cause comment |
| VS Code version | Reproduces on both 1.127.0 and 1.132.0 |
| Third-party VS Code customization | All injected CSS/JS extensions disabled and confirmed cleared |
| CPU / memory exhaustion in the CLI | Sampled claude.exe at 5 Hz during the degraded state: ~0% CPU with brief spikes, working set flat, no thread or handle growth |
| The xterm.js-specific scroll drain | See below |

The scroll drain specifically. useAdaptiveDrain is set from xterm.js detection, so VS Code takes d9y (a 2–3 line/frame tail) while other terminals take p9y (~75%/frame). I patched the single byte useAdaptiveDrain:i → useAdaptiveDrain:0 in the 2.1.224 binary to force the proportional drain, changing nothing else, and the bug reproduced identically. The drain is not the cause — noting it so nobody else spends time there.

Not reproducible outside the VS Code terminal

Same machine, same conversation, same settings, Windows Terminal: scrolling stays smooth indefinitely. No degradation at any point.

Control test: VS Code's terminal handles the same workload fine

To check whether this is simply a VS Code terminal limit under alt-screen scrolling, I ran less --mouse in the same VS Code terminal over two generated files, 20,000 lines each:

  • plain ASCII
  • dense styling: five 24-bit foreground/background spans plus box-drawing glyphs per line

Jumped to the end, then scrolled up hard with the wheel, the same motion that reproduces the bug.

Both were completely smooth, with no degradation. Same alt screen, same mouse reporting, same wheel-driven full-viewport repaints, heavier per-line styling than Claude Code emits — and VS Code's terminal handled it without trouble.

So the alt-screen scroll workload is not the problem. Something specific to Claude Code's fullscreen renderer is. (Now identified: less dirties a row or two per notch via scroll regions, fullscreen dirties the entire viewport. See the root-cause comment.)

One more observation, offered with a caveat

A Chrome DevTools performance trace of the VS Code renderer during the degraded state shows the main thread spending 17.2 s of 20.1 s of task time inside Commit, with individual commits over 1000 ms, while claude.exe is at 0% CPU and total JS across xterm.js, addon-webgl and the workbench is under 400 ms. Inside the longest commit, the GPU process is in a single 1077 ms task while using almost no CPU.

Caveat: the bug still reproduces with terminal.integrated.gpuAcceleration: "off", which should take the WebGL path out entirely, so I can't claim this is causal. Including it in case it's useful. (Resolved — forced synchronous layout bills to Commit, not to JS self-time, and is renderer-independent.)

Additional Notes

The repro is reliable and takes under a minute. Happy to run instrumentation, capture traces, or test a patched build against it.

View original on GitHub ↗

3 Comments

KamilDev · 2 months ago

Root cause found — editor.accessibilitySupport: "on" on my side, amplified by fullscreen's full-viewport repaint

Setting "editor.accessibilitySupport": "off" in VS Code eliminates the lag completely on my machine. I had it set to "on". Posting the full chain because the amplification is Claude Code's side, and because it invalidates one row of my "What I ruled out" table.

The chain

VS Code pipes that setting directly into xterm.js (resources/app/out/vs/workbench/workbench.desktop.main.js, 1.132.0):

updateAccessibilitySupport() {
  this.xterm.raw.options.screenReaderMode = this._accessibilityService.isScreenReaderOptimized()
}

screenReaderMode: true makes xterm construct AccessibilityManager, which mirrors the viewport into a parallel DOM tree — one <div> per terminal row. Its per-render refresh loop (xterm core, shipped in node_modules.asar) does this for every row, on every render:

a.textContent = o;
a.setAttribute("aria-posinset", n);
a.setAttribute("aria-setsize", s);
this._alignRowWidth(a);

and _alignRowWidth is the problem:

_alignRowWidth(e) {
  e.style.transform = "";                          // invalidates layout
  const t = e.getBoundingClientRect().width;       // forces synchronous layout
  const i = this._rowColumns.get(e)?.slice(-1)?.[0];
  if (!i) return;
  const s = i * this._renderService.dimensions.css.cell.width;
  e.style.transform = `scaleX(${s / t})`;          // invalidates again
}

Write → forced read → write, per row, per render. Classic layout thrash: one forced synchronous reflow per terminal row per frame.

There is a second cost on the parser side. In InputHandler.print(), the per-character hot loop:

if (screenReaderMode) { this._onA11yChar.fire(stringFromCodePoint(code)); }

An emitter dispatch plus a string allocation for every character printed, feeding a debounced ARIA live region.

Why this matches every symptom I reported

  • Trace shows 17.2s of 20.1s in Commit with under 400ms of JS across xterm/webgl/workbench. Forced reflow is browser layout, not JS self-time. It lands almost entirely outside the JS attribution, which is why the numbers looked contradictory.
  • gpuAcceleration: "off" changed nothing. In 1.132 _shouldLoadWebgl() reads only terminal.integrated.gpuAcceleration; screen reader mode is not gated on the renderer, and this cost is DOM layout regardless of which renderer draws. My table row claiming this eliminated a rendering-side cause was wrong — it never touched this path.
  • VS Code's own UI (terminal split/close buttons) lags identically. Same renderer main thread, blocked by the same forced layouts.
  • less --mouse over 20,000 dense-styled lines stayed smooth. This is the part that matters for Claude Code. A wheel notch in less emits a scroll-region index and one new row — a handful of dirty rows. Claude Code's fullscreen renderer repaints the entire viewport per notch, so it hits rows × forced reflow where less hits roughly 1 × forced reflow. My control test didn't disprove a terminal-side cause; it isolated the input that triggers it. At 3840×2160 with DPR 2 the row count is large, which is why it's this severe here.

What I think is still actionable for Claude Code

The VS Code setting is mine to fix and I've fixed it. But the reason this configuration is survivable in less and unusable in Claude Code is the full-viewport repaint per scroll notch. Emitting scroll-region / delta updates on wheel scroll instead of redrawing the whole alt screen would cut the per-frame row work by roughly the viewport height, and would also reduce cost on the normal path for anyone whose terminal has any per-row render overhead.

Happy for this to be retitled or closed as external if you'd rather not track the repaint strategy here. Flagging it because "screen reader mode makes Claude Code fullscreen unusable in the VS Code terminal" is a real trap — nothing surfaces the connection, and the obvious suspects (GPU acceleration, scroll drain, renderer choice) all lead away from it.

Residual I can't yet explain

The mechanism above accounts for the per-frame cost, but not cleanly for two observations: that the degradation outlives all input, and that sending a message restores responsiveness instantly. Queued wheel events draining at ~1s per frame would look like the first; I have no grounded explanation for the second. Not asserting a cause for either.

KamilDev · 2 months ago

Corrections: scrolling is not required, and the "sending a message fixes it" claim is retracted

Two amendments to the original report. I've struck both claims in the issue body rather than silently editing them.

1. Retracting "sending a message instantly restores full responsiveness"

I originally called this "the most reliable signal in the whole repro." It isn't. On repeated observation it sometimes did nothing, and I can't produce it on demand. Anyone using it as a diagnostic lever should stop — I put it in the report with more confidence than the observation supported.

This also removes one of the two loose ends I flagged in the root-cause comment above. It was the observation that fit the mechanism worst, and it turns out not to be a real effect.

2. Scrolling is not required — repaint volume is the trigger

More useful correction. The degradation also happens with no mouse input at all, during ordinary streaming output where the view auto-scrolls by itself. The animated spinner glyph makes it easy to see: its frame rate visibly drops and stutters while nothing is being scrolled, selected, or clicked.

This matters for the diagnosis, because it fits the mechanism better than the original scroll-based framing did:

  • The cost is rows × forced synchronous reflow, paid per render. Nothing about it is specific to wheel events.
  • Wheel scrolling was never the cause — it was just the cheapest way to make fullscreen emit a full-viewport repaint many times per second.
  • Streaming output plus an animated spinner produces the same repaint volume without any input, so it degrades the same way.
  • It also explains "persists after all input stops" without needing a separate mechanism: in fullscreen the terminal is rarely idle, because the spinner keeps rendering.

So the trigger condition is repaint volume, not scroll input, and the less --mouse control test contrasts even more sharply than I first described: less dirties a row or two per notch via scroll regions and never sustains a repaint stream on its own, while fullscreen repaints the entire viewport continuously whenever anything is animating.

Effect on the ask

Unchanged, and slightly stronger. Emitting scroll-region and delta updates instead of full-viewport repaints would cut per-frame row work by roughly the viewport height — and the passive-streaming case shows that benefit applies during normal operation, not only while someone is scrolling.

I've retitled the issue to match, since the old title led with scrolling and ended with the retracted claim. It was previously:

[BUG] Fullscreen: scrolling up through a long conversation progressively degrades all input latency until the next message is sent (VS Code integrated terminal)
0x48Maciek · 13 days ago

I think I found what makes this persist after input stops and clear on blur/keypress.

In screen reader mode, xterm.js's AccessibilityManager appends every printed character to an aria-live region and only stops after 20 line feeds. Fullscreen redraws by positioning the cursor, so it never hits that limit: the live region (a 1px-wide element) grows by every frame's text until the terminal loses focus or a key is pressed. Each append re-lays-out the whole thing, which fits the time you saw in Commit rather than JS.

Replaying recorded fullscreen scroll output into the xterm.js build VS Code 1.139 ships, the live region grew 145K → 611K → 977K chars over three 9 s loops, with main-thread stalls of 0.1 s → 3.1 s → 9.9 s. A blur cleared it. The per-row _alignRowWidth reflow was small by comparison (~50 ms per loop).

Fix proposed in xtermjs/xterm.js#6199. Until it ships, "editor.accessibilitySupport": "off" is the workaround, as you found.