dsh-scroll-boost
Verifieddsh-scroll-boost · v0.7.2 · MIT · Web UI
Client-side scroll performance booster for the DeepSeek Harness web GUI: DSH-aware surface detection, offscreen row skipping with real height truth, step-level chunking for long PTC reasoning groups and block-level chunking elsewhere, compositor hints, id
Install
dsh plugin add dsh-scroll-boost Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-scroll-boost
一个纯客户端的 DeepSeek Harness Web GUI 滚动优化插件。
它不卸载插件、不改 React 组件、不改主题,也不降低画面质量:只在空闲时给「真正可滚动容器里的
长列表直接子行」加上 content-visibility: auto,让浏览器跳过屏幕外行的布局与绘制,从而减少
滚动时的合成和重排压力。
解决什么问题
安装 @changfenhuang/dsh-genui 等会在 DOM 中生成大量围栏内容的插件后,长会话滚动可能出现掉帧。
根因通常是浏览器仍在对整页 DOM 做布局/绘制,而滚动帧预算只有约 16.7ms。
dsh-scroll-boost 的做法是:只让屏幕外的「行」不参与渲染,屏幕内的画面完全不变。
认表面,而不是猜列表
通用做法是「在页面里找一个看起来可滚动的容器,再把它的直接子节点当成行」。这在 DSH 里失效,
因为 聊天记录被渲染进一个 display: contents 的槽位——槽位自己没有盒子,那套「向下走直到
找到行列表」的搜索永远看不到它。
所以插件直接认 DSH 自己的稳定 data-* 表面,并按表面类型决定策略:
| 表面 | 选择器 | 策略 | 原因 |
|---|---|---|---|
chat-flow |
[data-slot="conversation.chat.flow"] |
标记其直接子行 | 聊天记录没有虚拟化,行数随会话增长,是收益最大的地方 |
conversation-scroll |
[data-conversation-scroll] |
只加 will-change 提示 |
这是真正的滚动口 |
trajectory |
[data-trajectory-scroll] |
静态规则只跳内容单元格 | DSH 每帧重建行,JS 写的属性活不到绘制 |
summary-region |
[data-summary-scroll-region] |
只加提示 | 摘要区是短列表,不需要跳行 |
composer-input |
[data-input-scroll] |
只加提示 | 输入框内部滚动口,没有行可跳;认出来是为了滚动时暂停扫描 |
approval-panel |
[data-approval-scroll] |
只加提示 | 审批卡片内部滚动口,同上 |
question-panel |
[data-question-scroll] |
只加提示 | 提问面板内部滚动口,同上 |
认不出来的界面仍走通用回退路径,所以 DSH 改版不会让插件彻底失效。文档缩放视口
[data-document-zoom-scrollport] 有意不认:它是缩放变换的目标,加合成提示会改变文档的
栅格化方式。
优化分层
| 层 | 做什么 | 关键点 |
|---|---|---|
| A. 离屏跳过(核心) | 给长列表的直接子行加 content-visibility: auto + contain-intrinsic-size |
先把真实行高写成字面量,再打标记;不改变可见区域 |
| B. 容器提示 | 给面积最大的最多 8 个滚动容器加 will-change: scroll-position |
只加属性,不每帧写 DOM;这是唯一会改像素的一层,可用 setPixelSafe(true) 关掉 |
| C. 空闲调度 | requestIdleCallback 分片扫描,每片约 8ms |
滚动期间暂停扫描/测量,滚动结束后继续 |
| D. 增量维护 | MutationObserver 监听 childList + 少量关键属性,50ms 去抖 |
新消息到达后只补新增行,不重扫全页 |
| E. 首屏快扫 | 视图挂载后下一帧只标记视口上下 1.5 屏内的行 | 上限 120 行 / 300 次 rect 读,剩下的交给空闲分片 |
| F. 块级跳过 | 长思考正文按直接块子元素加 content-visibility,高度先实测再钉 |
只碰 [data-markdown-variant="compact"];只在正文落定后打标记 |
| G. 步级跳过 | PTC 过程组按 step 跳过:一组一个盒子,而不是几十个段落 | 认 [data-step-process-content][data-chat-flow] 的直接子节点;组外仍走 F 层兜底 |
| H. 流式 containment | 正文还在流式增长时只加 contain: layout |
不打跳过标记、不钉高,等正文落定再交给 F / G |
| I. 生命周期与缓存 | bfcache 暂停/恢复、失联表面剪枝、resize 从观察条目取值 | 这些都只在「缓存已经错了」时触发;稳态下不增加每帧成本 |
扫描顺序是:表面发现 → 候选容器 → 行列表 → 逐行测量/标记。每一步都按空闲时间分片,避免启动时 产生长任务。首屏快扫是例外:它只跑一帧、只碰近端行,用来把「第一次可见帧就已经被优化」从几百 毫秒提前到十几毫秒。分片内部采用批量读写:一个分片里的行先全部读完、再全部写入,把每行一次 布局重算压成每批一次。
行高必须是真值
DSH 自己的「当前读到第几行」是拿 getBoundingClientRect() 做二分的。一旦占位高度和真实高度对不上,
跳转、跟随尾巴就会落到错误的行——画面不对,比掉帧更严重。所以行高在这里是一等公民:
- 离屏行不读布局,直接用按
data-chat-flow-key记住的历史高度(WeakMap + localStorage); - 可见带才实测一次,且减去 padding / border(
contain-intrinsic-size描述的是内容盒); - 高度以 0.01px 精度直接写成
contain-intrinsic-size: auto <N>px字面量。不走var()——每个被跳过的 盒子在每次样式重算时都要解析一次自定义属性,实测比字面量慢约 6 倍(18.1 ms vs 2.8 ms);--sb-h仍然写,只作为report()和人工排查的读数; ResizeObserver盯着近端行,行高变化时立即纠正;校正时只读观察回调自带的contentRect, 绝不在回调里调getBoundingClientRect()(原因见「宽度变化」一节)。
实测(600 行聊天记录,headless Edge):标记后 scrollHeight 完全不变,行位置最大偏差 9px,
DSH 的二分定位依然命中正确的行。
轨迹表:为什么要一条静态规则
轨迹面板确实做了虚拟化,但它在滚动时每帧重建 <tr>(tbody.replaceChildren)。插件原本的
打标记方式是「读一次高度 → 写一个 data-* 属性」,属性写上去的下一帧就被新节点换掉了,等于什么
都没做——轨迹页的手感反而回到没装插件时。
所以换成一条静态 CSS 规则:规则随样式表一次性生效,浏览器在它自己的样式计算里应用,不需要 主线程逐行写 DOM,也就不会被重建冲掉。
[data-trajectory-scroll] tbody tr[data-record-index]:not([data-turn-start]):not([data-terminal-request-boundary]):not([data-request-only]):not([data-collapsed-summary]) > td:last-child {
content-visibility: auto;
contain-intrinsic-size: auto 30px;
}
三个约束来自对真实轨迹表的像素级验证:
- 只标内容单元格(
td:last-child)。事件列里有绝对定位、故意越界的turnRail与requestBoundaryControl,content-visibility的 paint containment 会把它们裁掉。 - 排除
data-turn-start行。这类行用td:before在自己上边缘画 1px 分隔线,同样会被裁。 - 排除非标准高度行(9px 边界行、0px 请求行、24px 折叠摘要行)。它们的占位高度和真实高度 不一致,会让滚动条漂移。
实测(2000 行虚拟化轨迹表,每帧重建 46 行,富文本内容):每帧脚本+布局耗时中位数 6.3 → 5.0 ms
(−21%),p95 11.6 → 8.3 ms(−28%);行高集合与 scrollHeight 完全不变;同位置截图与未安装
插件时 diff = 0 像素。
长思考正文:按块跳过
展开一条很长的思考过程(尤其 PTC 模式下多轮 reasoning 叠加)后滚动会卡,原因和轨迹页完全不同:
整段思考正文是一个 Markdown 子树,高度远超视口,所以「按行跳过」永远没有可跳的行——
它在视口里,content-visibility 就不生效,每帧都要对整棵子树做一次 style + layout。
把跳过粒度从「行」下沉到「块」:只在思考正文的根上工作,给它高度超过 1.5 屏的直接块子元素 (段落、代码块、列表、表格)逐个打标记。
[data-scrollboost-chunk] {
content-visibility: auto;
contain-intrinsic-size: auto 120px; /* 真实高度由脚本逐个写回 */
}
命中根用的是 [data-markdown-variant="compact"]:DSH 只在思考正文上输出这个属性(普通回答的
Markdown 没有),所以不依赖任何哈希类名。
关键约束是必须先钉真实高度。块在离屏时读不到布局,contain-intrinsic-size 若给一个估算值,
整段正文的 scrollHeight 会塌掉(实测 480 块时漂移 −23444px),页面顶部那个 sticky 的思考头也会
被顶出屏幕。所以每个块都是「先量、再钉、后打标」,并且:
- 流式输出中的正文一律不动(
data-state还在 running)——它的文字每个 token 都在变,钉住的高度 立刻过期; - 流结束时
data-state翻转、但没有任何节点新增,这条属性变化本身会触发一次分块; - 块数量变化时(React 重建了正文)整组解钉重来,不会留下过期的占位;
disable()会把所有钉高与标记清干净,scrollHeight完全还原。
实测(12 个思考块组 × 42 块、真实类名、headless Edge):每帧脚本+布局耗时中位数 3.3 → 0.2 ms,
p95 6.3 → 0.3 ms;scrollHeight 前后一致(48512),每个正文根的高度不变(4016),sticky 思考头
位置不变;离屏块 checkVisibility 确认被跳过;disable() 后块标记清零、scrollHeight 还原。
像素核对:把「插件整体」与「插件但块规则不生效」两组截图对比,diff = 0——块级跳过本身不改变 任何像素。整体与未安装时仍有少量差异,来自 B 层的
will-change: scroll-position(见「代价」一节), 与这一层无关。
PTC 过程组:按步跳过
PTC 模式下的「思考过程」不是一个 Markdown 子树,而是一个过程组:DSH 把每一轮子调用渲染成组里 的一个 step,展开后整组直接铺在会话滚动流里。
<div data-step-process data-group-expanded-mode>
<div data-step-process-body> <!-- 展开时 max-height:none,没有内层滚动口 -->
<div data-step-process-content data-chat-flow>
<div data-chat-flow-key>…</div> <!-- 一个 step,往往就有好几千像素 -->
…
块级跳过在这里会失效,而且失效得很隐蔽:它要求「正文根」高过 1.5 屏、且至少有 6 个直接块子元素, 可一个 step 的正文常常两条都不满足,于是整组一块都没跳。即使跳上了,粒度也不对——真正该跳的 盒子是 step,不是 step 里那几十个段落。
所以过程组按步级处理:先认 [data-step-process-content][data-chat-flow],给它的每个直接子 step
打标记;组外的长思考正文仍按块跳过作为兜底。
两个数字决定了这个设计(40 步 × 70 块,每帧 style recalc + layout):
| 方案 | 中位数 | p95 |
|---|---|---|
| 不优化 | 9.6 ms | 14.6 ms |
| 块级(每步 70 块) | 3.7 ms | 5.8 ms |
| 步级(每步 1 盒) | 0.2 ms | 0.5 ms |
被跳过的盒子数量才是成本主因,节点数不是。
流式期间仍然不动:step 里只要还有 [data-state="running"],它就不参与;流结束时 data-state 翻转
会单独触发一次分块。一轮 PTC 是一个个 step 陆续结束的,所以组会边跑边长:新落定的 step 由一次
增量补齐(只测新增的那几个盒子,已钉的盒子不重新测量、不重新布局),只有 React 重建了整组
(已钉的盒子脱离文档)才整组解钉重来。
实测(PTC 40 步 × 70 块,真实类名,headless Edge):每帧耗时中位数 9.6 → 0.2 ms,p95
14.6 → 0.5 ms;scrollHeight 前后一致(134759),40 个 step 全部打上标记、内部的 Markdown 块
一个都没碰;disable() 后标记清零、scrollHeight 还原。增量补齐也单独验过:往已钉好的组里再追加
一个刚结束流式的 step,它会在一秒内被钉上,scrollHeight 变化 0px。
流式增长的正文:只加 containment
长思考正文在流式期间滚动最卡,流结束后反而不卡了。第一直觉是「既然离屏块跳过有效,那就给 流式中的块也打上标记」——实测这个方向更慢:
| 每帧 style recalc + layout(单条 3000 块正文,每帧追加 1 个 token) | 中位数 | p95 |
|---|---|---|
| 不加任何东西 | 2.2 ms | 4.8 ms |
给每个块打 content-visibility |
5.2 ms | 9.9 ms |
只加 contain: layout |
1.6 ms | 4.5 ms |
| 两者都加 | 3.7 ms | 8.1 ms |
为什么:正在增长的正文每来一个 token,整条正文都要重排一次。content-visibility: auto 在
流式中会不断判断「这个块离屏了吗」,这个判断本身就变成每帧的额外开销;而且块的钉高会随流式
不断过期、不断重写。它只在内容已经落定、块不再变的时候才是净收益。
contain: layout 不做跳过、不钉高度、不改像素,它只告诉浏览器「这个盒子的布局不会影响外面」。
于是正文内部的重排被限制在这个盒子里,不再扩散到整页。实测正好是这个杠杆:
- 流式期间每帧中位数 2.2 → 1.6 ms,p95 4.8 → 4.5 ms;
- 单 token 追加延迟 2.1 → 1.1 ms(约 2×);
- 帧级探针(3000 块正文 + 每帧 1 token + 每帧滚动跳变):逐块钉高产生 133 个长帧(29.6%), 新策略 0 个;
- 截图对比
plainvscontain:diffpix = 0,bbox = None——一个像素都没变; scrollHeight前后完全一致,行位置无漂移。
所以现在的分工是:
| 状态 | 策略 |
|---|---|
正文还在流式增长([data-state="running"] / 有 [data-streaming] 祖先) |
只给正文根加 contain: layout,不打跳过标记 |
| 正文已落定 | 走 F / G 层的块级 / 步级跳过(这才是收益区) |
| 整行已离屏 | 走 A 层的行级跳过 |
宽度变化与生命周期
宽度变化由 ResizeObserver 自己回答。 拖动会话宽度手柄时,被观察的行(最多 200 行)在同一帧
全部改变宽度,回调随即收到 200 个条目。回调里什么也不测:ResizeObserverEntry.contentRect
本身就是内容盒,正好是 contain-intrinsic-size 描述的那个盒子。再加一条「与当前值相差不到 0.5px
就不写」,纯宽度变化就不必把每行样式都标脏一次。
这条不只是性能问题。早先的写法在回调里对每个条目做「读 getBoundingClientRect → 读
getComputedStyle → 写样式」,读写交替,每次读都会把刚标脏的样式重新布局一遍——也就是 200 次
强制同步布局,而且正好发生在你拖动的过程中:
| 600 行 / 观察 200 行 / 拖拽 120 帧 | 旧写法 | 现在 |
|---|---|---|
每帧 getBoundingClientRect 次数 |
390 | 0 |
| 拖拽期间总 rect 读 | 46800 | 0 |
| 长帧(>25ms) | 0 | 0 |
长帧数看不出差别——帧间隔被 vsync 钉在 16.7ms,工作量再大,只要还在预算内就只是「刚好赶上」。 所以判据是 rect 读次数,不是帧时间。
表面表会自己收敛。 切会话、开关轨迹面板之后,旧元素原本永远留在内部表里,连同它的行集合一起
钉住一棵已经脱离文档的子树。现在每次发现都做一次剪枝,MutationObserver 也看 removedNodes。
bfcache 回退不会让插件失效。 pagehide 只在真正进 bfcache 时暂停调度(保留标记与缓存),
pageshow 再恢复;真正的卸载仍走 destroy()。
块高会持久化。 键是「最近的稳定祖先 key + 子节点索引路径」,没有稳定 key 的正文不缓存 (纯索引会跨正文撞车)。长会话重开后,展开过的思考正文不必重新测量每个块。
护栏:宁可不动,也不能动错
以下行会被主动跳过,并在 report().skipReasons 里说明原因:
| 原因 | 场景 |
|---|---|
motion |
行正在折叠 / 展开动画中(data-chat-motion)——此时行高在变 |
virtualized |
位于 DSH 已经虚拟化的表面内(轨迹页行不逐个打标记,改用静态规则) |
hidden |
行处于 hidden 状态 |
思考正文还在流式输出时并不计入
skipReasons:它根本不进打标记的流程,改由静态的contain: layout规则保护。report().chunks为 0 才是这个状态的信号。
content-visibility 只在行离屏时生效;行在视口内时没有任何 containment,所以行内的 sticky
折叠头、代码块横幅都能正常工作(实测两种模式的截图与几何完全一致)。真正的风险是占位高度过期:
行内容变高(展开思考过程、展开工具输出)后,占位高度必须跟着更新,否则这行滚出视野再滚回来时,
滚动条会跳一下。带 data-expanded / data-open / data-chat-motion / hidden 的元素发生变化时,
只重新测量它所在的那一行,而不是扫描整个对话。
另外 contenteditable、aria-live 区域、display: table-* 的行也一律不标记。
安装
安装后必须完全退出并重启 DSH(不是只刷新页面)。插件通过
package.json的dsh.bundle.patch和dsh.client自动挂载;host 端入口是空实现,实际逻辑在lib/client.js。
从 npm 安装
dsh plugin --profile desktop add dsh-scroll-boost
从 GitHub 安装
dsh plugin --profile desktop add "github:TWFU-wcy/dsh-scroll-boost"
本地目录(开发时)
git clone https://github.com/TWFU-wcy/dsh-scroll-boost.git
dsh plugin --profile desktop add ".\dsh-scroll-boost"
打包后安装
npm pack
dsh plugin --profile desktop add ".\dsh-scroll-boost-*.tgz"
每个版本在 Releases 都有更新说明, 安装包也附在那里。
安装后确认
重启后打开任意长会话,在开发者工具 Console 执行:
window.__scrollBoost.report()
如果
window.__scrollBoost是undefined:先确认已完全退出并重启 DSH,而不是只刷新页面。 如果report().rows为0:欢迎页、空会话或很短的消息列表没有可标记的行,这是正常的; 打开一个有较长消息列表的会话后再看。
正常应看到类似:
{
version: "<你安装的版本>",
enabled: true,
containers: 1,
hinted: 1,
rows: 599,
marked: 599,
pixelSafe: false,
skipped: 2,
scans: 1,
lastScanMs: 103,
scrolling: false,
fastPasses: 1,
fastRows: 27,
chunks: 42,
chunkRoots: 1,
surfaces: [
{ id: "chat-flow", kind: "rows", rows: 600, marked: 599 },
{ id: "conversation-scroll", kind: "hint", rows: 0, marked: 0 }
],
skipReasons: { motion: 2 },
pending: false
}
几个字段的含义:
surfaces:插件当前认出了哪些界面、认错了多少行(marked小于rows的部分通常是被护栏跳过的)。 离开文档的表面会被剪掉,所以这个数组只反映当前存活的界面;chunks/chunkRoots:展开一条长思考过程后应当大于 0,说明跳过已经生效(PTC 过程的每个 step 也计入chunks)。如果一直是 0,多半是正文还在流式输出;pixelSafe:为true时hinted恒为 0,画面逐像素一致。
手动控制
window.__scrollBoost.report(); // 查看统计
window.__scrollBoost.disable(); // 立即恢复 DOM(移除所有标记与样式)
window.__scrollBoost.enable(); // 重新启用
window.__scrollBoost.scanNow(); // 手动触发一次空闲扫描
window.__scrollBoost.setPixelSafe(true); // 关掉 will-change 提示,换取逐像素一致的画面
window.__scrollBoost.destroy(); // 彻底卸载当前实例
插件简介与图标
DSH 的插件列表从 locale/<语言>.json 读取显示文案(meta.title / meta.description),
缺失时回退到 package.json 的 name / description;图标取自 package.json 的 icon 字段。
本插件提供 locale/en.json 与 locale/zh.json,中文界面下显示为「滚动加速」及对应说明;
icon.svg 为 873 B 的矢量图标,位于包内。修改文案后需要完全重启 DSH 才会重新读取。
验收建议
在同一个会话、同一滚动位置做 A/B(各 3 次取中位数):
- 滚动 p95 帧耗时 ≤ 16.7ms,或相对基线改善 ≥ 30%
- Long Task 数量为 0
- CLS = 0(标记前后布局不跳动)
- 同位置截图 diff = 0(画面不变)
- 1000 行列表内存增长 < 50MB
已知边界
- 只管 CSS 能管的瓶颈:如果掉帧来自 React 重渲染、genui 的全文档扫描、轨迹面板全量
map, 本插件不会直接消除;它减少的是浏览器布局/绘制压力。 - PTC 过程按 step 跳过:一个 step 就是一个几千像素的盒子,滚到它中间时它必须渲染,所以 「单步内部」的滚动仍会整块布局。步内再切块收益为负,实测更慢,所以不做。
- 流式正文只加
contain: layout:正在增长的正文不做跳过,所以流式期间的滚动只有 containment 的收益,没有离屏跳过的收益;正文落定后才会交给块级 / 步级跳过。这是实测后的取舍 (流式期间逐块打标记反而慢约 2 倍,见上文数据表)。 - 首屏「优化生效」有延迟:不是首屏渲染变慢,而是标记要等分片跑完。首屏快扫会在视图挂载后的 下一帧就标记近端行,实测首次可见行标记约 14ms;离屏行仍然由空闲分片慢慢补齐,所以「整页都标记 完」仍需要一点时间。
- 依赖 DSH 的内部结构:认的是
[data-slot="conversation.chat.flow"]、[data-conversation-scroll]、[data-trajectory-scroll]这些内部属性。DSH 改版后可能失效—— 但插件会保留通用回退路径,且report().surfaces能直接看出当前认出了什么。 - 不降低内存和加载时间:DOM 节点仍在,只是离屏行不参与渲染。
- 特殊布局可能误判:嵌套行列表、绝对定位虚拟列表、
display: table-*、sticky/fixed 行会被跳过 或只标记部分行。 - Ctrl+F / 无障碍:
content-visibility: auto的离屏内容仍可被浏览器查找;但极端情况下屏幕阅读器 对「尚未渲染」的内容行为可能不同,遇到问题可disable()。 - GPU 内存:
will-change只在最多 8 个最大容器上使用;现代 Chromium 对scroll-position通常 不直接建整层,实测净增通常 0–50MB,远低于 8GB 显卡压力。
代价:will-change 会改变文字抗锯齿
B 层给滚动口加 will-change: scroll-position,会把该容器提升为独立合成层。层内的文字因此从
LCD 子像素抗锯齿切换到灰度抗锯齿——几何位置、行高、scrollHeight 全都不变,但同一位置的截图会差
几万个像素(实测 73926 个,幅度集中在 17–64,无任何位移)。
A / B 两层共同导致这个现象;块级跳过(F 层)本身经隔离对照验证是 diff = 0。如果更看重「逐像素
一致」而不是滚动帧率,执行 window.__scrollBoost.setPixelSafe(true)——它会让 B 层整体停用(容器不再
加 will-change),A / F / G 三层照常工作。这是一个运行时开关,不需要改代码,也不需要重启 DSH。
开发与测试
cd "D:\Project\dsh-scroll-boost"
npm install
npm test
npm test 会做语法检查、插件元信息检查(locale 字典与 icon 约束)、回归测试(表面识别、护栏
跳过、行高真值复用与字面量钉高、轨迹页静态规则、思考正文分块、PTC 步级分块、流式 containment 与
流式保护、像素一致开关、失效表面剪枝、resize 不读取布局)和 jsdom 冒烟测试(20 行列表、新增行、
disable/enable、cleanup)。
浏览器侧的真实回归(需要本机装有 Edge):
npm run test:browser
harness-chat.html:复刻 DSH 聊天页 DOM(display: contents槽位 + 600 行), 检查首屏标记时机、粘性横幅行是否被跳过、滚动口是否被提示;probe-drift.html:对比标记前后的每行位置与scrollHeight, 确认占位高度没有让 DSH 的行定位落到错误的行;probe-height.html:核对每一行真正写进contain-intrinsic-size的数值(内容盒,已扣除 padding / border)与--sb-h是否一致,确认字面量钉高没有引入累积漂移;probe-resize-plugin.html:模拟拖动会话宽度(每帧改一次宽度 + 强制布局), 统计被标记行上getBoundingClientRect的调用次数——这是拖拽是否卡顿的直接判据;probe-traj-plugin.html:复刻 DSH 轨迹表(2000 行、每帧重建 46 行), 对比安装前后的每帧耗时、行高与scrollHeight,并核对静态规则是否命中普通行、是否避开 turn-start / 边界行 / 请求行 / 折叠行;probe-chunk-plugin.html:复刻 12 组展开的长思考正文(每组 42 个块), 对比分块前后的每帧耗时、正文根高度、scrollHeight、sticky 头位置, 并用checkVisibility确认离屏块真的被跳过、disable()后完全还原;probe-ptc.html:复刻 PTC 展开态的过程组(默认 4 步,可用?steps=40&blocks=70放大), 核对 step 是否逐个打标、内部块是否被放过、scrollHeight与各正文根高度是否零漂移;probe-stream-plugin.html:复刻正在流式增长的长思考正文(3000 块 / 每帧 1 token), 对比四种策略(不动 / 逐块content-visibility/ 只加contain: layout/ 两者都加)的 每帧耗时与长帧数量,并核对最终截图是否逐像素一致。
卸载
dsh plugin --profile desktop remove dsh-scroll-boost
或先执行 window.__scrollBoost.destroy(),再移除插件并重启。
License
MIT