DSH / Atlas
2026-09-18implementedarchitecturearchived 2026-09-30

Bound turn navigation work and batch historical jumps

限制轮次导航工作量并聚合历史跳转

A long Session makes three independent costs visible: a turn rail with one DOM mark per known turn, transcript navigation that rediscovers an already known landing, and historical jumps that publish every fetched page. Each intermediate prepend rebuilds growing conversation indexes and exposes another transcript update before the requested turn is available. Combining these concerns into one controller obscures which

English

Problem

A long Session makes three independent costs visible: a turn rail with one DOM mark per known turn, transcript navigation that rediscovers an already known landing, and historical jumps that publish every fetched page. Each intermediate prepend rebuilds growing conversation indexes and exposes another transcript update before the requested turn is available. Combining these concerns into one controller obscures which operations require DOM geometry and which only apply reading or paging policy.

Decision

Fixed-pitch virtual rail

TurnNavigator uses TanStack React Virtual with a fixed 10px pitch. Only the visible range, overscan, and the focused mark with its neighbors remain mounted. The library owns the size container and absolute mark transforms. The full turn metadata remains resident; this is rail virtualization, not transcript virtualization.

The rail uses ResizeObserver-delivered viewport dimensions and fixed item sizes. Its offset calculation clamps against cached geometry and invokes the supported scrollBy operation, avoiding the library's DOM-based maximum-offset calculation used by scrollToIndex. The first usable size places the latest committed active turn directly after the body has restored its reading state. A ref exposes independent activation and rail-only scrolling. Native pointer targets replace coordinate hit testing, and mark length changes use transforms. Layout containment belongs to the floating rail frame; it does not promise that browser geometry reads flush only that subtree.

Two-layer transcript navigation

ChatViewport owns DOM reads, clamped writes, native events, and turn anchors. It returns the actual landing with available turn and semantic-position facts. Reading policy consumes those facts without another hit test; the viewport ignores its own unchanged scroll echoes and events from the nested rail.

ChatReading owns follow-tail policy, semantic restoration, and sampled reader movement. ChatNavigation owns loaded or unloaded jumps, paging anchors, replacement tasks, and fallback. useChatScroll supplies committed inputs and coordinates these owners. Timer and animation-frame completion invoke the owners directly instead of incrementing React tick state. The pinned-delivery rule remains independently necessary: small reader gestures cannot be erased by intervening layout growth.

Shared nested-follow mechanics and control placement are recorded in the Chat scroll and footer decision; sampled outer reading and historical navigation retain the owners above.

One publication per historical jump

Session.loadThrough retains accepted pages and a private paging cursor until its shared target is covered or loading ends. It reverses the page list and flattens it once, then publishes one ordered prepend. Live events continue through their normal publication path. A later page failure publishes the successful prefix once; stream or window replacement discards obsolete buffered pages. Single-page loadOlder remains immediate, and repeated jump requests retain the existing lowest-target policy.

Theoretical work model

Let T be known turns, V mounted virtual marks, P fetched pages, B entries per page, and W0 entries already loaded. These are work-count estimates, not timing measurements.

OperationPrevious workCurrent work
Rail DOM and React mark traversalO(T)O(V); turn metadata and initial index/cache construction remain O(T)
Active-turn index lookup with unchanged itemsLinear search, O(T)Map lookup, expected O(1)
Rail target-offset calculationSynchronous DOM extent/viewport readsFixed arithmetic and cached sizes; no DOM extent read on this path
Known transcript jumpLanding followed by position rediscoveryReuse the landing; no hit test, but existing anchor-selector and turn lookup costs remain
History-driven linear index passesO(P * W0 + B * P²)O(W0 + B * P) for one final pass, plus O(B * P) buffer flattening
History-driven sortingRepeated sorting of each growing windowOne final-window sort, O((W0 + B * P) log(W0 + B * P))
Historical prepend publicationsPOne; live-event and loading-state updates remain independent

For illustration, 10,000 turns and about 50 mounted marks imply roughly 99.5% fewer rail marks, not a 200× end-to-end speedup. With 20 equal pages and a negligible initial window, a full linear pass after each page visits 210B entries in aggregate; one final pass visits 20B, about 10.5× less work for that pass. Page requests remain 20, and the final transcript still needs assembly, rendering, and layout.

Alternatives considered

Containment or React batching alone. Containment does not remove repeated JavaScript work. Suppressing React renders after each published page still leaves conversation assembly and index rebuilds upstream.

Patch the virtualizer. The supported callbacks and relative-scroll operation cover fixed-height placement without maintaining a dependency fork. The native scroll container still needs a total extent; removing that extent would remove native scrolling, not virtualize it.

One large scrolling controller. It centralizes state but couples DOM mechanics to follow and paging policy. Separate owners allow business changes without copying geometry operations.

Virtualize the whole transcript. That introduces renderer lifetime and semantic-anchor constraints outside this change. Batching the requested historical jump removes intermediate publications without changing transcript residency.

Consequences

Loading a distant turn retains the current transcript until the batch lands, instead of revealing intermediate pages. Buffered pages add references proportional to newly fetched entries. Main-transcript geometry reads, semantic-anchor queries, ordinary native-scroll metrics, final layout, and center-width reflow during Sidebar animation remain; none is claimed eliminated by this change. Transform and color changes may still repaint, and assigning scrollTop may still deliver a native scroll event.

Rail tests cover bounded mounting, first placement, fixed-size controls, and focus retention. Chat tests cover semantic restoration, follow attribution, own-scroll echoes, and navigation without hit testing. Session tests cover atomic prepend publication with live delivery, failure, and replacement. Browser navigation covers real scrolling and unloaded-turn landing. Functional evidence is separate from the theoretical estimates above.

