A mail list is a deceptively hard rendering problem. Rows are variable height, the data updates while you scroll, and users expect the scrollbar to mean something.
Windowing is table stakes
We render roughly three viewports of rows and recycle the rest. Nothing surprising there. The surprising part is what breaks afterwards.
Height estimation is the whole game
With fixed rows, virtualization is arithmetic. With variable rows, every wrong estimate shifts the scroll position of everything below it, and the user watches their place drift while they read.
We measure rows once on first paint, cache the result keyed by thread id, and seed the estimate for unmeasured rows from a running median of the same label. Threads with attachments and long subjects get their own bucket, because they were dragging the median in the wrong direction.
Never resize during scroll
The real fix was refusing to apply measured heights while a scroll is in flight. Changes queue up and flush on idle. It means a row can be a few pixels off for one frame, which nobody notices, instead of the list jumping under a thumb, which everybody notices.
Where the remaining time goes
After all of that, the profile is dominated by string work: highlighting search matches and sanitizing HTML previews. Both moved to a worker. The main thread now does layout and nothing else, and the list holds its frame budget on a five-year-old laptop.

