Repository navigation
perf(web-ui): FlowChat 的 ::selection/::highlight() 规则使 WebKit 走慢速 CSS 匹配,渲染主线程持续满载 #3178
Description
FlowChat 的 ::selection / ::highlight() 规则导致渲染主线程持续满载(Linux / WebKitGTK)
可直接提交到 https://github.com/GCWing/OpenBitFun/issues
标题建议:perf(web-ui): FlowChat 的 ::selection/::highlight() 规则使 WebKit 走慢速 CSS 匹配,渲染主线程持续满载
环境
| 项 | 值 |
|---|---|
| OpenBitFun | 1.0.1(RPM 安装,open-bit-fun-1.0.1-1.x86_64) |
| 系统 | Fedora 44 / KDE Plasma / Wayland |
| GPU | Intel Arc B390(Panther Lake)+ Mesa(gallium 26.x) |
| WebKit | 系统 webkit2gtk4.1-2.52.5-1.fc44(非 AppImage 内置版) |
| 现象会话 | 单会话较大(约 9 MB,含数条 100–380 KB 的工具输出) |
现象
- 打开聊天界面后,渲染进程(
WebKitWebProcess)的主线程长期占用 44% ~ 99% 单核(实测峰值 105% 整体),UI 明显卡顿、输入延迟。 - 卡顿程度与会话长度 / 屏幕上同时渲染的文本量成正比 —— 越聊越卡;流式输出("思考中")时最重。
- 即使不操作(仅窗口在重绘 / 焦点变化触发状态刷新),占用仍有 core ~25% + renderer 21%~50%;同期
ai.log无增长(即没有任何模型调用、没有后台代理在跑)。 - 与 GPU 无关:合成器线程仅占 ~6%,
SkiaGPUWorker正常,无llvmpipe/swrast软光栅。
根因(gdb 栈证据)
对渲染进程主线程反复采样,8 次采样中 6 次落在同一条路径:
#0-3 WebCore::Style::ElementRuleCollector::collectMatchingRulesForListSlow(...)
#4 WebCore::Style::Resolver::styleForPseudoElement(...)
#5 WebCore::RenderElement::getUncachedPseudoStyle(...)
#6 WebCore::MarkedText::collectForHighlights(WebCore::RenderText const&, ...)
另有样本落在:
#0 WebCore::CSS::mix(...) ← 每次重算 color-mix()
#1 WebCore::Style::BuilderCustom::applyValueColor(...)
#2-4 WebCore::Style::Builder::applyProperty / applyCascadeProperty
即:每个文本框、每次重绘,WebKit 都要未缓存地解析高亮伪元素样式,并因此对整套样式表做线性(慢速)规则匹配,还会重复求值 color-mix()。
触发这些规则的 CSS(assets/index-Dg5Vb1KW.css 33 条、assets/ChatPane-BtnTdl19.css 10 条等,共 46 条):
[data-flowchat-selection-root]::selection,
[data-flowchat-selection-root] ::selection, /* ← 后代组合符 */
[data-openbitfun-product-component=conversation-excerpt]::selection,
[data-openbitfun-product-component=conversation-excerpt] ::selection {
background: color-mix(in oklab, var(--…-accent) …);
}
::highlight(openbitfun-flowchat-excerpt) { background-color: color-mix(…); }
::highlight(openbitfun-flowchat-annotations) { background-color: color-mix(…); }
::highlight(openbitfun-flowchat-search-match) { … }
::highlight(openbitfun-flowchat-search-current) { … }问题点:
X ::selection(带后代组合符)无法被 WebKit 的规则分桶优化,只能落到collectMatchingRulesForListSlow。该样式表总规模约 2.5 MB / ≈14 482 条规则(含 778 处:is()/:where()/:not()),于是"文本框数量 × 规则总量"的线性扫描在每次重绘发生。color-mix()在这些规则里每解析一次就要重新求值(栈中单独出现在WebCore::CSS::mix)。::highlight(...)同理:只要应用注册了高亮范围(摘录/批注/搜索命中),每次重绘都会为每个文本框解析其伪元素样式。
实验验证(同一条件、8 个栈样本/组)
| 变体 | collectForHighlights |
getUncachedPseudoStyle |
collectMatchingRulesForListSlow |
|---|---|---|---|
| 原版 | 6 | 6 | 14 |
| 仅拦掉高亮注册(JS,注册点置为 no-op) | 6 | 6 | 16 |
↑ + bind mount 覆盖 frontend/dist |
4 | 4 | 13 |
剥离那 46 条 ::selection/::highlight() 规则 |
0 | 0 | 4(−71%) |
剥离方式:按 CSS 规则(花括号配对)解析后丢弃选择器含 ::selection 或 ::highlight( 的规则(校验:花括号平衡、目标规则清零)。补丁落在
~/.config/openbitfun/data/frontend-workbench/revisions/<activeRevision>/assets/*.css(该副本即实际被加载的前端)。
代价:选中文本回落到系统默认底色,摘录/批注/搜索命中的高亮底色不再显示。
建议修法
- 不要对
::selection/::highlight()使用后代组合符选择器(A ::selection)。改为在需要定制选区的元素上加一个类,用单个复合选择器(X::selection),或直接避免定制::selection。 - 这些规则里不要用
color-mix():换成预计算的静态颜色或 CSS 变量(var(--x-selection-bg)),避免每次解析都求值。 - 把这些规则移出大样式表:慢速匹配的代价与规则总量成正比,放进一个独立的小样式表(或按需注入)可显著降低成本。
- 可选:没有选区/高亮时不要保留活跃的 marked range,避免
MarkedText::collectForHighlights每帧都有活干。
复现步骤
- 在 Linux(WebKitGTK)上启动 OpenBitFun 1.0.1,打开一个内容较多的会话;
R=$(pgrep -f WebKitWebProcess | ...)找到渲染进程,运行
gdb -p $R -batch -ex "bt 8"连续采样若干次;- 多数样本会落在
MarkedText::collectForHighlights → getUncachedPseudoStyle → collectMatchingRulesForListSlow; - 按上文剥离那 46 条规则并重启,栈中该路径消失、主线程占用明显下降。
English summary
On Linux/WebKitGTK, OpenBitFun 1.0.1's web process main thread stays pegged (44–99% of one core) whenever the FlowChat pane is open; the lag scales with transcript size. gdb sampling of the web process main thread repeatedly shows:
MarkedText::collectForHighlights
→ RenderElement::getUncachedPseudoStyle
→ Style::Resolver::styleForPseudoElement
→ ElementRuleCollector::collectMatchingRulesForListSlow
The trigger is 46 ::selection / ::highlight() rules in the bundled stylesheets (e.g. [data-flowchat-selection-root] ::selection, ::highlight(openbitfun-flowchat-excerpt)). Their descendant-combinator selectors cannot be bucketed, so WebKit falls back to a linear scan of the whole (~2.5 MB / ~14.5k rules) stylesheet for every text box on every paint, and color-mix() is re-evaluated each time. Stripping those 46 rules drops the hotspot from 6/8 to 0/8 samples and cuts slow rule matching by ~71%.
Suggested fix: avoid descendant combinators on ::selection/::highlight(), avoid color-mix() in them, and keep them in a small dedicated stylesheet.
Activity
perf(flowchat): Scope selection highlight paint
wsp1911 commentedon Sep 22, 2026
#3183
perf(flowchat): Scope selection highlight paint
guhan33551 commentedon Sep 23, 2026
对修复
3e9a392的实测结果:在 Linux/WebKitGTK 上仍未消除热点感谢修复,我们按该提交自己构建并实测了(不是只看代码)。结论:空闲开销明显改善,但负载下热点路径依旧存在。
怎么验的
v1.0.1源码 →git apply该提交(3e9a392)→pnpm install && pnpm -C src/web-ui run build/usr/lib/OpenBitFun/frontend/dist),后端仍是官方 1.0.1 RPM → 不存在前后端版本不匹配…conversation-excerpt] ::selection0 处;作用域版[data-flowchat-highlight-*]::highlight(…)10 处;JS 含data-flowchat-highlight-*6 处;这些规则里color-mix已消失实测数据
collectForHighlightscollectMatchingRulesForListSlow修复构建在负载下的栈(10 次采样,
gdb -p <WebKitWebProcess> -ex bt):(
WebCore::CSS::mix已为 0 ——color-mix那一条确实修好了 ✓)为什么还不够
::highlight(name)是伪元素规则,右端不可分桶,所以每次解析伪元素样式时,ElementRuleCollector都会退回collectMatchingRulesForListSlow,对整套 author 样式表(本机约 2.5 MB / 14 482 条规则)线性扫描。加属性作用域只减少了"真正匹配的规则数量",没有改变"每次解析都要扫全表"。只要注册了高亮范围
(搜索命中 / 摘录 / 批注),绘制文本时每个文本框仍会走到这条路径,规则越多的样式表代价越大。
建议(在这类 WebKitGTK 环境上真正去掉这条路径)
::highlight()写样式:把搜索命中 / 摘录 / 批注高亮改成真实 DOM(命中片段包<span class="…">或<mark>)来画;这样连伪元素样式解析都不需要,
MarkedText::collectForHighlights直接不参与。(减少解析次数;每次解析的扫描成本仍与总规则数相关)。
顺手发现(本 issue 之外的同类问题)
PdfViewer-*.css里的也是后代组合符写法 —— 打开 PDF 时会有同样的慢路径,建议一并改掉。
我们的临时处理
在 1.0.1 上把那 46 条
::selection/::highlight()规则整体剥离(代价:选中/高亮底色消失)。实测栈命中 0/8、主线程 21.9%。正式版发布后我们会重新评估是否还需要这个补丁。
English summary
We built v1.0.1 +
3e9a392ourselves (web-ui only, official backend) and measured on Linux/WebKitGTK.MarkedText::collectForHighlights(6/10 samples) →getUncachedPseudoStyle→styleForPseudoElement→
collectMatchingRulesForListSlow(15 stack lines).WebCore::CSS::mixis now 0 — thecolor-mixpart of the fix works.Root cause of the remainder:
::highlight(name)pseudo-element rules cannot be bucketed, so every pseudo-styleresolution still linearly scans the whole author stylesheet (~2.5 MB / ~14.5k rules). Attribute scoping reduces the
number of matched rules but not the scan cost; with any highlight registered, every painted text box still takes
this path.
Suggestion: paint search/excerpt/annotation highlights with real DOM elements/classes instead of styling
::highlight(). AlsoPdfViewer-*.cssuses a descendant-combinator::selectionand has the same problem when aPDF is open.
guhan33551 commentedon Sep 23, 2026
补充实验:作用域从属性选择器换成 class 选择器(改善但不足)
接上一条。为了定位"加了作用域为什么还是慢",我们做了第二个实验。
做法
WebKit 的规则分桶键只有 id / class / 标签名,属性选择器会落进"通用桶"。于是把作用域从属性换成 class:
[data-flowchat-highlight-search-match]::highlight(…)→.obf-hl-search-match::highlight(…)(4 条选择器)element.setAttribute('data-…','')→element.classList.add('obf-hl-…')(flowChatHighlights.ts里 4 个定义 + 2 处调用)::selection定制、物化color-mix)保持你们那版不变改动很小(完整 diff 见附件
实验-class作用域改造.diff)。结果(同一测法:10 次
gdb栈采样 + 3×8 秒 CPU 采样)MarkedText::collectForHighlightsStyle::Resolver::styleForPseudoElementElementRuleCollector::collectMatchingRulesForListSlowSelectorChecker::matchRecursivelyWebCore::CSS::mix结论
所以"属性选择器落进通用桶"这条成立 —— 建议在你们那版里把 4 条选择器也改成 class。
collectMatchingRulesForListSlow仍出现在 12/10 个栈样本里。::highlight(name)。只要还用它上色,每次解析伪元素样式就要线性扫 author 样式表(本机 ~2.5 MB / 14 482 条规则)。
选择器怎么写都救不回来。
(
<span class="…">或<mark>)来上色,不再用 CSS Custom Highlight。这样MarkedText::collectForHighlights完全不参与,伪元素解析也就不存在了。参考:我们本机的取舍
性能优先,所以我们目前采用"把那 46 条
::selection/::highlight()规则整体剥离"的方案(代价:高亮底色与自定义选区色消失)。实测主线程 36%、栈命中 0。
等上游改成真实 DOM 方案后我们会立即撤掉该补丁。
English summary
Second experiment: change the highlight scope from attribute selectors to class selectors
(
[data-flowchat-highlight-x]::highlight(…)→.obf-hl-x::highlight(…)).MarkedText::collectForHighlights6 → 2 samples,
matchRecursively13 → 7. (So attribute selectors falling into the universal bucket is real —worth changing in your implementation too.)
collectMatchingRulesForListSlowstill shows up in 12/10 stack samples.rightmost compound is
::highlight(name). So every pseudo-style resolution linearly scans the author stylesheet(~2.5 MB / ~14.5k rules here). No selector-level scoping can avoid that.
real DOM (
<span class="…">/<mark>) instead of using the CSS Custom Highlight API.