LibWeb: Limit sibling style invalidation by max distance

If an element is affected only by selectors using the direct sibling
combinator `+`, we can calculate the maximum invalidation distance and
use it to limit style invalidation. For example, the selector
`.a + .b + .c` has a maximum invalidation distance of 2, meaning we can
skip invalidating any element affected by this selector if it's more
than two siblings away from the element that triggered the style
invalidation.

This change results in visible performance improvement when hovering
PR list on GitHub.
This commit is contained in:
Aliaksandr Kalenik 2025-03-10 16:16:08 +01:00 committed by Alexander Kalenik
commit 84ecaaa75c
Notes: github-actions[bot] 2025-03-10 17:57:49 +00:00
6 changed files with 61 additions and 10 deletions

View file

@ -765,6 +765,36 @@ Optional<Selector::SimpleSelector> Selector::SimpleSelector::absolutized(Selecto
VERIFY_NOT_REACHED();
}
size_t Selector::sibling_invalidation_distance() const
{
if (m_sibling_invalidation_distance.has_value())
return *m_sibling_invalidation_distance;
m_sibling_invalidation_distance = 0;
size_t current_distance = 0;
for (auto const& compound_selector : compound_selectors()) {
if (compound_selector.combinator == Combinator::None)
continue;
if (compound_selector.combinator == Combinator::SubsequentSibling) {
m_sibling_invalidation_distance = NumericLimits<size_t>::max();
return *m_sibling_invalidation_distance;
}
if (compound_selector.combinator == Combinator::NextSibling) {
current_distance++;
} else {
m_sibling_invalidation_distance = max(*m_sibling_invalidation_distance, current_distance);
current_distance = 0;
}
}
if (current_distance > 0) {
m_sibling_invalidation_distance = max(*m_sibling_invalidation_distance, current_distance);
}
return *m_sibling_invalidation_distance;
}
SelectorList adapt_nested_relative_selector_list(SelectorList const& selectors)
{
// "Nested style rules differ from non-nested rules in the following ways:

View file

@ -271,12 +271,15 @@ public:
bool can_use_fast_matches() const { return m_can_use_fast_matches; }
bool can_use_ancestor_filter() const { return m_can_use_ancestor_filter; }
size_t sibling_invalidation_distance() const;
private:
explicit Selector(Vector<CompoundSelector>&&);
Vector<CompoundSelector> m_compound_selectors;
mutable Optional<u32> m_specificity;
Optional<Selector::PseudoElement> m_pseudo_element;
mutable Optional<size_t> m_sibling_invalidation_distance;
bool m_can_use_fast_matches { false };
bool m_can_use_ancestor_filter { false };
bool m_contains_the_nesting_selector { false };

View file

@ -978,7 +978,9 @@ bool matches(CSS::Selector const& selector, int component_list_index, DOM::Eleme
}
case CSS::Selector::Combinator::NextSibling:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_combinator(true);
const_cast<DOM::Element&>(element).set_affected_by_direct_sibling_combinator(true);
auto new_sibling_invalidation_distance = max(selector.sibling_invalidation_distance(), element.sibling_invalidation_distance());
const_cast<DOM::Element&>(element).set_sibling_invalidation_distance(new_sibling_invalidation_distance);
}
VERIFY(component_list_index != 0);
if (auto* sibling = element.previous_element_sibling())
@ -986,7 +988,7 @@ bool matches(CSS::Selector const& selector, int component_list_index, DOM::Eleme
return false;
case CSS::Selector::Combinator::SubsequentSibling:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_combinator(true);
const_cast<DOM::Element&>(element).set_affected_by_indirect_sibling_combinator(true);
}
VERIFY(component_list_index != 0);
for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) {