fix(web): thread post-list 闪动 — keyed-diff renderPosts + SSE-aware poll#106
Merged
Conversation
renderPosts 之前每次 refresh 都 innerHTML='' 然后整列重建,叠加 SSE +
8s poll + BroadcastChannel + scheduleRefIndexRefresh 四条管道几乎同时
触发 refreshCurrentThread → renderDetail,导致 thread message 页面经常
肉眼可见闪动(mermaid/markdown/滚动条都会抖一下)。
彻底修复方案:
1. _postSig + _threadSig:把 post 渲染输出依赖的所有字段(speaker /
phase / edited_at / parent / superseded / reactions / inlineDuration
/ content / 前一条 pid 等)拼成 signature。
2. refreshCurrentThread 收到新 thread 后先比 _threadSig,相等就直接
return,不走 renderDetail。SSE 跟 poll 重叠那批冗余刷新全部 noop。
3. renderPosts 改成 keyed diff:用 Map<post_id, {node, sig}> 缓存上一
轮的 DOM 节点。新一轮按 target 列表对齐 postList 子节点 —— sig
相同就复用节点(mermaid SVG / code highlight / event handlers /
scroll anchor 全部保留),不同才重建,多余的尾部节点 remove。
4. renderDetail 接受 force:true,settings 改 avatar/color 之类需要全
量重画的场景传 force 清缓存;其它路径走 diff。
5. SSE 收到 hello/event 后 state.threadEventHealthy=true,startPolling
只在 SSE 不健康时对当前 thread 跑 refetch(unread sweep 不动),
消除 SSE+poll 200ms 内打架。
后续可以再做 renderPostNode 内部 header/body/footer 分区 patch、以及
让 SSE 直接推 patch payload(post added / phase changed),那是后续 PR
的事。
🤖 bot-review (comment-only · phase 1)Diff: Red-line checks:
Phase 2: auto-approve + auto-merge fire only when red-lines are clean, author is internal, and no needs-human path is touched. Block with |
github-actions Bot
pushed a commit
that referenced
this pull request
Jun 26, 2026
PR #93 把 #post-list 的 scrollbar 改成 hover 时显示,通过 hover 切换 scrollbar-width: none → auto 实现。但这个切换在 Firefox 和 Chromium 都会真正占走 ~10px 布局宽度,鼠标移进 post-list 内容左移 10px、移走 再右移回来,肉眼可见的抖动。 修法:scrollbar-gutter: stable 永远预留 gutter,bar 仍然只在 hover 时画。视觉上跟之前完全一致(默认空、hover 出现),但布局宽度恒定, 鼠标进出零 reflow。 (原本是跟 PR #106 keyed-diff 一起修的,但 #106 在 scrollbar 改动 push 上去之前就 merge 了,所以这里单独再开一个 PR。) Co-authored-by: judy <judy@symbolstar.local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
thread message 页面经常肉眼可见闪动(mermaid / markdown / 滚动条都会抖)。
根因调查(见 thread th_19efda72d29_0c1061):
renderPosts每次 refresh 都innerHTML=''然后整列重建。refreshCurrentThread → renderDetail:SSE 推送、8s poll、BroadcastChannel 跨 tab、scheduleRefIndexRefresh兜底。彼此没去重,新 post 到来 200ms 内常见重复刷新 2-3 次。renderDetail末尾还会基于瞬时归零的 scrollHeight 调 scrollTop。修复
数据流单源(signature short-circuit)
_postSig(post, prevPid)和_threadSig(thread),把所有影响渲染输出的字段(speaker/phase/edited_at/parent/reactions/inlineDuration/content/前一条 pid…)拼成 monotonic 签名。refreshCurrentThread拿到新 thread 后先比 sig,相等直接 return,不进renderDetail。SSE+poll 重叠那批冗余刷新全部 no-op。增量 DOM(keyed diff renderPosts)
Map<post_id, {node, sig}>缓存上一轮 DOM 节点。postList子节点:sig 相同复用(mermaid SVG / code highlight / event handlers / scroll anchor 全部保留),不同才重建,尾部多余节点 remove。force: true清缓存。SSE-aware 降级
hello/ message 触发后state.threadEventHealthy = true。startPolling只在 SSE 不健康时对当前 thread 跑 refetch(unread sweep 不动),消除 SSE+poll 200ms 内打架。验证
node --check web/app.js通过。refreshCurrentThread,被 sig 短路。force显式触发。后续
renderPostNode内部 header/body/footer 分区 patch(hash markdown body)。/api/threads/<id>/events改推 patch payload(post added / phase changed),前端直接 apply 不再 refetch。