LibWeb: Implement partial SVG relayout

Previously, any SVG geometry attribute change would mark the entire
document layout tree as dirty, triggering a full layout pass even though
only the SVG subtree was affected. This made SVG geometry animations
unnecessarily expensive.

Fix this by stopping `needs_layout_update` propagation at the SVGSVGBox
boundary and tracking dirty SVG roots separately on the Document. When
`update_layout()` finds that only SVG roots need relayout (and the
document layout root is clean), it runs SVGFormattingContext on each
dirty SVG root in a fresh LayoutState and commits the results directly,
bypassing the full document layout pass entirely.

This results in a substantial performance improvement on pages with
animated SVGs, such as https://www.cloudflare.com/,
https://www.duolingo.com/, and our GC graph explorer page.
This commit is contained in:
Aliaksandr Kalenik
2026-02-06 02:26:56 +01:00
committed by Alexander Kalenik
parent f051bc45fc
commit abecc746d7
Notes: github-actions[bot] 2026-02-09 02:04:14 +00:00
6 changed files with 181 additions and 17 deletions

View File

@@ -30,6 +30,7 @@
#include <LibWeb/Layout/FormattingContext.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/SVGSVGBox.h>
#include <LibWeb/Layout/TableWrapper.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
@@ -1479,6 +1480,21 @@ void Node::set_needs_layout_update(DOM::SetNeedsLayoutReason reason)
if (ancestor->m_needs_layout_update)
break;
ancestor->m_needs_layout_update = true;
if (auto* svg_box = as_if<SVGSVGBox>(ancestor)) {
// Absolutely positioned elements are laid out by the formatting context (FC) of their
// containing block, not by the FC of their parent. When an abspos element lives inside
// an SVG subtree (e.g. inside <foreignObject>) but its containing block is an ancestor
// outside that subtree, partial SVG relayout cannot lay it out: only the SVGFormattingContext
// runs, while the containing block's FC (which is responsible for the abspos) does not.
// In this case we must not stop propagation here — letting needs_layout_update reach the
// layout root ensures update_layout() takes the full layout path instead.
if (is_absolutely_positioned()) {
if (auto cb = containing_block(); cb && !svg_box->is_inclusive_ancestor_of(*cb))
continue;
}
document().mark_svg_root_as_needing_relayout(*svg_box);
break;
}
}
// Reset intrinsic size caches for ancestors up to abspos or SVG root boundary.