Skip to content

perf(text): size the line numbers in one layout, not one per line - #696

Merged
andiwand merged 1 commit into
mainfrom
perf/text-line-number-layout
Aug 18, 2026
Merged

perf(text): size the line numbers in one layout, not one per line#696
andiwand merged 1 commit into
mainfrom
perf/text-line-number-layout

Conversation

@andiwand

@andiwand andiwand commented Aug 17, 2026

Copy link
Copy Markdown
Member

A megabyte of plain text took 38 seconds to appear in a WebView, of which 37.6 seconds was spent inside one function.

The bug

text.js's updateLineNumberHeight interleaves a write and a read:

for (var i = 0; i < textCells.length && i < nrCells.length; ++i) {
  nrCells[i].style.height =
    textCells[i].getBoundingClientRect().height + "px";
}

Setting style.height invalidates layout; the getBoundingClientRect() on the next iteration forces it again. With one gutter cell per line that is one full layout of the entire document per line.

Every height is read first, then written. Same output, one layout.

Numbers

Measured in a WebView on a Pixel 9 Pro emulator, timing wall clock to a stable DOM and accumulating the time spent inside this function. 1 MB of plain text, 7,823 lines:

ready in updateLineNumberHeight
as served 38.2s 37.6s
batched 0.26s 6ms
gutter script removed entirely 0.25s

Batching is indistinguishable from not doing the work at all. Repeated back to back to rule out warm-up: 29.1s / 0.19s / 30.2s.

Cost is linear in line count from here, and what remains is the browser, not this function:

text html elements fetch parse + layout ready
1 MB 1.2 MB 15,661 33ms 264ms 0.46s
10 MB 12 MB 155,229 71ms 1.5s 3.0s
25 MB 30 MB 386,343 192ms 6.7s 6.7s

Not in this PR

Two elements per line, half of them the gutter, costs roughly 4.6–5.4 KB of renderer memory each. At 50 MB and 100 MB the renderer is killed by the low-memory killer (2.7 GB RSS) and takes the app process with it — the engine itself is fine there, writing 121 MB of html in 2.5s. Replacing the parallel gutter column with a CSS counter would halve both the element count and the memory and remove this script; virtualising the line list is what would actually lift the ceiling. Neither is here.

Test

No test: this is a rendering performance fix with identical output, and the reference-output suite compares rendered pages. Verified by the measurements above.

🤖 Generated with Claude Code

`updateLineNumberHeight` set a gutter cell's height and then read the next
line's `getBoundingClientRect()`, so every read forced the layout the write
before it had invalidated - one full layout of the whole document per line.

Read every height first, then write them. Measured in a WebView on a Pixel 9
Pro emulator, a 1 MB text file (7.8k lines):

| | ready | in this function |
|---|---|---|
| before | 38.2s | 37.6s |
| after | 0.26s | 6ms |

The cost is linear in the number of lines from here: 10 MB takes 3.0s and
25 MB 6.7s, both of which are the browser parsing and laying out two elements
per line rather than anything this function does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouAQNfktsp9THcceennEX
@andiwand
andiwand force-pushed the perf/text-line-number-layout branch from 84ba844 to 5ff01ce Compare August 18, 2026 05:49
@andiwand
andiwand merged commit 514d51b into main Aug 18, 2026
25 checks passed
@andiwand
andiwand deleted the perf/text-line-number-layout branch August 18, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant