#238

perf(dom): reuse one Range for partial row redraws

Openwnzzerwants to merge intofrom

Each partial row redraw in Renderer.updateRowParts creates a new Range. A Range stays attached to its document until it is garbage collected, and every DOM mutation updates each attached Range. While a terminal streams output, thousands of these Ranges can be attached at once in a page that collects infrequently, and every later DOM mutation gets slower.

This change keeps one Range per renderer and releases it in destroy().

Measurements

These are from embedding @wterm/dom 0.5.4 with the Ghostty core in a large Vue app. The test streamed about 200 short colored lines per second into a 47×56 terminal on Chromium, with CPU throttling at 4×.

per deleteContentsper insertNodeframes (p95)
a new Range per redraw~190 µs~50 µs33–66 ms, 33–57 fps
same, gc() forced every frame~13 µs~3 µs—
one reused Range (this PR)——16.8 ms, 60 fps

A bare page doesn't reproduce the slowdown because it collects often enough. To reproduce it on a bare page, keep extra Ranges alive. With 0, 2,000, 8,000 and 30,000 extra live Ranges, deleteContents in the same redraw loop took 7.9, 36, 110 and 365 µs per call.

Tests

The new renderer test redraws the changed prefix of four rows over five frames. It checks that document.createRange is called at most once, that the unchanged suffix spans are kept, and that the text is correct. It fails on main with 20 Ranges created.

@wterm/dom: 22 files and 482 tests pass. tsc --noEmit and Prettier are clean.

Activity

3
vercel[bot]16d ago

@wnzzer is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

copilot-pull-request-reviewer[bot]reviewed16d ago
copilot-pull-request-reviewer[bot]16d ago

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.