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.
FlowChat 的
::selection/::highlight()规则导致渲染主线程持续满载(Linux / WebKitGTK)环境
open-bit-fun-1.0.1-1.x86_64)webkit2gtk4.1-2.52.5-1.fc44(非 AppImage 内置版)现象
WebKitWebProcess)的主线程长期占用 44% ~ 99% 单核(实测峰值 105% 整体),UI 明显卡顿、输入延迟。ai.log无增长(即没有任何模型调用、没有后台代理在跑)。SkiaGPUWorker正常,无llvmpipe/swrast软光栅。根因(gdb 栈证据)
对渲染进程主线程反复采样,8 次采样中 6 次落在同一条路径:
另有样本落在:
即:每个文本框、每次重绘,WebKit 都要未缓存地解析高亮伪元素样式,并因此对整套样式表做线性(慢速)规则匹配,还会重复求值
color-mix()。触发这些规则的 CSS(
assets/index-Dg5Vb1KW.css33 条、assets/ChatPane-BtnTdl19.css10 条等,共 46 条):问题点:
X ::selection(带后代组合符)无法被 WebKit 的规则分桶优化,只能落到collectMatchingRulesForListSlow。该样式表总规模约 2.5 MB / ≈14 482 条规则(含 778 处:is()/:where()/:not()),于是"文本框数量 × 规则总量"的线性扫描在每次重绘发生。color-mix()在这些规则里每解析一次就要重新求值(栈中单独出现在WebCore::CSS::mix)。::highlight(...)同理:只要应用注册了高亮范围(摘录/批注/搜索命中),每次重绘都会为每个文本框解析其伪元素样式。实验验证(同一条件、8 个栈样本/组)
collectForHighlightsgetUncachedPseudoStylecollectMatchingRulesForListSlowfrontend/dist::selection/::highlight()规则剥离方式:按 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)),避免每次解析都求值。MarkedText::collectForHighlights每帧都有活干。复现步骤
R=$(pgrep -f WebKitWebProcess | ...)找到渲染进程,运行gdb -p $R -batch -ex "bt 8"连续采样若干次;MarkedText::collectForHighlights → getUncachedPseudoStyle → collectMatchingRulesForListSlow;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:
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, andcolor-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(), avoidcolor-mix()in them, and keep them in a small dedicated stylesheet.