LibWeb: Bucket hover rules using RuleCache

Analysis of selectors on modern websites shows that the `:hover`
pseudo-class is mostly used in the subject position within relatively
simple selectors like `.a:hover`. This suggests that we could greatly
benefit from segregating them by id/class/tag name, this way reducing
number of selectors tested during hover style invalidation.

With this change, hover invalidation on Discord goes down from 70ms to
3ms on my machine. I also tested GMail and GitHub where this change
shows nice 2x-3x speedup.
This commit is contained in:
Aliaksandr Kalenik 2025-02-20 21:53:31 +01:00 committed by Alexander Kalenik
parent ff8826d582
commit 0ab61a94d7
Notes: github-actions[bot] 2025-02-22 09:15:47 +00:00
3 changed files with 28 additions and 23 deletions

View file

@ -420,10 +420,10 @@ RuleCache const* StyleComputer::rule_cache_for_cascade_origin(CascadeOrigin casc
return true;
}
Vector<MatchingRule> const& StyleComputer::get_hover_rules() const
RuleCache const& StyleComputer::get_hover_rules() const
{
build_rule_cache_if_needed();
return m_hover_rules;
return *m_hover_rule_cache;
}
InvalidationSet StyleComputer::invalidation_set_for_properties(Vector<InvalidationSet::Property> const& properties) const
@ -2637,7 +2637,7 @@ void StyleComputer::collect_selector_insights(Selector const& selector, Selector
}
}
void StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights, Vector<MatchingRule>& hover_rules)
void StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights)
{
Vector<MatchingRule> matching_rules;
size_t style_sheet_index = 0;
@ -2734,9 +2734,9 @@ void StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_ori
}
if (selector.contains_hover_pseudo_class()) {
hover_rules.append(matching_rule);
// For hover rule cache we intentionally pass pseudo_element as None, because we don't want to bucket hover rules by pseudo element type
m_hover_rule_cache->add_rule(matching_rule, {}, contains_root_pseudo_class);
}
rule_cache.add_rule(matching_rule, pseudo_element, contains_root_pseudo_class);
}
++rule_index;
@ -2864,9 +2864,10 @@ void StyleComputer::build_rule_cache()
build_qualified_layer_names_cache();
make_rule_cache_for_cascade_origin(CascadeOrigin::Author, *m_selector_insights, m_hover_rules);
make_rule_cache_for_cascade_origin(CascadeOrigin::User, *m_selector_insights, m_hover_rules);
make_rule_cache_for_cascade_origin(CascadeOrigin::UserAgent, *m_selector_insights, m_hover_rules);
m_hover_rule_cache = make<RuleCache>();
make_rule_cache_for_cascade_origin(CascadeOrigin::Author, *m_selector_insights);
make_rule_cache_for_cascade_origin(CascadeOrigin::User, *m_selector_insights);
make_rule_cache_for_cascade_origin(CascadeOrigin::UserAgent, *m_selector_insights);
}
void StyleComputer::invalidate_rule_cache()
@ -2883,7 +2884,7 @@ void StyleComputer::invalidate_rule_cache()
// If we are sure that it's safe, we could keep it as an optimization.
m_user_agent_rule_cache = nullptr;
m_hover_rules.clear_with_capacity();
m_hover_rule_cache = nullptr;
m_style_invalidation_data = nullptr;
}
@ -3112,7 +3113,7 @@ void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<Selector::P
}
}
if (matching_rule.contains_pseudo_element) {
if (matching_rule.contains_pseudo_element && pseudo_element.has_value()) {
if (Selector::PseudoElement::is_known_pseudo_element_type(pseudo_element.value())) {
rules_by_pseudo_element[to_underlying(pseudo_element.value())].append(matching_rule);
} else {

View file

@ -161,7 +161,7 @@ public:
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type> = {}) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>) const;
Vector<MatchingRule> const& get_hover_rules() const;
RuleCache const& get_hover_rules() const;
[[nodiscard]] Vector<MatchingRule const*> collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional<CSS::Selector::PseudoElement::Type>, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const;
InvalidationSet invalidation_set_for_properties(Vector<InvalidationSet::Property> const&) const;
@ -284,14 +284,14 @@ private:
HashMap<GC::Ref<DOM::ShadowRoot const>, NonnullOwnPtr<RuleCaches>> for_shadow_roots;
};
void make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&, Vector<MatchingRule>& hover_rules);
void make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&);
[[nodiscard]] RuleCache const* rule_cache_for_cascade_origin(CascadeOrigin, FlyString const& qualified_layer_name, GC::Ptr<DOM::ShadowRoot const>) const;
static void collect_selector_insights(Selector const&, SelectorInsights&);
OwnPtr<SelectorInsights> m_selector_insights;
Vector<MatchingRule> m_hover_rules;
OwnPtr<RuleCache> m_hover_rule_cache;
OwnPtr<StyleInvalidationData> m_style_invalidation_data;
OwnPtr<RuleCachesForDocumentAndShadowRoots> m_author_rule_cache;
OwnPtr<RuleCachesForDocumentAndShadowRoots> m_user_rule_cache;

View file

@ -1726,8 +1726,6 @@ void Document::invalidate_style_of_elements_affected_by_has()
void Document::invalidate_style_for_elements_affected_by_hover_change(Node& old_new_hovered_common_ancestor, GC::Ptr<Node> hovered_node)
{
auto const& hover_rules = style_computer().get_hover_rules();
if (hover_rules.is_empty())
return;
auto& root = old_new_hovered_common_ancestor.root();
auto shadow_root = is<ShadowRoot>(root) ? static_cast<ShadowRoot const*>(&root) : nullptr;
@ -1761,14 +1759,20 @@ void Document::invalidate_style_for_elements_affected_by_hover_change(Node& old_
};
auto matches_different_set_of_hover_rules_after_hovered_element_change = [&](Element const& element) {
for (auto const& rule : hover_rules) {
bool before = does_rule_match_on_element(element, rule);
TemporaryChange change { m_hovered_node, hovered_node };
bool after = does_rule_match_on_element(element, rule);
if (before != after)
return true;
}
return false;
bool result = false;
hover_rules.for_each_matching_rules(element, {}, [&](auto& rules) {
for (auto& rule : rules) {
bool before = does_rule_match_on_element(element, rule);
TemporaryChange change { m_hovered_node, hovered_node };
bool after = does_rule_match_on_element(element, rule);
if (before != after) {
result = true;
return IterationDecision::Break;
}
}
return IterationDecision::Continue;
});
return result;
};
Function<void(Node&)> invalidate_hovered_elements_recursively = [&](Node& node) -> void {