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:
Tim Ledbetter 2026-01-28 13:12:46 +00:00 committed by Shannon Booth
parent 2017347a77
commit 18b8ba1fd3
9 changed files with 213 additions and 21 deletions

View file

@ -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

View file

@ -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 roots 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 nodes 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 newParents 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())

View file

@ -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
{

View file

@ -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;

View 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;
}
}

View 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 };
};
}

View file

@ -90,17 +90,7 @@ GC::Ptr<HTML::HTMLSlotElement> find_a_slot(Slottable const& slottable, OpenFlag
// 6. Return the first slot in tree order in shadows descendants whose name is slottables 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 roots 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;

View file

@ -490,6 +490,7 @@ class PseudoElement;
class Range;
class RegisteredObserver;
class ShadowRoot;
class SlotRegistry;
class StaticNodeList;
class StaticRange;
class StyleInvalidator;

View file

@ -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 elements 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 elements root.
DOM::assign_slottables_for_a_tree(root());
}