Reported as: moho sluggish and holding 878MB after a day open.
The problem
MessageList builds a row for every message in the loaded list. There is no
windowing anywhere in the renderer - one IntersectionObserver in the whole
tree, and it is the history-gap filler. A buffer that has had history loaded
holds up to two thousand messages, and each one is a mounted MessageRow:
avatar, embeds, pictures, reactions, code blocks, the lot. Every one of them
is live whether it is on screen or a thousand rows above it.
Measured on the running client (12h uptime): renderer 690MB, GPU process
193MB. Opening #archlinux and scrolling back three pages left 8,338 DOM
nodes mounted, and coming back to the present released none of them -
still 8,336.
The solution
Draw the log as a tail of what is loaded - 120 rows - and let reading upwards
lengthen it. The scroll-to-top gesture already asks the daemon for an older
page; it now does the two in order, first drawing what this window is holding
and has not built, then fetching what it does not hold at all. Coming back to
the present releases it, either by the jump button or by scrolling there, and
switching conversations resets it.
A tail rather than a floating window: a chat log is read from the bottom, so
everything above the drawn part is older and there is nothing below it. That
means no spacer element, no estimated row heights and no second opinion about
how tall the page is - the scroll-anchoring machinery measures exactly what it
did before, which matters given how much of it is there to hold somebody's
place.
Two details that go with it: the first drawn row always starts its own run
(the message it would group with is not on the page, so hiding its author
would leave it unattributed), and the top-of-list trigger now fires only for a
reader who has actually scrolled up - a freshly built list sits at the top for
the moment before it is scrolled to the bottom, and reading that as "show me
more" both lengthened the page and asked the daemon for a page nobody wanted,
every single time a conversation was opened.
Avatars in the log and in the member list are loading="lazy" now as well.
Result
before
after
DOM nodes after reading back three pages
8,338
3,794
DOM nodes once back at the present
8,336
1,374
Fixed in 46e3d7f "Draw the part of the log somebody is reading".
Reported as: moho sluggish and holding 878MB after a day open.
## The problem
`MessageList` builds a row for every message in the loaded list. There is no
windowing anywhere in the renderer - one `IntersectionObserver` in the whole
tree, and it is the history-gap filler. A buffer that has had history loaded
holds up to two thousand messages, and each one is a mounted `MessageRow`:
avatar, embeds, pictures, reactions, code blocks, the lot. Every one of them
is live whether it is on screen or a thousand rows above it.
Measured on the running client (12h uptime): renderer 690MB, GPU process
193MB. Opening `#archlinux` and scrolling back three pages left **8,338 DOM
nodes** mounted, and coming back to the present released **none** of them -
still 8,336.
## The solution
Draw the log as a tail of what is loaded - 120 rows - and let reading upwards
lengthen it. The scroll-to-top gesture already asks the daemon for an older
page; it now does the two in order, first drawing what this window is holding
and has not built, then fetching what it does not hold at all. Coming back to
the present releases it, either by the jump button or by scrolling there, and
switching conversations resets it.
A tail rather than a floating window: a chat log is read from the bottom, so
everything above the drawn part is older and there is nothing below it. That
means no spacer element, no estimated row heights and no second opinion about
how tall the page is - the scroll-anchoring machinery measures exactly what it
did before, which matters given how much of it is there to hold somebody's
place.
Two details that go with it: the first drawn row always starts its own run
(the message it would group with is not on the page, so hiding its author
would leave it unattributed), and the top-of-list trigger now fires only for a
reader who has actually scrolled up - a freshly built list sits at the top for
the moment before it is scrolled to the bottom, and reading that as "show me
more" both lengthened the page and asked the daemon for a page nobody wanted,
every single time a conversation was opened.
Avatars in the log and in the member list are `loading="lazy"` now as well.
## Result
| | before | after |
|---|---|---|
| DOM nodes after reading back three pages | 8,338 | 3,794 |
| DOM nodes once back at the present | 8,336 | 1,374 |
Fixed in 46e3d7f "Draw the part of the log somebody is reading".
Salastil
added the bug label 2026-09-07 17:52:34 -04:00
Verified against #archlinux with the client running: 8,338 DOM nodes after reading back three pages became 3,794, and 8,336 once back at the present became 1,374.
Finished - https://git.salastil.com/Salastil/moho/commit/46e3d7f9530fcb1f46fbcb2fad0260110d78b2a6
Verified against `#archlinux` with the client running: 8,338 DOM nodes after reading back three pages became 3,794, and 8,336 once back at the present became 1,374.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Reported as: moho sluggish and holding 878MB after a day open.
The problem
MessageListbuilds a row for every message in the loaded list. There is nowindowing anywhere in the renderer - one
IntersectionObserverin the wholetree, and it is the history-gap filler. A buffer that has had history loaded
holds up to two thousand messages, and each one is a mounted
MessageRow:avatar, embeds, pictures, reactions, code blocks, the lot. Every one of them
is live whether it is on screen or a thousand rows above it.
Measured on the running client (12h uptime): renderer 690MB, GPU process
193MB. Opening
#archlinuxand scrolling back three pages left 8,338 DOMnodes mounted, and coming back to the present released none of them -
still 8,336.
The solution
Draw the log as a tail of what is loaded - 120 rows - and let reading upwards
lengthen it. The scroll-to-top gesture already asks the daemon for an older
page; it now does the two in order, first drawing what this window is holding
and has not built, then fetching what it does not hold at all. Coming back to
the present releases it, either by the jump button or by scrolling there, and
switching conversations resets it.
A tail rather than a floating window: a chat log is read from the bottom, so
everything above the drawn part is older and there is nothing below it. That
means no spacer element, no estimated row heights and no second opinion about
how tall the page is - the scroll-anchoring machinery measures exactly what it
did before, which matters given how much of it is there to hold somebody's
place.
Two details that go with it: the first drawn row always starts its own run
(the message it would group with is not on the page, so hiding its author
would leave it unattributed), and the top-of-list trigger now fires only for a
reader who has actually scrolled up - a freshly built list sits at the top for
the moment before it is scrolled to the bottom, and reading that as "show me
more" both lengthened the page and asked the daemon for a page nobody wanted,
every single time a conversation was opened.
Avatars in the log and in the member list are
loading="lazy"now as well.Result
Fixed in
46e3d7f"Draw the part of the log somebody is reading".Finished -
46e3d7f953Verified against
#archlinuxwith the client running: 8,338 DOM nodes after reading back three pages became 3,794, and 8,336 once back at the present became 1,374.