中文

Problem

长会话会放大三类独立成本:轮次轨道为每个已知轮次挂载一个 DOM 刻度,正文导航重新查找已知落点,以及历史跳转逐页发布数据。每次中间前插都会重建不断增大的对话索引,并在目标轮次可用前再次更新正文。把这些职责放进一个控制器,会使需要 DOM 几何的操作与只负责阅读、分页策略的操作难以区分。

Decision

固定间距虚拟轨道

TurnNavigator 使用 TanStack React Virtual,间距固定为 10px。只保留可见范围、预加载项,以及键盘焦点刻度及其相邻项。库负责尺寸容器和绝对定位刻度的变换。全部轮次元数据仍驻留内存;这里虚拟化的是轨道,不是正文。

轨道采用 ResizeObserver 提供的视口尺寸和固定条目高度。偏移计算根据缓存几何限制范围,再调用库支持的 scrollBy,避开 scrollToIndex 使用的基于 DOM 的最大偏移计算。正文恢复阅读状态后,轨道在首次获得可用尺寸时直接定位到最新已提交的活跃轮次。ref 分别提供激活轮次和仅滚动轨道的操作。原生指针目标替代坐标命中测试,刻度长度通过变换改变。布局包含作用于悬浮轨道外框,但不承诺浏览器几何读取只刷新该子树。

两层正文导航

ChatViewport 负责 DOM 读取、限制范围后的写入、原生事件和轮次锚点。它返回实际落点及可用的轮次、语义位置信息。阅读策略直接消费这些信息,无需再次命中测试;视口会忽略自身位置未变的滚动回声,以及内嵌轨道发出的事件。

ChatReading 负责跟随底部、语义恢复和读者移动采样。ChatNavigation 负责已加载或未加载轮次跳转、分页锚点、任务替换和回退。useChatScroll 提供已提交输入并协调各对象。定时器与动画帧完成时直接调用负责对象,不再递增 React tick 状态。贴底事件规则 仍有独立必要性:期间发生的布局增长不能抵消读者的小幅操作。

共用的嵌套跟随机制与控件定位见 Chat 滚动与页脚决策;外层采样阅读与历史导航仍由上述对象负责。

每次历史跳转统一发布

Session.loadThrough 在共享目标被覆盖或加载结束前,保留已接受的页面和私有分页游标。它只反转一次页面列表并展开一次,然后发布一次有序前插。实时事件继续走正常发布路径。后续页面失败时,已成功取得的部分统一发布一次;流或窗口被替换时丢弃失效的暂存页面。单页 loadOlder 仍立即发布,重复跳转请求保留既有的最低目标策略。

理论工作量模型

设 T 为已知轮次数,V 为已挂载虚拟刻度数,P 为获取页数,B 为每页条目数,W0 为原先已加载条目数。以下估算工作量,不代表耗时测量。

操作原有工作量当前工作量
轨道 DOM 与 React 刻度遍历O(T)O(V);轮次元数据与初始索引、缓存构建仍为 O(T)
条目未变时查找活跃轮次索引线性搜索,O(T)Map 查找,期望 O(1)
轨道目标偏移计算同步读取 DOM 总高度、视口尺寸固定算术与缓存尺寸;此路径不读取 DOM 滚动总高度
已知正文轮次跳转定位后重新查找位置复用落点;不做命中测试,但既有锚点选择器与轮次查找成本仍在
历史驱动的线性索引遍历O(P * W0 + B * P²)最终一次遍历为 O(W0 + B * P),另有 O(B * P) 暂存展开
历史驱动的排序对每个不断增大的窗口重复排序对最终窗口排序一次,O((W0 + B * P) log(W0 + B * P))
历史前插发布次数P一次;实时事件与加载状态更新仍独立发生

例如,10,000 个轮次只挂载约 50 个刻度,意味着轨道刻度约减少 99.5%,不代表端到端加速 200 倍。若获取 20 个等大页面且初始窗口可忽略,每页到达后做一次全量线性遍历,累计访问 210B 个条目;最终只遍历一次访问 20B,该遍历工作量约减少到原来的 1/10.5。分页请求仍为 20 次,最终正文仍需装配、渲染和布局。

Alternatives considered

只加布局包含或 React 批处理。 布局包含不会删除重复的 JavaScript 工作。仅抑制每页发布后的 React 渲染,也会保留上游对话装配与索引重建。

修改虚拟化库。 现有回调和相对滚动操作已能完成固定高度定位,无需维护依赖分支。原生滚动容器仍需总高度;删除总高度会使原生滚动失效,而不是实现虚拟化。

单一大滚动控制器。 它能集中状态,却把 DOM 操作与跟随、分页策略耦合。拆分负责对象后,业务变更无需复制几何操作。

虚拟化整个正文。 这会引入本次范围之外的 renderer 生命周期与语义锚点约束。聚合目标历史跳转即可删除中间发布,无需改变正文驻留方式。

Consequences

跳转远处轮次时,当前正文会保持到整批数据落地,不再逐页展示中间结果。暂存页面增加与新获取条目数成正比的引用。正文几何读取、语义锚点查询、普通原生滚动指标、最终布局,以及侧栏动画改变中间区域宽度时的重排仍然存在;本次不声称消除了这些成本。变换与颜色变化仍可能重绘,赋值 scrollTop 仍可能产生原生滚动事件。

轨道测试 覆盖有界挂载、首次定位、固定尺寸控制与焦点保留。Chat 测试 覆盖语义恢复、跟随归因、自身滚动回声与不做命中测试的导航。Session 测试 覆盖实时事件并发下的原子前插发布、失败与替换。浏览器导航 覆盖真实滚动与未加载轮次落点。功能证据与上述理论估算相互独立。