LibWeb: Reduce over-invalidation for :has() selectors
Feature-filter :not() arguments in :has() when the same compound also has a concrete tag, id, class, or attribute selector. Bare negations still stay conservative, but anchored negations no longer make unrelated subtree mutations walk every :has() candidate. Also avoid installing the whole-subtree :has() fallback for rightmost complex :is()/:where() arguments that only use descendant or child combinators. Keep the fallback for non-rightmost and sibling-combinator cases where the existing plan cannot represent the nested selector context. Add counter-based style invalidation coverage for both cases.
This commit is contained in:
parent
4c87227078
commit
baaaea6cf1
6 changed files with 108 additions and 14 deletions
|
|
@ -138,6 +138,22 @@ static bool selector_may_match_mutation_features(Selector const& selector, Pendi
|
|||
};
|
||||
|
||||
for (auto const& compound_selector : selector.compound_selectors()) {
|
||||
bool compound_has_positive_concrete_feature = false;
|
||||
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
||||
switch (simple_selector.type) {
|
||||
case Selector::SimpleSelector::Type::TagName:
|
||||
case Selector::SimpleSelector::Type::Id:
|
||||
case Selector::SimpleSelector::Type::Class:
|
||||
case Selector::SimpleSelector::Type::Attribute:
|
||||
compound_has_positive_concrete_feature = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (compound_has_positive_concrete_feature)
|
||||
break;
|
||||
}
|
||||
|
||||
if ((compound_selector.combinator == Selector::Combinator::NextSibling
|
||||
|| compound_selector.combinator == Selector::Combinator::SubsequentSibling)
|
||||
&& mutation_features.may_affect_sibling_relationships)
|
||||
|
|
@ -228,6 +244,15 @@ static bool selector_may_match_mutation_features(Selector const& selector, Pendi
|
|||
|| mutation_features.pseudo_classes.contains(pseudo_class.type);
|
||||
break;
|
||||
case PseudoClass::Not:
|
||||
// A bare negation can match because any unrelated node exists, but a negation
|
||||
// attached to a positive concrete feature only changes when either side changes.
|
||||
if (!compound_has_positive_concrete_feature) {
|
||||
must_be_conservative = true;
|
||||
break;
|
||||
}
|
||||
saw_concrete_feature = true;
|
||||
concrete_feature_found_in_mutation_subtree |= visit_selector_list(pseudo_class.argument_selector_list);
|
||||
break;
|
||||
case PseudoClass::Has:
|
||||
default:
|
||||
must_be_conservative = true;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@
|
|||
|
||||
namespace Web::CSS {
|
||||
|
||||
enum class SimpleSelectorGroupPosition {
|
||||
Rightmost,
|
||||
NonRightmost,
|
||||
};
|
||||
|
||||
static void append_or_merge_descendant_rule(Vector<DescendantInvalidationRule>& rules, DescendantInvalidationRule const& rule)
|
||||
{
|
||||
for (auto& existing_rule : rules) {
|
||||
|
|
@ -376,6 +381,17 @@ static bool selector_contains_featureless_subtree_sensitive_selector(Selector co
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool selector_contains_sibling_combinator(Selector const& selector)
|
||||
{
|
||||
for (auto const& compound_selector : selector.compound_selectors()) {
|
||||
if (compound_selector.combinator == Selector::Combinator::NextSibling
|
||||
|| compound_selector.combinator == Selector::Combinator::SubsequentSibling) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class, InvalidationPlan const& root_invalidation_plan);
|
||||
|
||||
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data);
|
||||
|
|
@ -386,7 +402,7 @@ static bool should_register_invalidation_property(InvalidationSet::Property cons
|
|||
}
|
||||
|
||||
static void collect_guard_properties_for_simple_selector(Selector::SimpleSelector const&, InvalidationSet&);
|
||||
static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSelector const&, InvalidationSet&, ExcludePropertiesNestedInNotPseudoClass, StyleInvalidationData&, InsideNthChildPseudoClass, InvalidationPlan const&);
|
||||
static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSelector const&, InvalidationSet&, ExcludePropertiesNestedInNotPseudoClass, StyleInvalidationData&, InsideNthChildPseudoClass, SimpleSelectorGroupPosition, InvalidationPlan const&);
|
||||
|
||||
static Optional<InvalidationSet> build_invalidation_guard_property_set_for_selector_subject(Selector const& selector)
|
||||
{
|
||||
|
|
@ -447,11 +463,11 @@ static InvalidationGuard build_invalidation_guard_for_simple_selectors(Vector<Se
|
|||
return guard;
|
||||
}
|
||||
|
||||
static InvalidationSet build_invalidation_set_for_simple_selectors(Vector<Selector::SimpleSelector const&> const& simple_selectors, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_pseudo_class, InvalidationPlan const& root_invalidation_plan)
|
||||
static InvalidationSet build_invalidation_set_for_simple_selectors(Vector<Selector::SimpleSelector const&> const& simple_selectors, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_pseudo_class, SimpleSelectorGroupPosition simple_selector_group_position, InvalidationPlan const& root_invalidation_plan)
|
||||
{
|
||||
InvalidationSet invalidation_set;
|
||||
for (auto const& simple_selector : simple_selectors)
|
||||
build_invalidation_sets_for_simple_selector_impl(simple_selector, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_pseudo_class, root_invalidation_plan);
|
||||
build_invalidation_sets_for_simple_selector_impl(simple_selector, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_pseudo_class, simple_selector_group_position, root_invalidation_plan);
|
||||
return invalidation_set;
|
||||
}
|
||||
|
||||
|
|
@ -536,7 +552,7 @@ static NonnullRefPtr<InvalidationPlan> build_invalidation_for_combinator(Selecto
|
|||
return invalidation;
|
||||
}
|
||||
|
||||
static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_selector, InvalidationPlan const& root_invalidation_plan)
|
||||
static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_selector, SimpleSelectorGroupPosition simple_selector_group_position, InvalidationPlan const& root_invalidation_plan)
|
||||
{
|
||||
switch (selector.type) {
|
||||
case Selector::SimpleSelector::Type::Class:
|
||||
|
|
@ -612,13 +628,15 @@ static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSel
|
|||
// non-rightmost positions (e.g., :is(:has(.x) .y)) is not propagated. We need it in the
|
||||
// outer invalidation set so outer compounds register plans for pseudo_class:Has that
|
||||
// account for the full selector context.
|
||||
// Additionally, when :has() is inside a complex :is()/:where() argument (multiple
|
||||
// compounds), the outer invalidation plan can't correctly capture the nested combinator
|
||||
// structure — e.g., sibling combinators at the outer level would be applied at the wrong
|
||||
// DOM level. Fall back to whole-subtree invalidation for :has() in these cases.
|
||||
// Additionally, when :has() is inside a complex :is()/:where() argument in a
|
||||
// non-rightmost compound, or in one that contains sibling combinators, the outer
|
||||
// invalidation plan can't correctly capture the nested combinator structure. Fall back
|
||||
// to whole-subtree invalidation for :has() in these cases. Descendant and child
|
||||
// combinators in the rightmost compound are represented by the nested selector's own
|
||||
// invalidation plan above.
|
||||
if (nested_selector->contains_pseudo_class(PseudoClass::Has)) {
|
||||
invalidation_set.set_needs_invalidate_pseudo_class(PseudoClass::Has);
|
||||
if (nested_selector->compound_selectors().size() > 1) {
|
||||
if (nested_selector->compound_selectors().size() > 1 && (simple_selector_group_position == SimpleSelectorGroupPosition::NonRightmost || selector_contains_sibling_combinator(*nested_selector))) {
|
||||
InvalidationSet has_only;
|
||||
has_only.set_needs_invalidate_pseudo_class(PseudoClass::Has);
|
||||
add_invalidation_plan_for_properties(style_invalidation_data, has_only, *make_invalidate_whole_subtree_invalidation());
|
||||
|
|
@ -634,7 +652,7 @@ static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSel
|
|||
if (pseudo_element.type() == PseudoElement::Slotted) {
|
||||
for (auto const& compound_selector : pseudo_element.compound_selector().compound_selectors()) {
|
||||
for (auto const& nested_simple : compound_selector.simple_selectors)
|
||||
build_invalidation_sets_for_simple_selector_impl(nested_simple, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_selector, root_invalidation_plan);
|
||||
build_invalidation_sets_for_simple_selector_impl(nested_simple, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_selector, simple_selector_group_position, root_invalidation_plan);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -646,7 +664,7 @@ static void build_invalidation_sets_for_simple_selector_impl(Selector::SimpleSel
|
|||
|
||||
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)
|
||||
{
|
||||
build_invalidation_sets_for_simple_selector_impl(selector, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_selector, *make_invalidate_self_invalidation());
|
||||
build_invalidation_sets_for_simple_selector_impl(selector, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_selector, SimpleSelectorGroupPosition::Rightmost, *make_invalidate_self_invalidation());
|
||||
}
|
||||
|
||||
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data)
|
||||
|
|
@ -662,7 +680,7 @@ static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in
|
|||
if (rightmost)
|
||||
return;
|
||||
|
||||
auto invalidation_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, InsideNthChildPseudoClass::No, *make_invalidate_self_invalidation());
|
||||
auto invalidation_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, InsideNthChildPseudoClass::No, SimpleSelectorGroupPosition::Rightmost, *make_invalidate_self_invalidation());
|
||||
add_invalidation_plan_for_properties(style_invalidation_data, invalidation_set, *make_invalidate_whole_subtree_invalidation());
|
||||
});
|
||||
};
|
||||
|
|
@ -696,8 +714,9 @@ static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidati
|
|||
collect_properties_used_in_has(simple_selector, style_invalidation_data, {});
|
||||
}
|
||||
|
||||
auto invalidation_properties = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, inside_nth_child_pseudo_class, root_invalidation_plan);
|
||||
auto subject_match_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::Yes, style_invalidation_data, inside_nth_child_pseudo_class, root_invalidation_plan);
|
||||
auto simple_selector_group_position = is_rightmost ? SimpleSelectorGroupPosition::Rightmost : SimpleSelectorGroupPosition::NonRightmost;
|
||||
auto invalidation_properties = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, inside_nth_child_pseudo_class, simple_selector_group_position, root_invalidation_plan);
|
||||
auto subject_match_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::Yes, style_invalidation_data, inside_nth_child_pseudo_class, simple_selector_group_position, root_invalidation_plan);
|
||||
auto subject_guard = build_invalidation_guard_for_simple_selectors(simple_selectors);
|
||||
bool subject_matches_any = subject_match_set.is_empty() && simple_selector_group_matches_any(simple_selectors);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
PASS: descendant-only complex :has() keeps later :has() invalidation targeted | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=2, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=1, hasMatchInvocations=1, hasResultCacheHits=0, hasResultCacheMisses=1
|
||||
|
|
@ -6,6 +6,8 @@ PASS: actual mutation root skips unrelated :has anchor: id argument | styleInval
|
|||
PASS: actual mutation root keeps related :has anchor: id argument | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=1, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
|
||||
PASS: actual mutation root skips unrelated :has anchor: tag argument | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=2, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=1, hasResultCacheHits=0, hasResultCacheMisses=1
|
||||
PASS: actual mutation root keeps related :has anchor: tag argument | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
|
||||
PASS: actual mutation root skips unrelated :has anchor: negation argument with positive tag | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=0, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0
|
||||
PASS: actual mutation root keeps related :has anchor: negation argument with positive tag | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
|
||||
PASS: actual mutation root skips unrelated :has anchor: child combinator argument | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=3, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=1, hasMatchInvocations=1, hasResultCacheHits=0, hasResultCacheMisses=1
|
||||
PASS: actual mutation root keeps related :has anchor: child combinator argument | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=1, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
|
||||
PASS: actual mutation root skips unrelated :has anchor: descendant combinator argument | styleInvalidations=3, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=2, hasMatchInvocations=1, hasResultCacheHits=0, hasResultCacheMisses=1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../include.js"></script>
|
||||
<script src="structural-matrix.js"></script>
|
||||
<style>
|
||||
:is(:where(.group):has(.flag) *) {
|
||||
--poison-probe: match;
|
||||
}
|
||||
|
||||
html:has(.toggle) .target {
|
||||
color: rgb(1, 2, 3);
|
||||
}
|
||||
|
||||
.target {
|
||||
color: rgb(4, 5, 6);
|
||||
}
|
||||
</style>
|
||||
<section id="fixture">
|
||||
<div id="toggle"></div>
|
||||
<div class="target"></div>
|
||||
</section>
|
||||
<script>
|
||||
test(() => {
|
||||
const fixture = document.getElementById("fixture");
|
||||
for (let i = 0; i < 80; ++i)
|
||||
fixture.appendChild(makeElement("span", { className: "filler" }));
|
||||
|
||||
const target = document.querySelector(".target");
|
||||
internals.updateStyle();
|
||||
assertEqual("target initially has base color", getComputedStyle(target).color, "rgb(4, 5, 6)");
|
||||
|
||||
resetStyleCounters();
|
||||
document.getElementById("toggle").classList.add("toggle");
|
||||
internals.updateStyle();
|
||||
|
||||
assertEqual("target matches after :has() mutation", getComputedStyle(target).color, "rgb(1, 2, 3)");
|
||||
const counters = styleCounters();
|
||||
assertEqual("descendant-only complex :has() does not force whole-subtree no-op recomputes", counters.elementStyleNoopRecomputations < 10, true);
|
||||
printPassWithCounters("descendant-only complex :has() keeps later :has() invalidation targeted");
|
||||
});
|
||||
</script>
|
||||
|
|
@ -57,6 +57,12 @@
|
|||
({ fixture }) => fixture.appendChild(makeElement("strong")),
|
||||
({ subgrid }) => subgrid.appendChild(makeElement("em"))
|
||||
);
|
||||
runMutationRootCase(
|
||||
"negation argument with positive tag",
|
||||
`div:not([role="group"])`,
|
||||
({ subgrid }) => subgrid.classList.add("unrelated"),
|
||||
({ subgrid }) => subgrid.appendChild(makeElement("div"))
|
||||
);
|
||||
runMutationRootCase(
|
||||
"child combinator argument",
|
||||
`> .needle`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue