LibWeb: Target :active and :open invalidation
Changing :active or :open used the broad style invalidation path, so large subtrees were recomputed even when only the target element and selector-matched relatives could be affected. Reuse the :has() feature collector to keep conservative fallback for observable :has() cases, then use pseudo-class property invalidation for the common targeted path.
This commit is contained in:
parent
4bbc5e0950
commit
d8a55dad1c
6 changed files with 154 additions and 54 deletions
|
|
@ -5,14 +5,36 @@
|
|||
*/
|
||||
|
||||
#include <LibWeb/CSS/Invalidation/ElementStateInvalidator.h>
|
||||
#include <LibWeb/CSS/Invalidation/HasMutationFeatureCollector.h>
|
||||
#include <LibWeb/CSS/InvalidationSet.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/StyleInvalidationReason.h>
|
||||
|
||||
namespace Web::CSS::Invalidation {
|
||||
|
||||
static void invalidate_style_after_pseudo_class_state_change(DOM::Element& element, DOM::StyleInvalidationReason reason,
|
||||
PseudoClass pseudo_class)
|
||||
{
|
||||
bool may_affect_has_selectors = false;
|
||||
element.for_each_style_scope_which_may_observe_the_node([&](StyleScope& scope) {
|
||||
if (element_has_feature_used_in_has_selector(element, pseudo_class, scope))
|
||||
may_affect_has_selectors = true;
|
||||
});
|
||||
if (may_affect_has_selectors) {
|
||||
element.invalidate_style(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector<InvalidationSet::Property, 1> pseudo_class_properties {
|
||||
{ .type = InvalidationSet::Property::Type::PseudoClass, .value = pseudo_class },
|
||||
};
|
||||
element.invalidate_style(reason, pseudo_class_properties, { .invalidate_self = true });
|
||||
}
|
||||
|
||||
void invalidate_style_after_active_state_change(DOM::Element& element)
|
||||
{
|
||||
element.invalidate_style(DOM::StyleInvalidationReason::ElementSetActive);
|
||||
invalidate_style_after_pseudo_class_state_change(element, DOM::StyleInvalidationReason::ElementSetActive,
|
||||
PseudoClass::Active);
|
||||
}
|
||||
|
||||
void invalidate_style_after_modal_state_change(DOM::Element& element)
|
||||
|
|
@ -22,9 +44,8 @@ void invalidate_style_after_modal_state_change(DOM::Element& element)
|
|||
|
||||
void invalidate_style_after_open_state_change(DOM::Element& element)
|
||||
{
|
||||
// The :open pseudo-class can affect sibling selectors (e.g. dialog:open + sibling), so keep the existing broad
|
||||
// subtree and sibling invalidation.
|
||||
element.invalidate_style(DOM::StyleInvalidationReason::HTMLDetailsOrDialogOpenAttributeChange);
|
||||
invalidate_style_after_pseudo_class_state_change(element,
|
||||
DOM::StyleInvalidationReason::HTMLDetailsOrDialogOpenAttributeChange, PseudoClass::Open);
|
||||
}
|
||||
|
||||
void invalidate_style_after_option_selected_state_change(DOM::Element& element)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,27 @@ bool HasMutationFeatureCollector::subtree_has_feature_used_in_has_selector(DOM::
|
|||
return found;
|
||||
}
|
||||
|
||||
bool element_has_feature_used_in_has_selector(DOM::Element const& element, PseudoClass changed_pseudo_class,
|
||||
StyleScope const& style_scope)
|
||||
{
|
||||
if (!style_scope.may_have_has_selectors())
|
||||
return false;
|
||||
|
||||
auto const* data = style_scope.m_rule_cache ? &style_scope.m_rule_cache->style_invalidation_data : nullptr;
|
||||
if (!data)
|
||||
return true;
|
||||
|
||||
HasMutationFeatureCollector collector { *data };
|
||||
if (!collector.has_any_metadata())
|
||||
return true;
|
||||
if (data->has_selectors_sensitive_to_featureless_subtree_changes)
|
||||
return true;
|
||||
if (data->pseudo_classes_used_in_has_selectors.contains(changed_pseudo_class))
|
||||
return true;
|
||||
|
||||
return collector.element_has_feature_used_in_has_selector(element);
|
||||
}
|
||||
|
||||
bool subtree_has_feature_used_in_has_selector(DOM::Node& node, StyleScope const& style_scope)
|
||||
{
|
||||
auto const* data = style_scope.m_rule_cache ? &style_scope.m_rule_cache->style_invalidation_data : nullptr;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class Node;
|
|||
|
||||
namespace Web::CSS {
|
||||
|
||||
enum class PseudoClass;
|
||||
struct StyleInvalidationData;
|
||||
class StyleScope;
|
||||
|
||||
|
|
@ -25,14 +26,14 @@ public:
|
|||
explicit HasMutationFeatureCollector(StyleInvalidationData const&);
|
||||
|
||||
[[nodiscard]] bool has_any_metadata() const;
|
||||
[[nodiscard]] bool element_has_feature_used_in_has_selector(DOM::Element const&) const;
|
||||
[[nodiscard]] bool subtree_has_feature_used_in_has_selector(DOM::Node&) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool element_has_feature_used_in_has_selector(DOM::Element const&) const;
|
||||
|
||||
StyleInvalidationData const& m_data;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool element_has_feature_used_in_has_selector(DOM::Element const&, PseudoClass, StyleScope const&);
|
||||
[[nodiscard]] bool subtree_has_feature_used_in_has_selector(DOM::Node&, StyleScope const&);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
plain text hover: styleInvalidations=0, elementStyleRecomputations=0, elementStyleNoopRecomputations=0
|
||||
link hover: styleInvalidations=1, elementStyleRecomputations=2, elementStyleNoopRecomputations=1
|
||||
link color: rgb(0, 128, 0)
|
||||
active state: fullStyleInvalidations=0, elementStyleRecomputations=8
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#plain,
|
||||
#link {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: rgb(0, 128, 0);
|
||||
}
|
||||
</style>
|
||||
<div id="plain">plain text</div>
|
||||
<a id="link" href="#">link text</a>
|
||||
<script>
|
||||
function moveTo(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
internals.mouseMove(rect.left + 5, rect.top + 5);
|
||||
internals.updateStyle();
|
||||
}
|
||||
|
||||
function dumpCounters(label) {
|
||||
const c = internals.getStyleInvalidationCounters();
|
||||
println(`${label}: styleInvalidations=${c.styleInvalidations}, elementStyleRecomputations=${c.elementStyleRecomputations}, elementStyleNoopRecomputations=${c.elementStyleNoopRecomputations}`);
|
||||
}
|
||||
|
||||
test(() => {
|
||||
const plain = document.getElementById("plain");
|
||||
const link = document.getElementById("link");
|
||||
|
||||
getComputedStyle(plain).color;
|
||||
internals.resetStyleInvalidationCounters();
|
||||
moveTo(plain);
|
||||
dumpCounters("plain text hover");
|
||||
|
||||
internals.resetStyleInvalidationCounters();
|
||||
moveTo(link);
|
||||
dumpCounters("link hover");
|
||||
println(`link color: ${getComputedStyle(link).color}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#plain,
|
||||
#link {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: rgb(0, 128, 0);
|
||||
}
|
||||
|
||||
#active-target {
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#active-target:active {
|
||||
color: rgb(1, 2, 3);
|
||||
}
|
||||
|
||||
#active-target:active + #active-sibling {
|
||||
color: rgb(4, 5, 6);
|
||||
}
|
||||
|
||||
#active-container:active #active-ancestor-descendant {
|
||||
color: rgb(7, 8, 9);
|
||||
}
|
||||
</style>
|
||||
<div id="plain">plain text</div>
|
||||
<a id="link" href="#">link text</a>
|
||||
<div id="active-container">
|
||||
<button id="active-target">target</button>
|
||||
<div id="active-sibling">sibling</div>
|
||||
<div id="active-ancestor-descendant">ancestor descendant</div>
|
||||
</div>
|
||||
<script>
|
||||
function moveTo(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
internals.mouseMove(rect.left + 5, rect.top + 5);
|
||||
internals.updateStyle();
|
||||
}
|
||||
|
||||
function dumpCounters(label) {
|
||||
const c = internals.getStyleInvalidationCounters();
|
||||
println(
|
||||
`${label}: styleInvalidations=${c.styleInvalidations}, ` +
|
||||
`elementStyleRecomputations=${c.elementStyleRecomputations}, ` +
|
||||
`elementStyleNoopRecomputations=${c.elementStyleNoopRecomputations}`
|
||||
);
|
||||
}
|
||||
|
||||
function appendUnaffectedSubtree(parent, count) {
|
||||
for (let i = 0; i < count; ++i) {
|
||||
let leaf = document.createElement("span");
|
||||
leaf.textContent = `leaf ${i}`;
|
||||
parent.appendChild(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
function clickAndHold(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
internals.clickAndHold(Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2));
|
||||
internals.updateStyle();
|
||||
}
|
||||
|
||||
test(() => {
|
||||
const plain = document.getElementById("plain");
|
||||
const link = document.getElementById("link");
|
||||
const activeContainer = document.getElementById("active-container");
|
||||
const activeTarget = document.getElementById("active-target");
|
||||
|
||||
getComputedStyle(plain).color;
|
||||
internals.resetStyleInvalidationCounters();
|
||||
moveTo(plain);
|
||||
dumpCounters("plain text hover");
|
||||
|
||||
internals.resetStyleInvalidationCounters();
|
||||
moveTo(link);
|
||||
dumpCounters("link hover");
|
||||
println(`link color: ${getComputedStyle(link).color}`);
|
||||
|
||||
appendUnaffectedSubtree(activeContainer, 100);
|
||||
getComputedStyle(activeContainer).color;
|
||||
internals.resetStyleInvalidationCounters();
|
||||
clickAndHold(activeTarget);
|
||||
|
||||
const counters = internals.getStyleInvalidationCounters();
|
||||
println(
|
||||
`active state: fullStyleInvalidations=${counters.fullStyleInvalidations}, ` +
|
||||
`elementStyleRecomputations=${counters.elementStyleRecomputations}`
|
||||
);
|
||||
internals.mouseUp(0, 0);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue