LibWeb: Skip layout update for disconnected elements querying metrics

Introduce Document::update_layout_if_needed_for_node() which only calls
update_layout() when the node is connected. Use it at all call sites
that query layout metrics (offsets, client dimensions, image size, SVG
bounding box, etc.) so disconnected elements no longer trigger an
unnecessary layout.
This commit is contained in:
Aliaksandr Kalenik 2026-03-05 12:47:32 +01:00 committed by Alexander Kalenik
parent 63113cf5c9
commit 8f7bb7dd2e
7 changed files with 24 additions and 17 deletions

View file

@ -1462,6 +1462,12 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo
overflow_origin_computed_values.set_overflow_y(CSS::Overflow::Visible);
}
void Document::update_layout_if_needed_for_node(Node const& node, UpdateLayoutReason reason)
{
if (node.is_connected())
update_layout(reason);
}
void Document::update_layout(UpdateLayoutReason reason)
{
auto navigable = this->navigable();

View file

@ -369,6 +369,7 @@ public:
void update_style_if_needed_for_element(AbstractElement const&);
[[nodiscard]] bool element_needs_style_update(AbstractElement const&) const;
void update_layout(UpdateLayoutReason);
void update_layout_if_needed_for_node(Node const&, UpdateLayoutReason);
[[nodiscard]] bool layout_is_up_to_date() const;
void update_paint_and_hit_testing_properties_if_needed();
void update_animated_style_if_needed();

View file

@ -1406,7 +1406,7 @@ Vector<CSSPixelRect> Element::get_client_rects() const
return {};
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<Document&>(document()).update_layout(UpdateLayoutReason::ElementGetClientRects);
const_cast<Document&>(document()).update_layout_if_needed_for_node(*this, UpdateLayoutReason::ElementGetClientRects);
// 1. If the element on which it was invoked does not have an associated layout box return an empty DOMRectList
// object and stop this algorithm.
@ -1508,7 +1508,7 @@ int Element::client_width() const
}
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<Document&>(document()).update_layout(UpdateLayoutReason::ElementClientWidth);
const_cast<Document&>(document()).update_layout_if_needed_for_node(*this, UpdateLayoutReason::ElementClientWidth);
// 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
if (!paintable_box())
@ -1533,7 +1533,7 @@ int Element::client_height() const
}
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<Document&>(document()).update_layout(UpdateLayoutReason::ElementClientHeight);
const_cast<Document&>(document()).update_layout_if_needed_for_node(*this, UpdateLayoutReason::ElementClientHeight);
// 1. If the element has no associated CSS layout box or if the CSS layout box is inline, return zero.
if (!paintable_box())
@ -3726,7 +3726,7 @@ GC::Ref<WebIDL::Promise> Element::scroll_by(HTML::ScrollToOptions options)
bool Element::check_visibility(Optional<CheckVisibilityOptions> options)
{
// NOTE: Ensure that layout is up-to-date before looking at metrics.
document().update_layout(UpdateLayoutReason::ElementCheckVisibility);
document().update_layout_if_needed_for_node(*this, UpdateLayoutReason::ElementCheckVisibility);
// 1. If this does not have an associated box, return false.
if (!paintable_box())

View file

@ -462,7 +462,7 @@ static bool any_ancestor_establishes_a_fixed_position_containing_block(Layout::N
GC::Ptr<DOM::Element> HTMLElement::scroll_parent() const
{
// NOTE: We have to ensure that the layout is up-to-date before querying the layout tree.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementScrollParent);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementScrollParent);
// 1. If any of the following holds true, return null and terminate this algorithm:
// - The element does not have an associated box.
@ -514,7 +514,7 @@ GC::Ptr<DOM::Element> HTMLElement::scroll_parent() const
GC::Ptr<DOM::Element> HTMLElement::offset_parent() const
{
// NOTE: We have to ensure that the layout is up-to-date before querying the layout tree.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetParent);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementOffsetParent);
// 1. If any of the following holds true return null and terminate this algorithm:
// - The element does not have an associated box.
@ -590,7 +590,7 @@ int HTMLElement::offset_top() const
return 0;
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetTop);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementOffsetTop);
if (!paintable_box())
return 0;
@ -632,7 +632,7 @@ int HTMLElement::offset_left() const
return 0;
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetLeft);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementOffsetLeft);
if (!paintable_box())
return 0;
@ -670,7 +670,7 @@ int HTMLElement::offset_left() const
int HTMLElement::offset_width() const
{
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetWidth);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementOffsetWidth);
// 1. If the element does not have any associated box return zero and terminate this algorithm.
auto const* box = paintable_box();
@ -689,7 +689,7 @@ int HTMLElement::offset_width() const
int HTMLElement::offset_height() const
{
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetHeight);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLElementOffsetHeight);
// 1. If the element does not have any associated box return zero and terminate this algorithm.
auto const* box = paintable_box();

