LibWeb: Invalidate less elements affected by CSS custom properties

Before this change, whenever element's attributes changed, we would add
a flag to "pending invalidation", indicating that all descendants whose
style uses CSS custom properties needed to be recomputed. This resulted
in severe overinvalidation, because we would run invalidation regardless
of whether any custom property on affected element actually changed.

This change takes another approach, and now we decide whether
descendant's style needs to be recomputed based on whether ancestor's
style recomputation results in a change of custom properties, though
this approach adds a little overhead to style computation as now we have
to compare old vs new hashmap of custom properties.

This brings substantial improvement on discord and x.com where, before
this change, advantage of using invalidation sets was lost and we had
to recompute all descendants, because almost all of them use custom
properties.
This commit is contained in:
Aliaksandr Kalenik 2025-07-27 19:41:21 +02:00 committed by Andreas Kling
commit d1fbb7b51e
Notes: github-actions[bot] 2025-07-30 09:07:23 +00:00
13 changed files with 71 additions and 67 deletions

View file

@ -4339,8 +4339,10 @@ NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(Parsin
NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(DOM::AbstractElement& element, GuardedSubstitutionContexts& guarded_contexts, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved)
{
// AD-HOC: Report that we might rely on custom properties.
// FIXME: This over-invalidates. Find a way of invalidating only when we need to - specifically, when var() is used.
element.element().set_style_uses_css_custom_properties(true);
if (unresolved.includes_attr_function())
element.element().set_style_uses_attr_css_function();
if (unresolved.includes_var_function())
element.element().set_style_uses_var_css_function();
// To replace substitution functions in a property prop:
auto const& property_name = property.visit(

View file

@ -2454,17 +2454,17 @@ GC::Ref<ComputedProperties> StyleComputer::create_document_style() const
return style;
}
GC::Ref<ComputedProperties> StyleComputer::compute_style(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element) const
GC::Ref<ComputedProperties> StyleComputer::compute_style(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element, Optional<bool&> did_change_custom_properties) const
{
return *compute_style_impl(element, move(pseudo_element), ComputeStyleMode::Normal);
return *compute_style_impl(element, move(pseudo_element), ComputeStyleMode::Normal, did_change_custom_properties);
}
GC::Ptr<ComputedProperties> StyleComputer::compute_pseudo_element_style_if_needed(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element) const
GC::Ptr<ComputedProperties> StyleComputer::compute_pseudo_element_style_if_needed(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element, Optional<bool&> did_change_custom_properties) const
{
return compute_style_impl(element, move(pseudo_element), ComputeStyleMode::CreatePseudoElementStyleIfNeeded);
return compute_style_impl(element, move(pseudo_element), ComputeStyleMode::CreatePseudoElementStyleIfNeeded, did_change_custom_properties);
}
GC::Ptr<ComputedProperties> StyleComputer::compute_style_impl(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element, ComputeStyleMode mode) const
GC::Ptr<ComputedProperties> StyleComputer::compute_style_impl(DOM::Element& element, Optional<CSS::PseudoElement> pseudo_element, ComputeStyleMode mode, Optional<bool&> did_change_custom_properties) const
{
build_rule_cache_if_needed();
@ -2488,6 +2488,9 @@ GC::Ptr<ComputedProperties> StyleComputer::compute_style_impl(DOM::Element& elem
PseudoClassBitmap attempted_pseudo_class_matches;
auto matching_rule_set = build_matching_rule_set(element, pseudo_element, attempted_pseudo_class_matches, did_match_any_pseudo_element_rules, mode);
DOM::AbstractElement abstract_element { element, pseudo_element };
auto old_custom_properties = abstract_element.custom_properties();
// Resolve all the CSS custom properties ("variables") for this element:
// FIXME: Also resolve !important custom properties, in a second cascade.
if (!pseudo_element.has_value() || pseudo_element_supports_property(*pseudo_element, PropertyID::Custom)) {
@ -2533,6 +2536,11 @@ GC::Ptr<ComputedProperties> StyleComputer::compute_style_impl(DOM::Element& elem
auto computed_properties = compute_properties(element, pseudo_element, cascaded_properties);
computed_properties->set_attempted_pseudo_class_matches(attempted_pseudo_class_matches);
if (did_change_custom_properties.has_value() && abstract_element.custom_properties() != old_custom_properties) {
*did_change_custom_properties = true;
}
return computed_properties;
}

View file

@ -145,8 +145,8 @@ public:
[[nodiscard]] GC::Ref<ComputedProperties> create_document_style() const;
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::Element&, Optional<CSS::PseudoElement> = {}) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::Element&, Optional<CSS::PseudoElement>) const;
[[nodiscard]] GC::Ref<ComputedProperties> compute_style(DOM::Element&, Optional<CSS::PseudoElement> = {}, Optional<bool&> did_change_custom_properties = {}) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_pseudo_element_style_if_needed(DOM::Element&, Optional<CSS::PseudoElement>, Optional<bool&> did_change_custom_properties) const;
[[nodiscard]] RuleCache const& get_pseudo_class_rule_cache(PseudoClass) const;
@ -217,7 +217,7 @@ private:
[[nodiscard]] MatchingRuleSet build_matching_rule_set(DOM::Element const&, Optional<PseudoElement>, PseudoClassBitmap& attempted_pseudo_class_matches, bool& did_match_any_pseudo_element_rules, ComputeStyleMode) const;
LogicalAliasMappingContext compute_logical_alias_mapping_context(DOM::Element&, Optional<CSS::PseudoElement>, ComputeStyleMode, MatchingRuleSet const&) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_style_impl(DOM::Element&, Optional<CSS::PseudoElement>, ComputeStyleMode) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_style_impl(DOM::Element&, Optional<CSS::PseudoElement>, ComputeStyleMode, Optional<bool&> did_change_custom_properties) const;
[[nodiscard]] GC::Ref<CascadedProperties> compute_cascaded_values(DOM::Element&, Optional<CSS::PseudoElement>, bool did_match_any_pseudo_element_rules, ComputeStyleMode, MatchingRuleSet const&, Optional<LogicalAliasMappingContext>, ReadonlySpan<PropertyID> properties_to_cascade) const;
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);

View file

@ -11,4 +11,11 @@ namespace Web::CSS {
StyleProperty::~StyleProperty() = default;
bool StyleProperty::operator==(StyleProperty const& other) const
{
if (important != other.important || property_id != other.property_id || custom_name != other.custom_name)
return false;
return value->equals(*other.value);
}
}

View file

@ -23,6 +23,8 @@ struct StyleProperty {
CSS::PropertyID property_id;
NonnullRefPtr<CSSStyleValue const> value;
FlyString custom_name {};
bool operator==(StyleProperty const& other) const;
};
}

View file

@ -25,6 +25,8 @@ public:
Vector<Parser::ComponentValue> const& values() const { return m_values; }
bool contains_arbitrary_substitution_function() const { return m_substitution_functions_presence.has_any(); }
bool includes_attr_function() const { return m_substitution_functions_presence.attr; }
bool includes_var_function() const { return m_substitution_functions_presence.var; }
virtual bool equals(CSSStyleValue const& other) const override;