LibWeb: Avoid subtree iteration when assigning slottables
This change introduces SlotRegistry to track slot elements per shadow root. This allows us to iterate slots directly when assigning slottables for a tree instead of walking an entire subtree.
This commit is contained in:
parent
2017347a77
commit
18b8ba1fd3
9 changed files with 213 additions and 21 deletions
|
|
@ -344,6 +344,7 @@ set(SOURCES
|
|||
DOM/Range.cpp
|
||||
DOM/ShadowRoot.cpp
|
||||
DOM/Slot.cpp
|
||||
DOM/SlotRegistry.cpp
|
||||
DOM/Slottable.cpp
|
||||
DOM/StaticNodeList.cpp
|
||||
DOM/StaticRange.cpp
|
||||
|
|
|
|||
|
|
@ -747,6 +747,15 @@ void Node::insert_before(GC::Ref<Node> node, GC::Ptr<Node> child, bool suppress_
|
|||
signal_a_slot_change(*this_slot_element);
|
||||
}
|
||||
|
||||
// AD-HOC: Register any slot elements in the inserted subtree with the shadow root’s slot registry
|
||||
// before running assign_slottables_for_a_tree, so the registry is up-to-date.
|
||||
if (auto* shadow_root = as_if<ShadowRoot>(node_to_insert->root())) {
|
||||
node_to_insert->for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto& slot) {
|
||||
shadow_root->register_slot(slot);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Run assign slottables for a tree with node’s root.
|
||||
assign_slottables_for_a_tree(node_to_insert->root());
|
||||
|
||||
|
|
@ -977,10 +986,15 @@ void Node::remove(bool suppress_observers)
|
|||
|
||||
// 10. If node has an inclusive descendant that is a slot, then:
|
||||
auto has_descendent_slot = false;
|
||||
auto* shadow_root = as_if<ShadowRoot>(parent_root);
|
||||
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto const&) {
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto& slot) {
|
||||
has_descendent_slot = true;
|
||||
return TraversalDecision::Break;
|
||||
if (!shadow_root)
|
||||
return TraversalDecision::Break;
|
||||
// AD-HOC: Unregister slot from the shadow root's registry before assign_slottables_for_a_tree.
|
||||
shadow_root->unregister_slot(slot);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
if (has_descendent_slot) {
|
||||
|
|
@ -1284,10 +1298,15 @@ WebIDL::ExceptionOr<void> Node::move_node(Node& new_parent, Node* child)
|
|||
|
||||
// 16. If node has an inclusive descendant that is a slot:
|
||||
auto has_descendent_slot = false;
|
||||
auto* shadow_root = as_if<ShadowRoot>(old_parent_root);
|
||||
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto const&) {
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto& slot) {
|
||||
has_descendent_slot = true;
|
||||
return TraversalDecision::Break;
|
||||
if (!shadow_root)
|
||||
return TraversalDecision::Break;
|
||||
// AD-HOC: Unregister slot from the shadow root's registry before assign_slottables_for_a_tree.
|
||||
shadow_root->unregister_slot(slot);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
if (has_descendent_slot) {
|
||||
|
|
@ -1344,6 +1363,15 @@ WebIDL::ExceptionOr<void> Node::move_node(Node& new_parent, Node* child)
|
|||
assign_a_slot(this_element.as_slottable());
|
||||
}
|
||||
|
||||
// AD-HOC: Register any slot elements in the moved subtree with the shadow root's slot registry so the registry is
|
||||
// up-to-date.
|
||||
if (auto* new_shadow_root = as_if<ShadowRoot>(root())) {
|
||||
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto& slot) {
|
||||
new_shadow_root->register_slot(slot);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
// 22. If newParent’s root is a shadow root, and newParent is a slot whose assigned nodes is empty, then run signal a slot change for newParent.
|
||||
if (auto* new_parent_slot_element = as_if<HTML::HTMLSlotElement>(new_parent); new_parent_slot_element && new_parent.root().is_shadow_root()) {
|
||||
if (new_parent_slot_element->assigned_nodes_internal().is_empty())
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <LibWeb/DOM/DocumentOrShadowRoot.h>
|
||||
#include <LibWeb/DOM/Event.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/DOM/SlotRegistry.h>
|
||||
#include <LibWeb/HTML/HTMLSlotElement.h>
|
||||
#include <LibWeb/HTML/HTMLTemplateElement.h>
|
||||
#include <LibWeb/HTML/Parser/HTMLParser.h>
|
||||
#include <LibWeb/Layout/BlockContainer.h>
|
||||
|
|
@ -230,6 +232,26 @@ ElementByIdMap& ShadowRoot::element_by_id() const
|
|||
return *m_element_by_id;
|
||||
}
|
||||
|
||||
void ShadowRoot::register_slot(HTML::HTMLSlotElement& slot)
|
||||
{
|
||||
if (!m_slot_registry)
|
||||
m_slot_registry = make<SlotRegistry>();
|
||||
m_slot_registry->add(slot);
|
||||
}
|
||||
|
||||
void ShadowRoot::unregister_slot(HTML::HTMLSlotElement& slot)
|
||||
{
|
||||
if (m_slot_registry)
|
||||
m_slot_registry->remove(slot);
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::HTMLSlotElement> ShadowRoot::first_slot_with_name(FlyString const& name) const
|
||||
{
|
||||
if (!m_slot_registry)
|
||||
return nullptr;
|
||||
return m_slot_registry->first_slot_with_name(name);
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-shadow-1/#shadow-root-part-element-map
|
||||
ShadowRoot::PartElementMap const& ShadowRoot::part_element_map() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <LibWeb/CSS/StyleScope.h>
|
||||
#include <LibWeb/DOM/DocumentFragment.h>
|
||||
#include <LibWeb/DOM/ElementByIdMap.h>
|
||||
#include <LibWeb/DOM/SlotRegistry.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/WebIDL/ObservableArray.h>
|
||||
|
||||
|
|
@ -72,6 +73,18 @@ public:
|
|||
|
||||
ElementByIdMap& element_by_id() const;
|
||||
|
||||
void register_slot(HTML::HTMLSlotElement&);
|
||||
void unregister_slot(HTML::HTMLSlotElement&);
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_registered_slot(Callback callback)
|
||||
{
|
||||
if (m_slot_registry)
|
||||
m_slot_registry->for_each_slot(callback);
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::HTMLSlotElement> first_slot_with_name(FlyString const& name) const;
|
||||
|
||||
CSS::StyleScope const& style_scope() const { return m_style_scope; }
|
||||
CSS::StyleScope& style_scope() { return m_style_scope; }
|
||||
|
||||
|
|
@ -110,6 +123,8 @@ private:
|
|||
|
||||
mutable OwnPtr<ElementByIdMap> m_element_by_id;
|
||||
|
||||
OwnPtr<SlotRegistry> m_slot_registry;
|
||||
|
||||
GC::Ptr<CSS::StyleSheetList> m_style_sheets;
|
||||
mutable GC::Ptr<WebIDL::ObservableArray> m_adopted_style_sheets;
|
||||
|
||||
|
|
|
|||
74
Libraries/LibWeb/DOM/SlotRegistry.cpp
Normal file
74
Libraries/LibWeb/DOM/SlotRegistry.cpp
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/DOM/SlotRegistry.h>
|
||||
#include <LibWeb/HTML/HTMLSlotElement.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
void SlotRegistry::add(HTML::HTMLSlotElement& slot)
|
||||
{
|
||||
m_slot_count -= m_slots.remove_all_matching([](GC::Weak<HTML::HTMLSlotElement>& s) {
|
||||
return !s;
|
||||
});
|
||||
|
||||
auto slot_removed = m_slots.remove_first_matching([&](auto const& other_slot) {
|
||||
return &slot == other_slot.ptr();
|
||||
});
|
||||
if (slot_removed)
|
||||
--m_slot_count;
|
||||
|
||||
if (m_slots.is_empty() || m_slots.last()->is_before(slot) || !try_insert_in_tree_order(slot)) {
|
||||
m_slots.empend(slot);
|
||||
++m_slot_count;
|
||||
}
|
||||
}
|
||||
|
||||
bool SlotRegistry::try_insert_in_tree_order(HTML::HTMLSlotElement& slot)
|
||||
{
|
||||
// Walk forward from the slot we're adding to find the next registered slot. If we find one, insert before it.
|
||||
auto& shadow_root = slot.root();
|
||||
for (auto* node = slot.next_in_pre_order(&shadow_root); node; node = node->next_in_pre_order(&shadow_root)) {
|
||||
auto* following_slot = as_if<HTML::HTMLSlotElement>(*node);
|
||||
if (!following_slot)
|
||||
continue;
|
||||
|
||||
auto index = m_slots.find_first_index_if([&](auto const& s) {
|
||||
return s.ptr() == following_slot;
|
||||
});
|
||||
|
||||
if (index.has_value()) {
|
||||
m_slots.insert(index.value(), GC::Weak { slot });
|
||||
++m_slot_count;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SlotRegistry::remove(HTML::HTMLSlotElement& slot)
|
||||
{
|
||||
m_slot_count -= m_slots.remove_all_matching([&](GC::Weak<HTML::HTMLSlotElement>& s) {
|
||||
return !s || &slot == s.ptr();
|
||||
});
|
||||
}
|
||||
|
||||
GC::Ptr<HTML::HTMLSlotElement> SlotRegistry::first_slot_with_name(FlyString const& name) const
|
||||
{
|
||||
for (auto const& slot : m_slots) {
|
||||
if (slot && slot->slot_name() == name)
|
||||
return slot.ptr();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SlotRegistry::is_empty() const
|
||||
{
|
||||
return m_slot_count == 0;
|
||||
}
|
||||
|
||||
}
|
||||
44
Libraries/LibWeb/DOM/SlotRegistry.h
Normal file
44
Libraries/LibWeb/DOM/SlotRegistry.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/FlyString.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGC/Ptr.h>
|
||||
#include <LibGC/Weak.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
class SlotRegistry {
|
||||
public:
|
||||
void add(HTML::HTMLSlotElement&);
|
||||
void remove(HTML::HTMLSlotElement&);
|
||||
GC::Ptr<HTML::HTMLSlotElement> first_slot_with_name(FlyString const& name) const;
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_slot(Callback callback)
|
||||
{
|
||||
if (is_empty())
|
||||
return;
|
||||
|
||||
for (auto& slot : m_slots) {
|
||||
if (slot)
|
||||
callback(*slot);
|
||||
}
|
||||
}
|
||||
|
||||
bool is_empty() const;
|
||||
|
||||
private:
|
||||
bool try_insert_in_tree_order(HTML::HTMLSlotElement&);
|
||||
|
||||
Vector<GC::Weak<HTML::HTMLSlotElement>> m_slots;
|
||||
size_t m_slot_count { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -90,17 +90,7 @@ GC::Ptr<HTML::HTMLSlotElement> find_a_slot(Slottable const& slottable, OpenFlag
|
|||
|
||||
// 6. Return the first slot in tree order in shadow’s descendants whose name is slottable’s name, if any; otherwise null.
|
||||
auto const& slottable_name = slottable.visit([](auto const& node) { return node->slottable_name(); });
|
||||
GC::Ptr<HTML::HTMLSlotElement> slot;
|
||||
|
||||
shadow->for_each_in_subtree_of_type<HTML::HTMLSlotElement>([&](auto& child) {
|
||||
if (child.slot_name() != slottable_name)
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
slot = child;
|
||||
return TraversalDecision::Break;
|
||||
});
|
||||
|
||||
return slot;
|
||||
return shadow->first_slot_with_name(slottable_name);
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#find-slotables
|
||||
|
|
@ -227,14 +217,21 @@ void assign_slottables(GC::Ref<HTML::HTMLSlotElement> slot)
|
|||
// https://dom.spec.whatwg.org/#assign-slotables-for-a-tree
|
||||
void assign_slottables_for_a_tree(GC::Ref<Node> root)
|
||||
{
|
||||
// AD-HOC: This method iterates over the root's entire subtree. That iteration does nothing if the root is not a
|
||||
// shadow root (see `find_slottables`). This iteration can be very expensive as the HTML parser inserts
|
||||
// nodes, especially on sites with many elements. So we skip it if we know it's going to be a no-op anyways.
|
||||
if (!root->is_shadow_root() && !root->is_html_slot_element())
|
||||
return;
|
||||
|
||||
// To assign slottables for a tree, given a node root, run assign slottables for each slot of root’s inclusive
|
||||
// descendants, in tree order.
|
||||
|
||||
// OPTIMIZATION: If root is not a shadow root or slot element, there are no slots to process, so return early.
|
||||
auto* shadow_root = as_if<ShadowRoot>(*root);
|
||||
if (!shadow_root && !root->is_html_slot_element())
|
||||
return;
|
||||
|
||||
if (shadow_root) {
|
||||
shadow_root->for_each_registered_slot([](HTML::HTMLSlotElement& slot) {
|
||||
assign_slottables(slot);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
root->for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([](auto& slot) {
|
||||
assign_slottables(slot);
|
||||
return TraversalDecision::Continue;
|
||||
|
|
|
|||
|
|
@ -490,6 +490,7 @@ class PseudoElement;
|
|||
class Range;
|
||||
class RegisteredObserver;
|
||||
class ShadowRoot;
|
||||
class SlotRegistry;
|
||||
class StaticNodeList;
|
||||
class StaticRange;
|
||||
class StyleInvalidator;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <LibWeb/Bindings/HTMLSlotElementPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/HTML/HTMLSlotElement.h>
|
||||
|
||||
|
|
@ -151,6 +152,11 @@ void HTMLSlotElement::attribute_changed(FlyString const& local_name, Optional<St
|
|||
if (value == String {} && !old_value.has_value())
|
||||
return;
|
||||
|
||||
// OPTIMIZATION: Update the slot registry before changing the name.
|
||||
auto* shadow_root = as_if<DOM::ShadowRoot>(root());
|
||||
if (shadow_root)
|
||||
shadow_root->unregister_slot(*this);
|
||||
|
||||
// 4. If value is null or the empty string, then set element’s name to the empty string.
|
||||
if (!value.has_value())
|
||||
set_slot_name({});
|
||||
|
|
@ -158,6 +164,10 @@ void HTMLSlotElement::attribute_changed(FlyString const& local_name, Optional<St
|
|||
else
|
||||
set_slot_name(*value);
|
||||
|
||||
// OPTIMIZATION: Register the slot with its new name.
|
||||
if (shadow_root)
|
||||
shadow_root->register_slot(*this);
|
||||
|
||||
// 6. Run assign slottables for a tree with element’s root.
|
||||
DOM::assign_slottables_for_a_tree(root());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue