perf(zerolag): rebuild only the changed overlay line, one paint layer per line

The local echo overlay rebuilt every character of the prompt on each
keystroke, one absolutely positioned span per character, so a keystroke
cost O(prompt length) in DOM work, style, layout and paint. Every
positioned span was also its own paint layer, so the browser re-layerized
and repainted thousands of them per frame. In headless Chromium with a 6x
CPU slowdown, a ~4,100-character prompt ran at a median 813 ms per frame,
and ~300 characters already missed 60 fps (54 ms).

renderOverlay() now keeps a per-container record of the line elements it
drew and rebuilds only lines whose text, position or composition range
changed. It starts over when the font, colour, cell size or Unicode
version changes, or when something else emptied or edited the container.

makeLine() lays the spans out in a flex row instead of positioning each
one, and sets the shared font styles once on the line. Cell edges are
snapped to a quarter pixel, which Blink, WebKit and Gecko all store
exactly, so the widths add up to the grid without drift and every edge
stays within 1/8 px of col * cellW. Same prompt: 17 ms per frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-25 06:07:34 +01:00
1 parent 3e06f3c141
commit 1082724db9
9 files changed
+425 -122

No files matched your search

@@ -0,0 +1,6 @@
---
"aicodeman": patch
"xterm-zerolag-input": patch
---
**Typing stays smooth in a long prompt.** The local echo overlay (the instant preview of what you type before the session echoes it back) used to throw away and rebuild every character of the prompt on each keystroke, so the longer the prompt, the slower each key. On a phone-class CPU it dropped below 60 fps at about 300 characters, and at about 4,000 characters a single keystroke took most of a second to appear. It now rebuilds only the line that changed, and each line is one paint layer instead of one layer per character, so a keystroke costs the same however long the prompt is. What you see is unchanged: every character still sits on its terminal cell, within 1/8 px. This ships in `xterm-zerolag-input` as well: the README's rendering section describes the new layout.