Skip to content

perf(web-ui): FlowChat 的 ::selection/::highlight() 规则使 WebKit 走慢速 CSS 匹配,渲染主线程持续满载 #3178

Description

@guhan33551

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) { … }

问题点:

  1. X ::selection(带后代组合符)无法被 WebKit 的规则分桶优化,只能落到 collectMatchingRulesForListSlow。该样式表总规模约 2.5 MB / ≈14 482 条规则(含 778 处 :is()/:where()/:not()),于是"文本框数量 × 规则总量"的线性扫描在每次重绘发生。
  2. color-mix() 在这些规则里每解析一次就要重新求值(栈中单独出现在 WebCore::CSS::mix)。
  3. ::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(该副本即实际被加载的前端)。
代价:选中文本回落到系统默认底色,摘录/批注/搜索命中的高亮底色不再显示。

建议修法

  1. 不要对 ::selection / ::highlight() 使用后代组合符选择器A ::selection)。改为在需要定制选区的元素上加一个类,用单个复合选择器(X::selection),或直接避免定制 ::selection
  2. 这些规则里不要用 color-mix():换成预计算的静态颜色或 CSS 变量(var(--x-selection-bg)),避免每次解析都求值。
  3. 把这些规则移出大样式表:慢速匹配的代价与规则总量成正比,放进一个独立的小样式表(或按需注入)可显著降低成本。
  4. 可选:没有选区/高亮时不要保留活跃的 marked range,避免 MarkedText::collectForHighlights 每帧都有活干。

复现步骤

  1. 在 Linux(WebKitGTK)上启动 OpenBitFun 1.0.1,打开一个内容较多的会话;
  2. R=$(pgrep -f WebKitWebProcess | ...) 找到渲染进程,运行
    gdb -p $R -batch -ex "bt 8" 连续采样若干次;
  3. 多数样本会落在 MarkedText::collectForHighlights → getUncachedPseudoStyle → collectMatchingRulesForListSlow
  4. 按上文剥离那 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions