LibWeb: Narrow :has() style invalidation to ancestor nodes

The current implementation of `:has()` style invalidation is divided
into two cases:
- When used in subject position (e.g., `.a:has(.b)`).
- When in a non-subject position (e.g., `.a > .b:has(.c)`).

This change focuses on improving the first case. For non-subject usage,
we still perform a full tree traversal and invalidate all elements
affected by the `:has()` pseudo-class invalidation set.

We already optimize subject `:has()` invalidations by limiting
invalidated elements to ones that were tested against `has()` selectors
during selector matching. However, selectors like `div:has(.a)`
currently cause every div element in the document to be invalidated.
By modifying the invalidation traversal to consider only ancestor nodes
(and, optionally, their siblings), we can drastically reduce the number
of invalidated elements for broad selectors like the example above.

On Discord, when scrolling through message history, this change allows
to reduce number of invalidated elements from ~1k to ~5.
This commit is contained in:
Aliaksandr Kalenik 2025-02-08 19:09:08 +01:00 committed by Andreas Kling
commit e677ab1699
Notes: github-actions[bot] 2025-02-10 00:15:09 +00:00
7 changed files with 104 additions and 22 deletions

View file

@ -107,6 +107,8 @@ enum class InsideNthChildPseudoClass {
static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class);
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data);
static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_selector)
{
switch (selector.type) {
@ -130,7 +132,12 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector
case PseudoClass::Disabled:
case PseudoClass::PlaceholderShown:
case PseudoClass::Checked:
case PseudoClass::Has:
case PseudoClass::Has: {
for (auto const& nested_selector : pseudo_class.argument_selector_list) {
add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(*nested_selector, style_invalidation_data);
}
[[fallthrough]];
}
case PseudoClass::Link:
case PseudoClass::AnyLink:
case PseudoClass::LocalLink:
@ -158,6 +165,47 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector
}
}
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data)
{
// Normally, :has() invalidation scope is limited to ancestors and ancestor siblings, however it could require
// descendants invalidation when :is() with complex selector is used inside :has() relative selector.
// For example ".a:has(:is(.b .c))" requires invalidation whenever "b" class is added or removed.
// To cover this case, we add descendant invalidation set that requires whole subtree invalidation for each
// property used in non-subject part of complex selector.
auto invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector = [&](Selector const& selector) {
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator, bool rightmost) {
if (rightmost) {
return;
}
InvalidationSet invalidation_set;
for (auto const& simple_selector : simple_selectors) {
build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, InsideNthChildPseudoClass::No);
}
invalidation_set.for_each_property([&](auto const& invalidation_property) {
auto& descendant_invalidation_set = style_invalidation_data.descendant_invalidation_sets.ensure(invalidation_property, [] { return InvalidationSet {}; });
descendant_invalidation_set.set_needs_invalidate_whole_subtree();
return IterationDecision::Continue;
});
});
};
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator, bool) {
for (auto const& simple_selector : simple_selectors) {
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
continue;
auto const& pseudo_class = simple_selector.pseudo_class();
if (pseudo_class.type == PseudoClass::Is || pseudo_class.type == PseudoClass::Where || pseudo_class.type == PseudoClass::Not) {
for (auto const& selector : pseudo_class.argument_selector_list) {
invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector(*selector);
}
}
}
});
}
static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class)
{
auto const& compound_selectors = selector.compound_selectors();