LibWeb: Use invalidation sets to reduce style recalculation

Implements idea described in
https://docs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8

Invalidation sets are used to reduce the number of elements marked for
style recalculation by collecting metadata from style rules about the
dependencies between properties that could affect an element’s style.

Currently, this optimization is only applied to style invalidation
triggered by class list mutations on an element.
This commit is contained in:
Aliaksandr Kalenik 2025-01-12 18:38:05 +03:00 committed by Andreas Kling
commit c5f2a88f69
Notes: github-actions[bot] 2025-01-19 18:55:55 +00:00
11 changed files with 549 additions and 3 deletions

View file

@ -462,7 +462,7 @@ void Element::run_attribute_change_steps(FlyString const& local_name, Optional<S
attribute_changed(local_name, old_value, value, namespace_);
if (old_value != value) {
invalidate_style_after_attribute_change(local_name);
invalidate_style_after_attribute_change(local_name, old_value, value);
document().bump_dom_tree_version();
}
}
@ -1142,6 +1142,29 @@ bool Element::affected_by_hover() const
return false;
}
bool Element::affected_by_invalidation_property(CSS::InvalidationSet::Property const& property) const
{
switch (property.type) {
case CSS::InvalidationSet::Property::Type::Class:
return m_classes.contains_slow(property.name);
case CSS::InvalidationSet::Property::Type::Id:
return m_id == property.name;
case CSS::InvalidationSet::Property::Type::TagName:
return local_name() == property.name;
case CSS::InvalidationSet::Property::Type::Attribute: {
if (property.name == HTML::AttributeNames::id || property.name == HTML::AttributeNames::class_)
return true;
return has_attribute(property.name);
}
case CSS::InvalidationSet::Property::Type::InvalidateSelf:
return false;
case CSS::InvalidationSet::Property::Type::InvalidateWholeSubtree:
return true;
default:
VERIFY_NOT_REACHED();
}
}
bool Element::has_pseudo_elements() const
{
if (m_pseudo_element_data) {
@ -1948,7 +1971,7 @@ static bool attribute_name_may_affect_selectors(Element const& element, FlyStrin
return element.document().style_computer().has_attribute_selector(attribute_name);
}
void Element::invalidate_style_after_attribute_change(FlyString const& attribute_name)
void Element::invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value)
{
// FIXME: Only invalidate if the attribute can actually affect style.
@ -1970,6 +1993,29 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute
return;
}
if (attribute_name == HTML::AttributeNames::class_) {
Vector<StringView> old_classes;
Vector<StringView> new_classes;
if (old_value.has_value())
old_classes = old_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
if (new_value.has_value())
new_classes = new_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
Vector<CSS::InvalidationSet::Property> changed_properties;
for (auto& old_class : old_classes) {
if (!new_classes.contains_slow(old_class)) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(old_class.bytes()) });
}
}
for (auto& new_class : new_classes) {
if (!old_classes.contains_slow(new_class)) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Class, .name = FlyString::from_utf8_without_validation(new_class.bytes()) });
}
}
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Attribute, .name = HTML::AttributeNames::class_ });
invalidate_style(StyleInvalidationReason::ElementAttributeChange, changed_properties);
return;
}
if (is_presentational_hint(attribute_name)
|| attribute_name_may_affect_selectors(*this, attribute_name)) {
invalidate_style(StyleInvalidationReason::ElementAttributeChange);

View file

@ -263,6 +263,7 @@ public:
static GC::Ptr<Layout::NodeWithStyle> create_layout_node_for_display_type(DOM::Document&, CSS::Display const&, GC::Ref<CSS::ComputedProperties>, Element*);
bool affected_by_hover() const;
bool affected_by_invalidation_property(CSS::InvalidationSet::Property const&) const;
void set_pseudo_element_node(Badge<Layout::TreeBuilder>, CSS::Selector::PseudoElement::Type, GC::Ptr<Layout::NodeWithStyle>);
GC::Ptr<Layout::NodeWithStyle> get_pseudo_element_node(CSS::Selector::PseudoElement::Type) const;
@ -420,7 +421,7 @@ protected:
private:
void make_html_uppercased_qualified_name();
void invalidate_style_after_attribute_change(FlyString const& attribute_name);
void invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value);
WebIDL::ExceptionOr<GC::Ptr<Node>> insert_adjacent(StringView where, GC::Ref<Node> node);

View file

@ -470,6 +470,64 @@ void Node::invalidate_style(StyleInvalidationReason reason)
document().schedule_style_update();
}
void Node::invalidate_style(StyleInvalidationReason reason, Vector<CSS::InvalidationSet::Property> const& properties)
{
if (is_character_data())
return;
bool properties_used_in_has_selectors = false;
for (auto const& property : properties) {
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;
}
auto invalidation_set = document().style_computer().invalidation_set_for_properties(properties);
if (invalidation_set.is_empty())
return;
if (invalidation_set.needs_invalidate_self()) {
set_needs_style_update(true);
}
auto element_has_properties_from_invalidation_set = [&](Element& element) {
bool result = false;
invalidation_set.for_each_property([&](auto const& property) {
if (element.affected_by_invalidation_property(property))
result = true;
});
return result;
};
auto invalidate_entire_subtree = [&](Node& subtree_root) {
subtree_root.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 = invalidation_set.needs_invalidate_whole_subtree() || element_has_properties_from_invalidation_set(element);
if (needs_style_recalculation) {
element.set_needs_style_update(true);
} else {
element.set_needs_inherited_style_update(true);
}
return TraversalDecision::Continue;
});
};
invalidate_entire_subtree(*this);
if (invalidation_set.needs_invalidate_whole_subtree()) {
for (auto* sibling = next_sibling(); sibling; sibling = sibling->next_sibling()) {
if (sibling->is_element())
invalidate_entire_subtree(*sibling);
}
}
document().schedule_style_update();
}
String Node::child_text_content() const
{
if (!is<ParentNode>(*this))

View file

@ -14,6 +14,7 @@
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/DOM/AccessibilityTreeNode.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/Slottable.h>
@ -297,6 +298,7 @@ public:
void set_child_needs_style_update(bool b) { m_child_needs_style_update = b; }
void invalidate_style(StyleInvalidationReason);
void invalidate_style(StyleInvalidationReason, Vector<CSS::InvalidationSet::Property> const&);
void set_document(Badge<Document>, Document&);