From 0ab61a94d76b8e257c3823b95d8db4ca8b574122 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 20 Feb 2025 21:53:31 +0100 Subject: [PATCH] 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. --- Libraries/LibWeb/CSS/StyleComputer.cpp | 21 +++++++++++---------- Libraries/LibWeb/CSS/StyleComputer.h | 6 +++--- Libraries/LibWeb/DOM/Document.cpp | 24 ++++++++++++++---------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 633e7abb077..955f79e67eb 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -420,10 +420,10 @@ RuleCache const* StyleComputer::rule_cache_for_cascade_origin(CascadeOrigin casc return true; } -Vector 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 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& hover_rules) +void StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_origin, SelectorInsights& insights) { Vector 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(); + 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 compute_style(DOM::Element&, Optional = {}) const; [[nodiscard]] GC::Ptr compute_pseudo_element_style_if_needed(DOM::Element&, Optional) const; - Vector const& get_hover_rules() const; + RuleCache const& get_hover_rules() const; [[nodiscard]] Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const; InvalidationSet invalidation_set_for_properties(Vector const&) const; @@ -284,14 +284,14 @@ private: HashMap, NonnullOwnPtr> for_shadow_roots; }; - void make_rule_cache_for_cascade_origin(CascadeOrigin, SelectorInsights&, Vector& 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) const; static void collect_selector_insights(Selector const&, SelectorInsights&); OwnPtr m_selector_insights; - Vector m_hover_rules; + OwnPtr m_hover_rule_cache; OwnPtr m_style_invalidation_data; OwnPtr m_author_rule_cache; OwnPtr m_user_rule_cache; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 29808713d6b..99c934a09b3 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -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 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(root) ? static_cast(&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 invalidate_hovered_elements_recursively = [&](Node& node) -> void {