LibWeb: Avoid full tree traversal for non-subject :has() invalidation

Instead of checking all elements in a document for containment in
`:has()` invalidation set, we could narrow this down to ancestors and
ancestor siblings, like we already do for subject `:has()` invalidation.

This change brings great improvement on GitHub that has selectors with
non-subject `:has()` and sibling combinators (e.g., `.a:has(.b) ~ .c`)
which prior to this change meant style invalidation for whole document.
This commit is contained in:
Aliaksandr Kalenik 2025-02-12 16:33:24 +01:00 committed by Andreas Kling
commit 327dc8e82a
Notes: github-actions[bot] 2025-02-13 15:25:48 +00:00
6 changed files with 42 additions and 54 deletions

View file

@ -508,8 +508,12 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
// :has() cannot be nested in a :has()
if (selector_kind == SelectorKind::Relative)
return false;
if (context.collect_per_element_selector_involvement_metadata && &element == context.subject) {
const_cast<DOM::Element&>(element).set_affected_by_has_pseudo_class_in_subject_position(true);
if (context.collect_per_element_selector_involvement_metadata) {
if (&element == context.subject) {
const_cast<DOM::Element&>(element).set_affected_by_has_pseudo_class_in_subject_position(true);
} else {
const_cast<DOM::Element&>(element).set_affected_by_has_pseudo_class_in_non_subject_position(true);
}
}
// These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector)
for (auto& selector : pseudo_class.argument_selector_list) {

View file

@ -1679,30 +1679,25 @@ void Document::invalidate_style_of_elements_affected_by_has()
for (auto const& node : m_pending_nodes_for_style_invalidation_due_to_presence_of_has) {
if (node.is_null())
continue;
node->invalidate_ancestors_affected_by_has_in_subject_position();
}
for (auto* ancestor = node.ptr(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
if (!ancestor->is_element())
continue;
auto& element = static_cast<Element&>(*ancestor);
element.invalidate_style_if_affected_by_has();
// Take care of elements that affected by :has() in non-subject position, i.e., ".a:has(.b) > .c".
// Elements affected by :has() in subject position, i.e., ".a:has(.b)", are handled by
// Node::invalidate_style() and should already be marked for style update by now.
Vector<CSS::InvalidationSet::Property, 1> changed_properties;
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Has });
auto invalidation_set = style_computer().invalidation_set_for_properties(changed_properties);
for_each_shadow_including_inclusive_descendant([&](Node& node) {
if (!node.is_element())
return TraversalDecision::Continue;
auto& element = static_cast<Element&>(node);
bool needs_style_recalculation = false;
if (invalidation_set.needs_invalidate_whole_subtree()) {
needs_style_recalculation = true;
} else if (element.includes_properties_from_invalidation_set(invalidation_set)) {
needs_style_recalculation = true;
auto* parent = ancestor->parent_or_shadow_host();
if (!parent)
return;
// If any ancestor's sibling was tested against selectors like ".a:has(+ .b)" or ".a:has(~ .b)"
// its style might be affected by the change in descendant node.
parent->for_each_child_of_type<Element>([&](auto& ancestor_sibling_element) {
if (ancestor_sibling_element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator())
ancestor_sibling_element.invalidate_style_if_affected_by_has();
return IterationDecision::Continue;
});
}
if (needs_style_recalculation)
element.set_needs_style_update(true);
return TraversalDecision::Continue;
});
}
}
void Document::invalidate_style_for_elements_affected_by_hover_change(Node& old_new_hovered_common_ancestor, GC::Ptr<Node> hovered_node)

View file

