LibWeb: Retain calculated Element::ordinal_value for lists

`Element::ordinal_value` is called for every `li` element in
a list (ul, ol, menu).

Before:
  `ordinal_value` iterates through all of the children of the list
  owner. It is called once for each element: complexity $O(n^2)$.

After:
  - Save the result of the first calculation in `m_ordinal_value`
  then return it in subsequent calls.
  - Tree modifications are intercepted and trigger invalidation
    of the first node's `m_ordinal_value`:
    - insert_before
    - append
    - remove
  Results in noticeable performance improvement rendering' large
  lists: from 20s to 4s for 20K elements.
This commit is contained in:
Manuel Zahariev 2025-05-13 06:59:03 -07:00 committed by Sam Atkins
parent d27b43c1ee
commit 00d43b39d1
Notes: github-actions[bot] 2025-06-16 11:46:28 +00:00
5 changed files with 137 additions and 54 deletions

View file

@ -805,6 +805,16 @@ void Node::insert_before(GC::Ref<Node> node, GC::Ptr<Node> child, bool suppress_
set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::NodeInsertBefore);
}
// AD-HOC: invalidate the ordinal of the first list_item of the list_owner of the child node, if any.
if (child && child->is_element())
static_cast<Element*>(child.ptr())->maybe_invalidate_ordinals_for_list_owner();
else if (this->is_element() && !this->is_html_ol_ul_menu_element())
static_cast<Element*>(this)->maybe_invalidate_ordinals_for_list_owner();
// NOTE: If the child node is null and the parent node is an ol, ul or menu element then:
// the new node will be the first in the list of a potential list owner and it will not have
// an ordinal value (default from constructor).
// FIXME: This will not work if the child or the parent is not an element. Is insert_before even possible in this situation?
document().bump_dom_tree_version();
}
@ -854,6 +864,11 @@ WebIDL::ExceptionOr<GC::Ref<Node>> Node::append_child(GC::Ref<Node> node)
{
// To append a node to a parent, pre-insert node into parent before null.
return pre_insert(node, nullptr);
// AD-HOC: invalidate the ordinal of the first list_item of the first child sibling of the appended node, if any.
// NOTE: This works since ordinal values are accessed (for layout and paint) in the preorder of list_item nodes !!
if (auto* first_child_element = this->first_child_of_type<Element>())
first_child_element->maybe_invalidate_ordinals_for_list_owner();
}
// https://dom.spec.whatwg.org/#live-range-pre-remove-steps
@ -917,6 +932,12 @@ void Node::remove(bool suppress_observers)
// 6. Let oldNextSibling be nodes next sibling.
GC::Ptr<Node> old_next_sibling = next_sibling();
// AD-HOC: invalidate the ordinal of the first list_item of the list_owner of the removed node, if any.
if (is_element()) {
auto* this_element = static_cast<Element*>(this);
this_element->maybe_invalidate_ordinals_for_list_owner(this_element);
}
if (is_connected()) {
// Since the tree structure is about to change, we need to invalidate both style and layout.
// In the future, we should find a way to only invalidate the parts that actually need it.