Files
ladybird/Libraries/LibWeb/DOM/StyleInvalidator.cpp
Andreas Kling 118802b3f0 LibWeb: Scope media rule cache invalidation
Invalidate only the style scope whose media rules changed instead
of throwing away every shadow root rule cache whenever any active
stylesheet changes media query match state. Shadow-root stylesheet
changes still dirty the host side because :host and ::slotted
selectors can affect nodes outside the shadow tree.

When scoped invalidation leaves dirty descendants in a shadow root,
preserve the host ancestor chain so the document style update walk
reaches them before forced layout.

Add coverage that a matching media rule introduced in one shadow tree
does not broadly invalidate a page full of unrelated shadow roots,
and that a dirty shadow root is updated before layout is forced.
2026-04-28 09:49:50 +02:00

184 lines
6.8 KiB
C++

/*
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/StyleInvalidator.h>
namespace Web::DOM {
GC_DEFINE_ALLOCATOR(StyleInvalidator);
static bool element_matches_invalidation_rule(Element const& element, CSS::InvalidationSet const& match_set, bool match_any)
{
return match_any || element.includes_properties_from_invalidation_set(match_set);
}
void StyleInvalidator::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
for (auto const& it : m_pending_invalidations)
visitor.visit(it.key);
}
void StyleInvalidator::invalidate(Node& node)
{
perform_pending_style_invalidations(node, false);
m_pending_invalidations.clear();
}
bool StyleInvalidator::enqueue_invalidation_plan(Node& node, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan)
{
if (plan.is_empty())
return false;
if (plan.invalidate_whole_subtree) {
node.invalidate_style(reason);
return true;
}
if (plan.invalidate_self)
node.set_needs_style_update(true);
add_pending_invalidation(node, reason, plan);
if (auto* element = as_if<Element>(node)) {
for (auto const& sibling_rule : plan.sibling_rules)
apply_sibling_invalidation(*element, reason, sibling_rule);
}
return false;
}
void StyleInvalidator::add_pending_invalidation(GC::Ref<Node> node, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan)
{
if (plan.descendant_rules.is_empty())
return;
auto& pending_invalidations = m_pending_invalidations.ensure(node, [] {
return Vector<PendingDescendantInvalidation> {};
});
for (auto const& descendant_rule : plan.descendant_rules) {
PendingDescendantInvalidation pending_invalidation { reason, descendant_rule };
if (!pending_invalidations.contains_slow(pending_invalidation))
pending_invalidations.append(move(pending_invalidation));
}
}
void StyleInvalidator::apply_invalidation_plan(Element& element, StyleInvalidationReason reason, CSS::InvalidationPlan const& plan, bool& invalidate_entire_subtree)
{
if (plan.is_empty())
return;
if (plan.invalidate_whole_subtree) {
element.invalidate_style(reason);
invalidate_entire_subtree = true;
element.set_needs_style_update_internal(true);
if (element.has_child_nodes())
element.set_child_needs_style_update(true);
return;
}
if (plan.invalidate_self)
element.set_needs_style_update(true);
for (auto const& descendant_rule : plan.descendant_rules) {
PendingDescendantInvalidation pending_invalidation { reason, descendant_rule };
if (!m_active_descendant_invalidations.contains_slow(pending_invalidation))
m_active_descendant_invalidations.append(move(pending_invalidation));
}
for (auto const& sibling_rule : plan.sibling_rules)
apply_sibling_invalidation(element, reason, sibling_rule);
}
void StyleInvalidator::apply_sibling_invalidation(Element& element, StyleInvalidationReason reason, CSS::SiblingInvalidationRule const& sibling_rule)
{
auto apply_if_matching = [&](Element* sibling) {
if (!sibling)
return;
if (!element_matches_invalidation_rule(*sibling, sibling_rule.match_set, sibling_rule.match_any))
return;
(void)enqueue_invalidation_plan(*sibling, reason, *sibling_rule.payload);
};
switch (sibling_rule.reach) {
case CSS::SiblingInvalidationReach::Adjacent:
apply_if_matching(element.next_element_sibling());
break;
case CSS::SiblingInvalidationReach::Subsequent:
for (auto* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling())
apply_if_matching(sibling);
break;
default:
VERIFY_NOT_REACHED();
}
}
// This function makes a full pass over the entire DOM and:
// - converts "entire subtree needs style update" into "needs style update" for each inclusive descendant where it's found.
// - applies descendant invalidation rules to matching elements
void StyleInvalidator::perform_pending_style_invalidations(Node& node, bool invalidate_entire_subtree)
{
invalidate_entire_subtree |= node.entire_subtree_needs_style_update();
if (invalidate_entire_subtree) {
node.set_needs_style_update_internal(true);
if (node.has_child_nodes())
node.set_child_needs_style_update(true);
}
auto previous_active_descendant_invalidations_size = m_active_descendant_invalidations.size();
ScopeGuard restore_state = [this, previous_active_descendant_invalidations_size] {
m_active_descendant_invalidations.shrink(previous_active_descendant_invalidations_size);
};
if (!invalidate_entire_subtree) {
if (auto pending_invalidations = m_pending_invalidations.get(node); pending_invalidations.has_value()) {
m_active_descendant_invalidations.extend(*pending_invalidations);
}
if (auto* element = as_if<Element>(node)) {
size_t invalidation_index = 0;
while (invalidation_index < m_active_descendant_invalidations.size()) {
auto const& pending_invalidation = m_active_descendant_invalidations[invalidation_index++];
if (!element_matches_invalidation_rule(*element, pending_invalidation.rule.match_set, pending_invalidation.rule.match_any))
continue;
apply_invalidation_plan(*element, pending_invalidation.reason, *pending_invalidation.rule.payload, invalidate_entire_subtree);
if (invalidate_entire_subtree)
break;
}
if (invalidate_entire_subtree) {
node.set_needs_style_update_internal(true);
if (node.has_child_nodes())
node.set_child_needs_style_update(true);
}
}
}
for (auto* child = node.first_child(); child; child = child->next_sibling())
perform_pending_style_invalidations(*child, invalidate_entire_subtree);
if (node.is_element()) {
auto& element = static_cast<Element&>(node);
if (auto shadow_root = element.shadow_root()) {
perform_pending_style_invalidations(*shadow_root, invalidate_entire_subtree);
if (invalidate_entire_subtree || shadow_root->needs_style_update() || shadow_root->child_needs_style_update()) {
for (auto* ancestor = &node; ancestor; ancestor = ancestor->parent_or_shadow_host())
ancestor->set_child_needs_style_update(true);
}
}
}
node.set_entire_subtree_needs_style_update(false);
}
}