LibWeb: Avoid sibling walks for last-child invalidation
Track last-child and backward positional selector dependencies separately on parent nodes. A last-child or only-child selector can only change the element at the trailing edge, so insertions and removals can invalidate that element directly instead of walking every previous sibling. Keep the previous-sibling walk for selectors such as nth-last-child and last-of-type, where every previous element's from-end position may change.
This commit is contained in:
parent
62a5ee7ff9
commit
bae4434780
5 changed files with 31 additions and 25 deletions
|
|
@ -89,27 +89,21 @@ void invalidate_structurally_affected_siblings(DOM::Node& node, DOM::StyleInvali
|
|||
}
|
||||
|
||||
if (is_insertion_or_removal) {
|
||||
// OPTIMIZATION: Only walk previous siblings if the parent has been observed to contain a child that matches a
|
||||
// pseudo-class whose match result can depend on siblings after that element. Otherwise, no
|
||||
// previous sibling can possibly need invalidation due to this insertion or removal.
|
||||
if (auto* parent_node = as_if<DOM::ParentNode>(node.parent()); parent_node && parent_node->has_child_affected_by_backward_structural_changes()) {
|
||||
// OPTIMIZATION: :last-child / :only-child can only flip for the element transitioning into or out of the
|
||||
// trailing position, so handle that directly. Only walk all previous siblings if the parent has
|
||||
// children affected by selectors whose from-end index can change, such as :nth-last-child.
|
||||
if (auto* parent_node = as_if<DOM::ParentNode>(node.parent()); parent_node && parent_node->has_child_affected_by_last_child_pseudo_class()) {
|
||||
if (last_child_transition_target && last_child_transition_target->affected_by_last_child_pseudo_class())
|
||||
mark_sibling_for_style_update(*last_child_transition_target);
|
||||
}
|
||||
if (auto* parent_node = as_if<DOM::ParentNode>(node.parent()); parent_node && parent_node->has_child_affected_by_backward_positional_pseudo_class()) {
|
||||
auto& counters = node.document().style_invalidation_counters();
|
||||
for (auto* sibling = node.previous_sibling(); sibling; sibling = sibling->previous_sibling()) {
|
||||
++counters.previous_sibling_invalidation_walk_visits;
|
||||
auto* element = as_if<DOM::Element>(sibling);
|
||||
if (!element)
|
||||
continue;
|
||||
bool needs_mark = false;
|
||||
if (element->affected_by_backward_positional_pseudo_class()) {
|
||||
// :nth-last-child / :nth-last-of-type / :last-of-type / :only-of-type all need
|
||||
// every previous sibling re-evaluated since their from-end indices shift.
|
||||
needs_mark = true;
|
||||
} else if (element->affected_by_last_child_pseudo_class() && element == last_child_transition_target) {
|
||||
// :last-child and :only-child only flip for the element transitioning into/out of
|
||||
// the trailing position.
|
||||
needs_mark = true;
|
||||
}
|
||||
if (needs_mark)
|
||||
if (element->affected_by_backward_positional_pseudo_class())
|
||||
mark_sibling_for_style_update(*element);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ void Element::set_affected_by_last_child_pseudo_class(bool value)
|
|||
m_affected_by_last_child_pseudo_class = value;
|
||||
if (value) {
|
||||
if (auto* parent = as_if<ParentNode>(this->parent()))
|
||||
parent->set_has_child_affected_by_backward_structural_changes(true);
|
||||
parent->set_has_child_affected_by_last_child_pseudo_class(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ void Element::set_affected_by_backward_positional_pseudo_class(bool value)
|
|||
m_affected_by_backward_positional_pseudo_class = value;
|
||||
if (value) {
|
||||
if (auto* parent = as_if<ParentNode>(this->parent()))
|
||||
parent->set_has_child_affected_by_backward_structural_changes(true);
|
||||
parent->set_has_child_affected_by_backward_positional_pseudo_class(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ public:
|
|||
|
||||
GC::Ptr<Element> get_element_by_id(FlyString const& id) const;
|
||||
|
||||
bool has_child_affected_by_backward_structural_changes() const { return m_has_child_affected_by_backward_structural_changes; }
|
||||
void set_has_child_affected_by_backward_structural_changes(bool value) { m_has_child_affected_by_backward_structural_changes = value; }
|
||||
bool has_child_affected_by_last_child_pseudo_class() const { return m_has_child_affected_by_last_child_pseudo_class; }
|
||||
void set_has_child_affected_by_last_child_pseudo_class(bool value) { m_has_child_affected_by_last_child_pseudo_class = value; }
|
||||
|
||||
bool has_child_affected_by_backward_positional_pseudo_class() const { return m_has_child_affected_by_backward_positional_pseudo_class; }
|
||||
void set_has_child_affected_by_backward_positional_pseudo_class(bool value) { m_has_child_affected_by_backward_positional_pseudo_class = value; }
|
||||
|
||||
protected:
|
||||
ParentNode(JS::Realm& realm, Document& document, NodeType type)
|
||||
|
|
@ -60,7 +63,8 @@ protected:
|
|||
|
||||
private:
|
||||
GC::Ptr<HTMLCollection> m_children;
|
||||
bool m_has_child_affected_by_backward_structural_changes { false };
|
||||
bool m_has_child_affected_by_last_child_pseudo_class { false };
|
||||
bool m_has_child_affected_by_backward_positional_pseudo_class { false };
|
||||
};
|
||||
|
||||
template<>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
PASS: no backward-structural rule: append does not invalidate previous siblings | previousSiblingInvalidationWalkVisits=0
|
||||
PASS: forward-only rule (:first-child): append does not invalidate previous siblings | previousSiblingInvalidationWalkVisits=0
|
||||
PASS: backward-structural rule on unrelated subtree: append does not invalidate previous siblings | previousSiblingInvalidationWalkVisits=0
|
||||
PASS: backward-structural rule on fixture: append DOES invalidate previous siblings | previousSiblingInvalidationWalkVisits=20
|
||||
PASS: last-child rule on fixture: append targets previous last child without walking | previousSiblingInvalidationWalkVisits=0
|
||||
PASS: nth-last-child rule on fixture: append DOES walk previous siblings | previousSiblingInvalidationWalkVisits=20
|
||||
|
|
|
|||
|
|
@ -73,12 +73,19 @@
|
|||
};
|
||||
});
|
||||
|
||||
// Case 4: A backward-structural rule (:last-child) DOES apply to our fixture's children.
|
||||
// The optimization correctly does not skip the walk — the previously-last child must be re-styled
|
||||
// when a new last child is appended. This case exists to confirm the optimization does not over-apply.
|
||||
runCase("backward-structural rule on fixture: append DOES invalidate previous siblings", () => {
|
||||
// Case 4: A trailing-position rule (:last-child) DOES apply to our fixture's children.
|
||||
// The previously-last child must be re-styled when a new last child is appended, but that does not
|
||||
// require walking all previous siblings.
|
||||
runCase("last-child rule on fixture: append targets previous last child without walking", () => {
|
||||
const style = addStyle(document.head, "section > div:last-child { color: rebeccapurple; }");
|
||||
return () => style.remove();
|
||||
});
|
||||
|
||||
// Case 5: A backward-position rule (:nth-last-child) DOES apply to our fixture's children.
|
||||
// Every previous child must be re-evaluated since each child's from-end index changes.
|
||||
runCase("nth-last-child rule on fixture: append DOES walk previous siblings", () => {
|
||||
const style = addStyle(document.head, "section > div:nth-last-child(odd) { color: rebeccapurple; }");
|
||||
return () => style.remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue