Skip to content

Commit

Permalink
LibWeb: Use invalidation sets for :has() invalidation
Browse files Browse the repository at this point in the history
Prior to this change, we invalidated all elements in the document if it
used any selectors with :has(). This change aims to improve that by
applying a combination of techniques:
- Collect metadata for each element if it was matched against a selector
  with :has() in the subject position. This is needed to invalidate all
  elements that could be affected by selectors like `div:has(.a:empty)`
  because they are not covered by the invalidation sets.
- Use invalidation sets to invalidate elements that are affected by
  selectors with :has() in a non-subject position.

Selectors like `.a:has(.b) + .c` still cause whole-document invalidation
because invalidation sets cover only descendants, not siblings. As a
result, there is no performance improvement on github.com due to this
limitation. However, youtube.com and discord.com benefit from this
change.
  • Loading branch information
kalenikaliaksandr authored and awesomekling committed Jan 29, 2025
1 parent e33037a commit d762d16
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Libraries/LibWeb/CSS/SelectorEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,9 @@ 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);
}
// These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector)
for (auto& selector : pseudo_class.argument_selector_list) {
if (matches_has_pseudo_class(selector, element, shadow_host, context))
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/CSS/SelectorEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ enum class SelectorKind {

struct MatchContext {
GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule {};
GC::Ptr<DOM::Element const> subject {};
bool collect_per_element_selector_involvement_metadata { false };
bool did_match_any_hover_rules { false };
};

Expand Down
6 changes: 5 additions & 1 deletion Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,11 @@ Vector<MatchingRule const*> StyleComputer::collect_matching_rules(DOM::Element c

auto const& selector = rule_to_run.selector;

SelectorEngine::MatchContext context { .style_sheet_for_rule = *rule_to_run.sheet };
SelectorEngine::MatchContext context {
.style_sheet_for_rule = *rule_to_run.sheet,
.subject = element,
.collect_per_element_selector_involvement_metadata = true,
};
ScopeGuard guard = [&] {
if (context.did_match_any_hover_rules)
did_match_any_hover_rules = true;
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/CSS/StyleInvalidationData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector
case PseudoClass::Disabled:
case PseudoClass::PlaceholderShown:
case PseudoClass::Checked:
case PseudoClass::Has:
invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class.type);
break;
default:
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,8 @@ bool Element::includes_properties_from_invalidation_set(CSS::InvalidationSet con
}
case CSS::InvalidationSet::Property::Type::PseudoClass: {
switch (property.value.get<CSS::PseudoClass>()) {
case CSS::PseudoClass::Has:
return true;
case CSS::PseudoClass::Enabled: {
return (is<HTML::HTMLButtonElement>(*this) || is<HTML::HTMLInputElement>(*this) || is<HTML::HTMLSelectElement>(*this) || is<HTML::HTMLTextAreaElement>(*this) || is<HTML::HTMLOptGroupElement>(*this) || is<HTML::HTMLOptionElement>(*this) || is<HTML::HTMLFieldSetElement>(*this))
&& !is_actually_disabled();
Expand Down
4 changes: 4 additions & 0 deletions Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ class Element
bool has_style_containment() const;
bool has_paint_containment() const;

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; }

protected:
Element(Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
Expand Down Expand Up @@ -500,6 +503,7 @@ class Element

bool m_in_top_layer { false };
bool m_style_uses_css_custom_properties { false };
bool m_affected_by_has_pseudo_class_in_subject_position { false };

OwnPtr<CSS::CountersSet> m_counters_set;

Expand Down
46 changes: 38 additions & 8 deletions Libraries/LibWeb/DOM/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,47 @@ GC::Ptr<HTML::Navigable> Node::navigable() const
}
}

void Node::invalidate_elements_affected_by_has()
{
Vector<CSS::InvalidationSet::Property, 1> changed_properties;
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Has });
auto invalidation_set = document().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;
// There are two cases in which an element must be invalidated, depending on the position of :has() in a selector:
// 1) In the subject position, i.e., ".a:has(.b)". In that case, invalidation sets are not helpful
// for narrowing down the set of elements that need to be invalidated. Instead, we invalidate
// all elements that were tested against selectors with :has() in the subject position during
// selector matching.
// 2) In the non-subject position, i.e., ".a:has(.b) > .c". Here, invalidation sets can be used to
// determine that only elements with the "c" class have to be invalidated.
if (element.affected_by_has_pseudo_class_in_subject_position()) {
needs_style_recalculation = true;
} else 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;
}

if (needs_style_recalculation) {
element.set_needs_style_update(true);
} else {
element.set_needs_inherited_style_update(true);
}
return TraversalDecision::Continue;
});
}

void Node::invalidate_style(StyleInvalidationReason reason)
{
if (is_character_data())
return;

// FIXME: This is very not optimal! We should figure out a smaller set of elements to invalidate,
// but right now the :has() selector means we have to invalidate everything.
if (!is_document() && document().style_computer().may_have_has_selectors()) {
document().invalidate_style(reason);
return;
if (document().style_computer().may_have_has_selectors()) {
document().invalidate_elements_affected_by_has();
}

if (!needs_style_update() && !document().needs_full_style_update()) {
Expand Down Expand Up @@ -462,7 +493,7 @@ void Node::invalidate_style(StyleInvalidationReason reason)
document().schedule_style_update();
}

void Node::invalidate_style(StyleInvalidationReason reason, Vector<CSS::InvalidationSet::Property> const& properties, StyleInvalidationOptions options)
void Node::invalidate_style(StyleInvalidationReason, Vector<CSS::InvalidationSet::Property> const& properties, StyleInvalidationOptions options)
{
if (is_character_data())
return;
Expand All @@ -472,8 +503,7 @@ void Node::invalidate_style(StyleInvalidationReason reason, Vector<CSS::Invalida
properties_used_in_has_selectors |= document().style_computer().invalidation_property_used_in_has_selector(property);
}
if (properties_used_in_has_selectors) {
document().invalidate_style(reason);
return;
document().invalidate_elements_affected_by_has();
}

auto invalidation_set = document().style_computer().invalidation_set_for_properties(properties);
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/DOM/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ class Node : public EventTarget {
[[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_elements_affected_by_has();
void invalidate_style(StyleInvalidationReason);
struct StyleInvalidationOptions {
bool invalidate_self { false };
Expand Down

0 comments on commit d762d16

Please sign in to comment.