@ -498,6 +498,8 @@ CSS::RequiredInvalidationAfterStyleChange Element::recompute_style()
VERIFY(parent());
m_affected_by_has_pseudo_class_in_subject_position = false;
m_affected_by_has_pseudo_class_in_non_subject_position = false;
m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = false;
m_affected_by_sibling_combinator = false;
m_affected_by_first_or_last_child_pseudo_class = false;
m_affected_by_nth_child_pseudo_class = false;
@ -1312,6 +1314,16 @@ bool Element::includes_properties_from_invalidation_set(CSS::InvalidationSet con
return includes_any;
}
void Element::invalidate_style_if_affected_by_has()
{
if (affected_by_has_pseudo_class_in_subject_position()) {
set_needs_style_update(true);
}
if (affected_by_has_pseudo_class_in_non_subject_position()) {
invalidate_style(StyleInvalidationReason::Other, { { CSS::InvalidationSet::Property::Type::PseudoClass, CSS::PseudoClass::Has } }, {});
}
}
bool Element::has_pseudo_elements() const
{
if (m_pseudo_element_data) {

View file

@ -428,9 +428,14 @@ public:
bool matches_link_pseudo_class() const;
bool matches_local_link_pseudo_class() const;
void invalidate_style_if_affected_by_has();
bool affected_by_has_pseudo_class_in_subject_position() const { return m_affected_by_has_pseudo_class_in_subject_position; }
void set_affected_by_has_pseudo_class_in_subject_position(bool value) { m_affected_by_has_pseudo_class_in_subject_position = value; }
bool affected_by_has_pseudo_class_in_non_subject_position() const { return m_affected_by_has_pseudo_class_in_non_subject_position; }
void set_affected_by_has_pseudo_class_in_non_subject_position(bool value) { m_affected_by_has_pseudo_class_in_non_subject_position = value; }
bool affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator() const { return m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator; }
void set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(bool value) { m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = value; }
@ -539,6 +544,7 @@ private:
bool m_rendered_in_top_layer : 1 { false };
bool m_style_uses_css_custom_properties { false };
bool m_affected_by_has_pseudo_class_in_subject_position : 1 { false };
bool m_affected_by_has_pseudo_class_in_non_subject_position : 1 { false };
bool m_affected_by_sibling_combinator : 1 { false };
bool m_affected_by_first_or_last_child_pseudo_class : 1 { false };
bool m_affected_by_nth_child_pseudo_class : 1 { false };

View file

@ -397,34 +397,6 @@ GC::Ptr<HTML::Navigable> Node::navigable() const
}
}
void Node::invalidate_ancestors_affected_by_has_in_subject_position()
{
// This call only takes care of invalidating `:has()` in subject position (e.g., ".a:has(.b)").
// Non-subject position (e.g., ".a:has(.b) + .c") is still handled by another call that uses
// invalidation sets and requires whole document traversal.
// Here we assume that :has() invalidation scope is limited to ancestors and sibling ancestors.
for (auto* ancestor = this; ancestor; ancestor = ancestor->parent_or_shadow_host()) {
if (!ancestor->is_element())
continue;
auto& element = static_cast<Element&>(*ancestor);
if (element.affected_by_has_pseudo_class_in_subject_position()) {
element.set_needs_style_update(true);
}
auto* parent = ancestor->parent_or_shadow_host();
if (!parent)
return;
// If any ancestor's sibling was tested against selectors like ".a:has(+ .b)" or ".a:has(~ .b)"
// its style might be affected by the change in descendant node.
parent->for_each_child_of_type<Element>([&](auto& element) {
if (element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator())
element.set_needs_style_update(true);
return IterationDecision::Continue;
});
}
}
void Node::invalidate_style(StyleInvalidationReason reason)
{
if (is_character_data())
@ -436,7 +408,7 @@ void Node::invalidate_style(StyleInvalidationReason reason)
document().schedule_ancestors_style_invalidation_due_to_presence_of_has(*parent);
parent->for_each_child_of_type<Element>([&](auto& element) {
if (element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator())
element.set_needs_style_update(true);
element.invalidate_style_if_affected_by_has();
return IterationDecision::Continue;
});
}

View file

@ -309,7 +309,6 @@ public:
[[nodiscard]] bool entire_subtree_needs_style_update() const { return m_entire_subtree_needs_style_update; }
void set_entire_subtree_needs_style_update(bool b) { m_entire_subtree_needs_style_update = b; }
void invalidate_ancestors_affected_by_has_in_subject_position();
void invalidate_style(StyleInvalidationReason);
struct StyleInvalidationOptions {
bool invalidate_self { false };