View file

@ -254,7 +254,7 @@ void HTMLImageElement::set_visible_in_viewport(bool)
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-width
WebIDL::UnsignedLong HTMLImageElement::width() const
{
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLImageElementWidth);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLImageElementWidth);
// Return the rendered width of the image, in CSS pixels, if the image is being rendered.
if (auto* paintable_box = this->paintable_box())
@ -285,7 +285,7 @@ void HTMLImageElement::set_width(WebIDL::UnsignedLong width)
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-height
WebIDL::UnsignedLong HTMLImageElement::height() const
{
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLImageElementHeight);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLImageElementHeight);
// Return the rendered height of the image, in CSS pixels, if the image is being rendered.
if (auto* paintable_box = this->paintable_box())
@ -343,7 +343,7 @@ int HTMLImageElement::x() const
// The x attribute, on getting, must return the scaled x-coordinate of the left border edge of the first box
// associated with the element, relative to the initial containing block origin, ignoring any transforms that apply
// to the element and its ancestors, or zero if there is no box.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLImageElementX);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLImageElementX);
auto const* paintable_box = this->paintable_box();
if (!paintable_box)
@ -362,7 +362,7 @@ int HTMLImageElement::y() const
// The y attribute, on getting, must return the scaled y-coordinate of the top border edge of the first box
// associated with the element, relative to the initial containing block origin, ignoring any transforms that apply
// to the element and its ancestors, or zero if there is no box.
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLImageElementY);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLImageElementY);
auto const* paintable_box = this->paintable_box();
if (!paintable_box)

View file

@ -2352,7 +2352,7 @@ WebIDL::ExceptionOr<void> HTMLInputElement::set_size(WebIDL::UnsignedLong value)
// https://html.spec.whatwg.org/multipage/input.html#dom-input-height
WebIDL::UnsignedLong HTMLInputElement::height() const
{
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLInputElementHeight);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLInputElementHeight);
// When the input element's type attribute is not in the Image Button state, then no image is available.
if (type_state() != TypeAttributeState::ImageButton)
@ -2387,7 +2387,7 @@ void HTMLInputElement::set_height(WebIDL::UnsignedLong value)
// https://html.spec.whatwg.org/multipage/input.html#dom-input-width
WebIDL::UnsignedLong HTMLInputElement::width() const
{
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLInputElementWidth);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::HTMLInputElementWidth);
// When the input element's type attribute is not in the Image Button state, then no image is available.
if (type_state() != TypeAttributeState::ImageButton)

View file

@ -355,7 +355,7 @@ WebIDL::ExceptionOr<GC::Ref<Geometry::DOMRect>> SVGGraphicsElement::get_b_box(Op
// SVG coordinate space (before any viewbox or other transformations), so it should be possible to
// calculate this from SVG geometry without a full layout tree (at least for simple cases).
// See: https://svgwg.org/svg2-draft/coords.html#BoundingBoxes
const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::SVGGraphicsElementGetBBox);
const_cast<DOM::Document&>(document()).update_layout_if_needed_for_node(*this, DOM::UpdateLayoutReason::SVGGraphicsElementGetBBox);
if (!layout_node())
return Geometry::DOMRect::create(realm());
// Invert the SVG -> screen space transform.