style_tree のノード走査は 1 ページあたり約 600 tick(約
6 秒)かかる。 内訳を測ったところ:
index_build=27tick walk=605tick nodes=358 rules=2403
candidates_total=17396 (索引なしなら 860274)
セレクタ照合は索引で 2.0%(49 分の 1)まで落ちており、 照合はもうボトルネックではない。時間は照合ループの外にある。
原因が判明した。cascade.rs はノード 1
個につきカスケードを 4 回走らせている:
| 回 | 目的 | 呼び出し |
|---|---|---|
| 1 | 通常の算出値 | compute_styles_with_vars(..., &[]) |
| 2 | :hover 時の差分 |
compute_styles_with_vars(..., &["hover"]) |
| 3 | :focus 時の差分 |
compute_styles_with_vars(..., &["focus"]) |
| 4 | :active 時の差分 |
compute_styles_with_vars(..., &["active"]) |
hover_values / focus_values /
active_values を事前計算しておく方式のため
(マウス移動のたびに再カスケードしないで済むという利点はある)、
4 倍の固定コストが全ノードにかかっている。
実サイトでは :hover 等を持つルールはごく一部で、
それに該当し得る要素はさらに少ない。
そのノードの候補ルールが該当の状態を一切参照していなければ、 その状態のパスは走らせない。
PseudoClass::Other(s) の s
が対象の状態名と一致するか。 :hover / :focus /
:active はいずれもこの表現で保持される
(types.rs の PseudoClass
に専用のバリアントは無い)。
以下の入れ子も再帰的に見る必要がある:
Selector::Chain の全ステップ
(a:hover span のように祖先側に付く場合がある)PseudoClass::Not(inner)PseudoClass::Is(list) /
PseudoClass::Where(list)PseudoClass::NthChild(_, _, Some(of)) /
NthLastChild の of SPseudoClass::Has(...) の各枝:focus-within / :focus-visible は
Other に別名として入るため、 focus
の判定では前方一致ではなく完全一致にしないと
取りこぼしと過剰検出の両方が起きる。 selector.rs:1238 が
active_pseudo_states.contains(&"focus") を
:focus-within
の判定に使っているため、focus-within /
focus-visible を見つけたら focus
を参照しているとみなす(D-4 に従い安全側)。
css/pseudo_state.rs(純粋・no_std・グローバル状態なし)
pub fn simple_uses_state(s: &SimpleSelector, state: &str) -> bool;
pub fn selector_uses_state(sel: &Selector, state: &str) -> bool;
pub fn rule_uses_state(rule: &Rule, state: &str) -> bool; walk candidates_total
修正前 605 tick 17396
修正後 291 tick 6022
:hover / :focus /
:active のパスが不要だったことの裏付けmake test 720 件パス(test_pseudo_state 23
件を新規追加)。
同一条件でも walk が 291 / 394 / 748 tick とばらつく。 協調スケジューラのため、wall time には他スレッド (TLS ダウンロード)に食われた分が含まれる。 最小値がその時点での実力に最も近い。