From 2f39b2dd637db6f0f7482e2d54b5d7e1d82be5f9 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 30 Apr 2026 23:56:56 +0200 Subject: [PATCH] LibWeb: Split shadow stylesheet host-side invalidation reach Broad shadow-root stylesheet changes already restyle the whole shadow tree, but host-side fallout does not always need a document-wide invalidation. Split the host-side reach classification so selectors contained to the host subtree, such as `:host *` and `:host > *`, invalidate the host, while sibling-escaping selectors such as `:host + :has(*)` still invalidate the host's root. Recognize sibling escapes through positive selector-list pseudos such as `:is()` and `:where()` as well, including selectors like `:is(:host) + :has(*)` and `:is(:host + .item)`. This avoids turning host-contained shadow stylesheet changes into full document style invalidations. On https://pomax.github.io/bezierinfo/, this reduces the time to produce a layout tree from about 8.7s to 3.6s on my machine. --- .../LibWeb/CSS/StyleSheetInvalidation.cpp | 128 +++++++++++++++--- Libraries/LibWeb/CSS/StyleSheetInvalidation.h | 15 ++ Libraries/LibWeb/CSS/StyleSheetList.cpp | 2 + .../LibWeb/TestCSSStyleSheetInvalidation.cpp | 38 ++++++ ...ht-dom-trailing-universal-invalidation.txt | 4 + .../media-list-shadow-mutation-counters.txt | 4 +- ...t-dom-trailing-universal-invalidation.html | 96 ++++++++++++- 7 files changed, 261 insertions(+), 26 deletions(-) diff --git a/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp b/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp index d3034e6984..b3b07cca4c 100644 --- a/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetInvalidation.cpp @@ -38,6 +38,83 @@ bool selector_may_match_light_dom_under_shadow_host(Selector const& selector) return selector.compound_selectors().size() > 1; } +static bool selector_may_match_shadow_host(Selector const&); +static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const&); + +static bool pseudo_class_has_positive_selector_list_arguments(PseudoClass pseudo_class) +{ + return first_is_one_of(pseudo_class, PseudoClass::Is, PseudoClass::Where); +} + +static bool simple_selector_may_match_shadow_host(Selector::SimpleSelector const& simple_selector) +{ + if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass) + return false; + + auto const& pseudo_class = simple_selector.pseudo_class(); + if (pseudo_class.type == PseudoClass::Host) + return true; + + if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type)) + return false; + + return any_of(pseudo_class.argument_selector_list, selector_may_match_shadow_host); +} + +static bool selector_may_match_shadow_host(Selector const& selector) +{ + return any_of(selector.compound_selectors(), [](auto const& compound_selector) { + return any_of(compound_selector.simple_selectors, [](auto const& simple_selector) { + return simple_selector_may_match_shadow_host(simple_selector); + }); + }); +} + +static bool compound_selector_may_match_shadow_host(Selector::CompoundSelector const& compound_selector) +{ + return any_of(compound_selector.simple_selectors, simple_selector_may_match_shadow_host); +} + +static bool simple_selector_may_match_light_dom_outside_shadow_host(Selector::SimpleSelector const& simple_selector) +{ + if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass) + return false; + + auto const& pseudo_class = simple_selector.pseudo_class(); + if (!pseudo_class_has_positive_selector_list_arguments(pseudo_class.type)) + return false; + + return any_of(pseudo_class.argument_selector_list, selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list); +} + +static bool compound_selector_may_match_light_dom_outside_shadow_host(Selector::CompoundSelector const& compound_selector) +{ + return any_of(compound_selector.simple_selectors, simple_selector_may_match_light_dom_outside_shadow_host); +} + +static bool selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(Selector const& selector) +{ + auto const& compound_selectors = selector.compound_selectors(); + for (size_t i = 0; i < compound_selectors.size(); ++i) { + if (compound_selector_may_match_light_dom_outside_shadow_host(compound_selectors[i])) + return true; + + if (!compound_selector_may_match_shadow_host(compound_selectors[i])) + continue; + + for (size_t j = i + 1; j < compound_selectors.size(); ++j) { + if (first_is_one_of(compound_selectors[j].combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling)) + return true; + } + } + return false; +} + +bool selector_may_match_light_dom_outside_shadow_host(Selector const& selector) +{ + return selector_may_match_light_dom_outside_shadow_host_via_positive_selector_list(selector); +} + bool selector_may_match_light_dom_under_shadow_host(StringView selector_text) { CSS::Parser::ParsingParams parsing_params; @@ -47,6 +124,15 @@ bool selector_may_match_light_dom_under_shadow_host(StringView selector_text) return selector_may_match_light_dom_under_shadow_host(selectors->first()); } +bool selector_may_match_light_dom_outside_shadow_host(StringView selector_text) +{ + CSS::Parser::ParsingParams parsing_params; + auto selectors = parse_selector(parsing_params, selector_text); + if (!selectors.has_value() || selectors->size() != 1) + return false; + return selector_may_match_light_dom_outside_shadow_host(selectors->first()); +} + static bool is_universal_only_compound(Selector::CompoundSelector const& compound_selector) { if (compound_selector.simple_selectors.is_empty()) @@ -127,6 +213,7 @@ void extend_style_sheet_invalidation_set_with_style_rule(StyleSheetInvalidationS for (auto const& selector : style_rule.absolutized_selectors()) { result.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector); + result.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector); result.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host); auto const& compound_selectors = selector->compound_selectors(); @@ -316,13 +403,14 @@ void invalidate_root_for_style_sheet_change(DOM::Node& root, StyleSheetInvalidat if (auto* host = shadow_root->host()) { // Broad shadow-root mutations are only allowed to escape the shadow tree when the stylesheet can - // actually reach host-side nodes. A layer-order-only change, for example, still needs a full restyle - // inside the shadow tree, but it should not turn into a document-wide invalidation for unrelated - // light-DOM. - if (result.may_match_light_dom_under_shadow_host && !result.may_match_shadow_host) { - host->invalidate_style(reason); - } else if (result.may_match_light_dom_under_shadow_host) { + // actually reach host-side nodes. Keep host-contained escapes bounded to the host subtree; a + // layer-order-only change, for example, still needs a full restyle inside the shadow tree, but it + // should not turn into a document-wide invalidation for unrelated light-DOM. Selectors with + // host-side sibling reach still need a root-side invalidation. + if (result.may_match_light_dom_outside_shadow_host) { host->root().invalidate_style(reason); + } else if (result.may_match_light_dom_under_shadow_host) { + host->invalidate_style(reason); } else if (result.may_match_shadow_host) { host->invalidate_style(reason); shadow_root->set_needs_style_update(true); @@ -340,16 +428,13 @@ void invalidate_root_for_style_sheet_change(DOM::Node& root, StyleSheetInvalidat invalidate_assigned_elements_for_dirty_slots(*shadow_root); if (auto* host = shadow_root->host()) { - // Slotted selectors never match the host itself, so a targeted ::slotted(...) invalidation only needs to - // walk the current host's light-DOM subtree. We can identify that case because it reaches host-side nodes - // without ever setting may_match_shadow_host. - // - // :host combinators are different: they can escape to siblings or other nodes rooted alongside the host, + // Slotted and host-contained :host selectors only need to walk the current host's light-DOM subtree. + // :host selectors with sibling combinators can escape to siblings or other nodes rooted alongside the host, // so they still need the broader host-root walk below. - if (result.may_match_light_dom_under_shadow_host && !result.may_match_shadow_host) { - invalidate_elements_matching_invalidation_set_and_anchor_rules(*host, result, host, shadow_root); - } else if (result.may_match_light_dom_under_shadow_host) { + if (result.may_match_light_dom_outside_shadow_host) { invalidate_elements_matching_invalidation_set_and_anchor_rules(host->root(), result, host, shadow_root); + } else if (result.may_match_light_dom_under_shadow_host) { + invalidate_elements_matching_invalidation_set_and_anchor_rules(*host, result, host, shadow_root); } else if (result.may_match_shadow_host) { bool host_or_shadow_tree_needs_style_update = false; if (Invalidation::element_matches_any_invalidation_set_property(*host, invalidation_set)) @@ -510,7 +595,7 @@ static ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects_for_ }; style_sheet.for_each_effective_style_producing_rule([&](CSSRule const& rule) { - if (effects.may_match_shadow_host && effects.may_match_light_dom_under_shadow_host && effects.may_affect_assigned_nodes_via_slots) + if (effects.all_set()) return; if (!is(rule)) @@ -520,11 +605,12 @@ static ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects_for_ for (auto const& selector : style_rule.absolutized_selectors()) { effects.may_match_shadow_host |= selector->contains_pseudo_class(PseudoClass::Host); effects.may_match_light_dom_under_shadow_host |= selector_may_match_light_dom_under_shadow_host(*selector); + effects.may_match_light_dom_outside_shadow_host |= selector_may_match_light_dom_outside_shadow_host(*selector); if (!effects.may_affect_assigned_nodes_via_slots && !slots.is_empty()) effects.may_affect_assigned_nodes_via_slots = selector_may_affect_assigned_nodes_via_slot_inheritance(*selector); - if (effects.may_match_shadow_host && effects.may_match_light_dom_under_shadow_host && effects.may_affect_assigned_nodes_via_slots) + if (effects.all_set()) return; } }); @@ -540,6 +626,7 @@ ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(DOM::Shadow auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, shadow_root); effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host; effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host; + effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host; effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots; }); @@ -558,9 +645,10 @@ ShadowRootStylesheetEffects determine_shadow_root_stylesheet_effects(CSSStyleShe auto sheet_effects = determine_shadow_root_stylesheet_effects_for_sheet(style_sheet, *shadow_root); effects.may_match_shadow_host |= sheet_effects.may_match_shadow_host; effects.may_match_light_dom_under_shadow_host |= sheet_effects.may_match_light_dom_under_shadow_host; + effects.may_match_light_dom_outside_shadow_host |= sheet_effects.may_match_light_dom_outside_shadow_host; effects.may_affect_assigned_nodes_via_slots |= sheet_effects.may_affect_assigned_nodes_via_slots; - if (effects.may_match_shadow_host && effects.may_match_light_dom_under_shadow_host && effects.may_affect_assigned_nodes_via_slots) + if (effects.all_set()) break; } @@ -573,10 +661,10 @@ static bool invalidate_shadow_host_side_for_style_sheet_change(DOM::ShadowRoot& if (!host) return false; - if (effects.may_match_light_dom_under_shadow_host && !effects.may_match_shadow_host) { - host->invalidate_style(reason); - } else if (effects.may_match_light_dom_under_shadow_host) { + if (effects.may_match_light_dom_outside_shadow_host) { host->root().invalidate_style(reason); + } else if (effects.may_match_light_dom_under_shadow_host) { + host->invalidate_style(reason); } else if (effects.may_affect_assigned_nodes_via_slots) { host->invalidate_style(reason); } else if (effects.may_match_shadow_host) { diff --git a/Libraries/LibWeb/CSS/StyleSheetInvalidation.h b/Libraries/LibWeb/CSS/StyleSheetInvalidation.h index b81ccc1213..78e43ed703 100644 --- a/Libraries/LibWeb/CSS/StyleSheetInvalidation.h +++ b/Libraries/LibWeb/CSS/StyleSheetInvalidation.h @@ -23,6 +23,7 @@ struct StyleSheetInvalidationSet { InvalidationSet invalidation_set; bool may_match_shadow_host { false }; bool may_match_light_dom_under_shadow_host { false }; + bool may_match_light_dom_outside_shadow_host { false }; struct PseudoElementInvalidationRule { InvalidationSet anchor_set; @@ -42,7 +43,16 @@ struct StyleSheetInvalidationSet { struct ShadowRootStylesheetEffects { bool may_match_shadow_host { false }; bool may_match_light_dom_under_shadow_host { false }; + bool may_match_light_dom_outside_shadow_host { false }; bool may_affect_assigned_nodes_via_slots { false }; + + bool all_set() const + { + return may_match_shadow_host + && may_match_light_dom_under_shadow_host + && may_match_light_dom_outside_shadow_host + && may_affect_assigned_nodes_via_slots; + } }; enum class ShouldInvalidateRuleCache { @@ -60,6 +70,11 @@ void extend_style_sheet_invalidation_set_with_style_rule(StyleSheetInvalidationS bool selector_may_match_light_dom_under_shadow_host(Selector const&); WEB_API bool selector_may_match_light_dom_under_shadow_host(StringView selector_text); +// Shadow-root :host rules with sibling combinators can target host siblings or other nodes outside the host subtree. +// These need broader host-root invalidation than host-subtree selectors like :host > * or :host *. +bool selector_may_match_light_dom_outside_shadow_host(Selector const&); +WEB_API bool selector_may_match_light_dom_outside_shadow_host(StringView selector_text); + // Apply a built invalidation set to `root` (a Document or a ShadowRoot). When `force_broad_invalidation` is true, // schedule a tree-wide restyle regardless of the targeted set; this is used when the sheet contains rule kinds (such // as @property or @keyframes) whose effects are not captured by selector invalidation alone. diff --git a/Libraries/LibWeb/CSS/StyleSheetList.cpp b/Libraries/LibWeb/CSS/StyleSheetList.cpp index 664e922fc2..1fcc40db49 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetList.cpp @@ -189,6 +189,7 @@ void StyleSheetList::add_sheet(CSSStyleSheet& sheet) auto effects = determine_shadow_root_stylesheet_effects(*shadow_root); invalidation_set_result.may_match_shadow_host |= effects.may_match_shadow_host; invalidation_set_result.may_match_light_dom_under_shadow_host |= effects.may_match_light_dom_under_shadow_host; + invalidation_set_result.may_match_light_dom_outside_shadow_host |= effects.may_match_light_dom_outside_shadow_host; } } @@ -224,6 +225,7 @@ void StyleSheetList::remove_sheet(CSSStyleSheet& sheet) auto effects = determine_shadow_root_stylesheet_effects(*shadow_root); invalidation_set_result.may_match_shadow_host |= effects.may_match_shadow_host; invalidation_set_result.may_match_light_dom_under_shadow_host |= effects.may_match_light_dom_under_shadow_host; + invalidation_set_result.may_match_light_dom_outside_shadow_host |= effects.may_match_light_dom_outside_shadow_host; } } diff --git a/Tests/LibWeb/TestCSSStyleSheetInvalidation.cpp b/Tests/LibWeb/TestCSSStyleSheetInvalidation.cpp index 8f67138fb3..967809f264 100644 --- a/Tests/LibWeb/TestCSSStyleSheetInvalidation.cpp +++ b/Tests/LibWeb/TestCSSStyleSheetInvalidation.cpp @@ -13,11 +13,49 @@ TEST_CASE(selector_escape_to_light_dom_under_shadow_host) { EXPECT(!CSS::selector_may_match_light_dom_under_shadow_host(":host"sv)); EXPECT(!CSS::selector_may_match_light_dom_under_shadow_host(":host(.active)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_under_shadow_host(":is(:host)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_under_shadow_host(":where(:host)"sv)); EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":host > *"sv)); EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":host + .item"sv)); EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":host ~ .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":is(:host) > *"sv)); + EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":where(:host) .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_under_shadow_host(":is(.foo, :host) + .item"sv)); EXPECT(CSS::selector_may_match_light_dom_under_shadow_host("::slotted(.item)"sv)); } +TEST_CASE(selector_escape_to_light_dom_outside_shadow_host) +{ + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":host"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":host(.active)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":host > *"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":host *"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host("::slotted(.item)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":where(:host)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host) > *"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":where(:host) *"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host > .item)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":where(:host .item)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":host:has(+ .item)"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":has(:host) + .item"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":not(:host) + .item"sv)); + EXPECT(!CSS::selector_may_match_light_dom_outside_shadow_host(":nth-child(1 of :host) + .item"sv)); + + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":host + .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":host ~ .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":host + :has(*)"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host) + .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":where(:host) ~ .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host) + :has(*)"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(.foo, :host) + .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":where(.foo, :host(.active)) + .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(:where(:host)) + .item"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host + .item)"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":where(:host ~ .item)"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(:host + .item) > .child"sv)); + EXPECT(CSS::selector_may_match_light_dom_outside_shadow_host(":is(.foo, :where(:host + .item))"sv)); +} + } diff --git a/Tests/LibWeb/Text/expected/css/shadow-root-host-light-dom-trailing-universal-invalidation.txt b/Tests/LibWeb/Text/expected/css/shadow-root-host-light-dom-trailing-universal-invalidation.txt index 4cedba4653..f259bc1b22 100644 --- a/Tests/LibWeb/Text/expected/css/shadow-root-host-light-dom-trailing-universal-invalidation.txt +++ b/Tests/LibWeb/Text/expected/css/shadow-root-host-light-dom-trailing-universal-invalidation.txt @@ -1,2 +1,6 @@ PASS: host child combinator add stays on the host-side tree (2 invalidations) PASS: host descendant combinator add stays on the host-side tree (2 invalidations) +PASS: broad host-side stylesheet add avoids document-wide invalidation (2 invalidations) +PASS: broad host-side sibling escape reaches root-side invalidation (1 full invalidations) +PASS: broad functional host-side selector avoids document-wide invalidation (2 invalidations) +PASS: broad functional host-side sibling escape reaches root-side invalidation (1 full invalidations) diff --git a/Tests/LibWeb/Text/expected/css/style-invalidation/media-list-shadow-mutation-counters.txt b/Tests/LibWeb/Text/expected/css/style-invalidation/media-list-shadow-mutation-counters.txt index 814408c08c..63a03254ee 100644 --- a/Tests/LibWeb/Text/expected/css/style-invalidation/media-list-shadow-mutation-counters.txt +++ b/Tests/LibWeb/Text/expected/css/style-invalidation/media-list-shadow-mutation-counters.txt @@ -7,7 +7,7 @@ PASS: shadow media mutation activates :host :where selector | styleInvalidations PASS: shadow media mutation deactivates classed :host selector | styleInvalidations=1, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media appendMedium activates :host selector | styleInvalidations=1, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media deleteMedium deactivates :host selector | styleInvalidations=1, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 -PASS: shadow media mutation activates internal rule guarded by :host | styleInvalidations=0, fullStyleInvalidations=1, elementStyleRecomputations=14, elementStyleNoopRecomputations=13, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 +PASS: shadow media mutation activates internal rule guarded by :host | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=4, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates ::slotted class selector | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=4, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates ::slotted type and class selector | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=4, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates ::slotted attribute selector | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=4, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 @@ -20,5 +20,5 @@ PASS: shadow media mutation activates named slot inheritance | styleInvalidation PASS: shadow media mutation activates descendant slot inheritance | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=3, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates nested host structure | styleInvalidations=1, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates paragraph assigned node | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=4, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 -PASS: shadow media mutation activates compound host and slotted selectors | styleInvalidations=1, fullStyleInvalidations=1, elementStyleRecomputations=14, elementStyleNoopRecomputations=9, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 +PASS: shadow media mutation activates compound host and slotted selectors | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=0, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 PASS: shadow media mutation activates inner and slotted selectors together | styleInvalidations=0, fullStyleInvalidations=0, elementStyleRecomputations=5, elementStyleNoopRecomputations=3, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=0, hasInvalidationMetadataCandidates=0, hasMatchInvocations=0, hasResultCacheHits=0, hasResultCacheMisses=0 diff --git a/Tests/LibWeb/Text/input/css/shadow-root-host-light-dom-trailing-universal-invalidation.html b/Tests/LibWeb/Text/input/css/shadow-root-host-light-dom-trailing-universal-invalidation.html index da8a672f5e..88d69a8269 100644 --- a/Tests/LibWeb/Text/input/css/shadow-root-host-light-dom-trailing-universal-invalidation.html +++ b/Tests/LibWeb/Text/input/css/shadow-root-host-light-dom-trailing-universal-invalidation.html @@ -10,11 +10,19 @@ } function verifyInvalidationsStayHostSide(label, maxInvalidations) { - const invalidations = internals.getStyleInvalidationCounters().styleInvalidations; - if (invalidations > 0 && invalidations <= maxInvalidations) - println(`PASS: ${label} (${invalidations} invalidations)`); + const counters = internals.getStyleInvalidationCounters(); + if (counters.fullStyleInvalidations == 0 && counters.styleInvalidations > 0 && counters.styleInvalidations <= maxInvalidations) + println(`PASS: ${label} (${counters.styleInvalidations} invalidations)`); else - println(`FAIL: ${label} (${invalidations} invalidations)`); + println(`FAIL: ${label} (${counters.styleInvalidations} invalidations, ${counters.fullStyleInvalidations} full invalidations)`); + } + + function verifyInvalidationsReachRootSide(label) { + const counters = internals.getStyleInvalidationCounters(); + if (counters.fullStyleInvalidations > 0) + println(`PASS: ${label} (${counters.fullStyleInvalidations} full invalidations)`); + else + println(`FAIL: ${label} (${counters.styleInvalidations} invalidations, ${counters.fullStyleInvalidations} full invalidations)`); } function populateShadowBystanders(shadowRoot, prefix) { @@ -52,5 +60,85 @@ descendantShadowRoot.appendChild(descendantStyle); getComputedStyle(nestedDescendant).color; verifyInvalidationsStayHostSide("host descendant combinator add stays on the host-side tree", 4); + + const broadHost = document.createElement("div"); + broadHost.innerHTML = "broad target"; + document.body.appendChild(broadHost); + for (let i = 0; i < 25; ++i) { + const bystander = document.createElement("span"); + bystander.textContent = `document bystander ${i}`; + document.body.appendChild(bystander); + } + + const broadTarget = document.getElementById("broad-target"); + const broadShadowRoot = broadHost.attachShadow({ mode: "open" }); + settleAndReset(broadTarget); + const broadStyle = document.createElement("style"); + broadStyle.textContent = ` + @keyframes shadow-root-host-side-broad-invalidation { + from { opacity: 1; } + to { opacity: 1; } + } + :host * { color: rgb(0, 0, 255); } + `; + broadShadowRoot.appendChild(broadStyle); + getComputedStyle(broadTarget).color; + verifyInvalidationsStayHostSide("broad host-side stylesheet add avoids document-wide invalidation", 4); + + const siblingEscapeHost = document.createElement("div"); + document.body.appendChild(siblingEscapeHost); + const siblingEscapeTarget = document.createElement("div"); + siblingEscapeTarget.innerHTML = "sibling escape target"; + document.body.appendChild(siblingEscapeTarget); + const siblingEscapeShadowRoot = siblingEscapeHost.attachShadow({ mode: "open" }); + settleAndReset(siblingEscapeTarget); + const siblingEscapeStyle = document.createElement("style"); + siblingEscapeStyle.textContent = ` + @keyframes shadow-root-host-side-sibling-escape-invalidation { + from { opacity: 1; } + to { opacity: 1; } + } + :host + :has(*) { color: rgb(255, 0, 0); } + `; + siblingEscapeShadowRoot.appendChild(siblingEscapeStyle); + getComputedStyle(siblingEscapeTarget).color; + verifyInvalidationsReachRootSide("broad host-side sibling escape reaches root-side invalidation"); + + const functionalHost = document.createElement("div"); + functionalHost.innerHTML = "functional host target"; + document.body.appendChild(functionalHost); + const functionalHostTarget = document.getElementById("functional-host-target"); + const functionalHostShadowRoot = functionalHost.attachShadow({ mode: "open" }); + settleAndReset(functionalHostTarget); + const functionalHostStyle = document.createElement("style"); + functionalHostStyle.textContent = ` + @keyframes shadow-root-functional-host-side-invalidation { + from { opacity: 1; } + to { opacity: 1; } + } + :is(:host) * { color: rgb(0, 0, 255); } + `; + functionalHostShadowRoot.appendChild(functionalHostStyle); + getComputedStyle(functionalHostTarget).color; + verifyInvalidationsStayHostSide("broad functional host-side selector avoids document-wide invalidation", 4); + + const functionalSiblingEscapeHost = document.createElement("div"); + document.body.appendChild(functionalSiblingEscapeHost); + const functionalSiblingEscapeTarget = document.createElement("div"); + functionalSiblingEscapeTarget.innerHTML = "functional sibling escape target"; + document.body.appendChild(functionalSiblingEscapeTarget); + const functionalSiblingEscapeShadowRoot = functionalSiblingEscapeHost.attachShadow({ mode: "open" }); + settleAndReset(functionalSiblingEscapeTarget); + const functionalSiblingEscapeStyle = document.createElement("style"); + functionalSiblingEscapeStyle.textContent = ` + @keyframes shadow-root-functional-sibling-escape-invalidation { + from { opacity: 1; } + to { opacity: 1; } + } + :is(:host) + :has(*) { color: rgb(255, 0, 0); } + `; + functionalSiblingEscapeShadowRoot.appendChild(functionalSiblingEscapeStyle); + getComputedStyle(functionalSiblingEscapeTarget).color; + verifyInvalidationsReachRootSide("broad functional host-side sibling escape reaches root-side invalidation"); });