Skip to content

Make a full feed cost what a screenful costs - #32

Merged
JRBlaze merged 1 commit into
mainfrom
feed-costs-a-screenful
Sep 4, 2026
Merged

JRBlaze merged 1 commit into
mainfrom
feed-costs-a-screenful

Conversation

@JRBlaze

@JRBlaze JRBlaze commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Reported as: on Kick watching xQc with both Kick and Twitch chats joined, the whole machine
became unusable the moment the overlay's counter reached 400 messages — mouse barely moving,
Task Manager taking minutes to open, the page's video freezing and the chat stalling in cycles.
A refresh fixed it until it hit 400 again.

400 is not a threshold, it is the ceiling

Nothing breaks at 400. 400 is the default maxMessages, so it is where the feed stops
growing — and therefore where the browser's cost of holding that feed stops growing and settles
at its worst. That is why it was deterministic, why a refresh bought exactly one more climb, and
why it recurred at the same place.

What it was not

Driven through the real render/feed path in tests/harness.html (Kick adapter, both platforms
merged, real animated 7TV emotes):

  • 600 messages/second sustained at the cap: zero long tasks, 120fps, flat heap. The message
    path is not the problem.
  • No leak — trimmed rows are released, DOM row count and heap stay flat over thousands of
    messages.
  • Kick emotes are 70×70, not oversized originals.

Which matches the report: a pegged JS thread makes the tab janky, not the mouse. Mouse lag,
video freezing and a slow Task Manager are the raster/image/GPU layer — exactly the part a
requestAnimationFrame-based measurement inside the tab cannot see.

What it was

The feed kept 400 rows fully live. Every one of them — including the ~370 nobody can see — was
laid out on each reflow, painted into the scroller's tiles, and holding decoded emote images the
browser kept advancing frame by frame for the animated ones that make up most of a busy chat.
Two chats merged into one feed on top of a page playing video is a few thousand live animated
images at once.

The fix

content-visibility: auto on feed rows, so the browser skips layout, paint and image work for
anything scrolled out of view. contain-intrinsic-size: auto 24px keeps the scrollbar honest:
every row is measured for real when it arrives at the live end, and that measurement is
remembered while the row is skipped.

Measured on a full 400-row feed of real animated emotes:

before after
full relayout 28.9 ms 8.1 ms
scrollHeight 12506 px 12506 px

Verified that scrolling back renders every row on demand with images intact, and that pinning,
the jump-to-live button and trimming all still behave.

Second, smaller fault of the same kind: queue() ran querySelector('.fcm-empty') over the
whole feed on every message, so the cost of finding nothing grew with every row kept. The
feed now holds the placeholder and owns showing and clearing it.

Tests

node tests/run.js — 1712 passed, 0 failed (+10 covering the placeholder's new ownership).

Caveat

The exact freeze could not be reproduced in the harness, whose mock page has no video competing
for the GPU. The fix targets the layer the evidence points at rather than one reproduced in
isolation. Options → "Messages kept in the feed" remains the direct lever if it is ever
needed again.

🤖 Generated with Claude Code

A merged feed keeps 400 messages so you can read back through them, and
every one of those rows was a live piece of the page: laid out on each
reflow, painted into the scroller's tiles, and holding decoded emote
images that the browser kept advancing frame by frame for the animated
ones that are most of a busy chat. Only the last screenful is ever on
show; the rest were being paid for in full.

Nothing goes wrong at 400. 400 is where the feed stops growing, which is
to say where that cost stops growing and settles at its worst — which is
why a busy channel was fine for the first few hundred messages, why a
refresh bought exactly one more climb, and why it came back at the same
place every time. Two chats merged into one feed on a page that is also
playing video is a few thousand live animated images at once, and the
machine has to find room for all of them.

The rows now carry content-visibility, so the browser skips layout, paint
and image work for anything scrolled out of view. The `auto` in
contain-intrinsic-size is what keeps the scrollbar honest: every row is
measured for real when it arrives at the live end and that measurement is
remembered while it is skipped, so scrolling back lands where you expect
rather than on an estimate. On a full feed of animated emotes a complete
relayout goes from 28.9ms to 8.1ms, with scrollHeight identical either
way.

The feed's own hot path had a smaller version of the same fault: every
queued message ran querySelector('.fcm-empty') over the whole feed, so
the cost of finding nothing grew with every row kept — thousands of nodes
walked per message to fail to find a placeholder that has not been there
since the first message of the session. The feed holds the placeholder
instead, and owns showing and clearing it.

None of this was the message path, which handles 600 messages a second at
the cap with no long tasks and a flat heap. It was what the page was
being asked to draw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JRBlaze
JRBlaze force-pushed the feed-costs-a-screenful branch from 9b52bd1 to 60fbbac Compare September 4, 2026 02:56
@JRBlaze
JRBlaze merged commit 87af7b0 into main Sep 4, 2026
3 checks passed
@JRBlaze
JRBlaze deleted the feed-costs-a-screenful branch September 4, 2026 02:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b52bd1f0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/content/overlay.css
Comment on lines +550 to +553
.fcm-msg,
.fcm-sys {
content-visibility: auto;
contain-intrinsic-size: auto 24px;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent paint containment from clipping moderation controls

When a moderator uses the minimum text-size setting on a short one-line message, content-visibility: auto applies paint containment to the row, so content outside its box is clipped. The absolutely positioned moderation bar does not contribute to row height: it reaches roughly 28px from the row's top, while a plain 10px message row is only about 20.5px tall, causing the bottom of the buttons to be visibly clipped and reducing their clickable area. Give these rows enough height/clip margin or avoid paint containment where the bar is present.

Useful? React with 👍 / 👎.

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