The first version did the obvious thing: one line of log output, one appended DOM node. At a few hundred lines a second that is fine. At forty thousand it is a denial-of-service attack you wrote yourself — layout never finishes, the scrollbar becomes decorative, and the tab dies somewhere past two million nodes.
The fix is to stop pretending the DOM is the data structure. Lines land in a preallocated ring buffer that holds the last half million entries and nothing else; the DOM renders only the slice the viewport can show, about eighty rows, recycled as you scroll. Scrollback becomes an index into the ring, not a pile of nodes — memory is flat no matter how long the session runs.
Ingest and paint run on different clocks. Messages arrive whenever the socket feels like it, in bursts of hundreds; the renderer wakes once per animation frame, drains whatever is queued into the ring in one pass, and updates the visible slice once. Nothing re-renders per message. A frame that receives one line and a frame that receives nine hundred do the same amount of DOM work.
The detail that took longest was the tail — the terminal’s follow mode. Pinning to the bottom by scrolling after every append fights the user the moment they drag the scrollbar. Follow is a state, not a scroll position: on when you are at the live edge, off the instant you scroll up, back on when you return to the bottom. The ring keeps filling either way; the view just stops chasing it.
What survived the soak test was, in the end, a very old idea: fixed memory, fixed work per frame, and a strict boundary between what the machine records and what the human is looking at. The terminal does not try to show everything. It tries to never fall behind.
