Improve gating around nodeIsReadySubtree to prevent unnecessary calls

Free temp DocumentFragment when possible
This commit is contained in:
Karl Seguin
2026-07-17 09:44:19 +08:00
parent cb23ad7cbb
commit f5ace3701f
2 changed files with 36 additions and 19 deletions

View File

@@ -2563,16 +2563,17 @@ pub fn moveAllChildren(self: *Frame, source: *Node, parent: *Node, ref_node: ?*N
// Nodes moved into a not-yet-started script run the script's
// children-changed steps first (whatwg/html#10188), then each inserted
// node's own ready work runs, in tree order, with everything in place.
// Ready work only happens on becoming connected, so a detached
// destination has none.
if (parent.isConnected()) {
if (parent.is(Element.Html.Script)) |script| {
if (!script._executed) {
try self.nodeIsReady(false, parent);
}
}
}
for (moved.items) |child| {
try self.nodeIsReadySubtree(child);
for (moved.items) |child| {
try self.nodeIsReadySubtree(child);
}
}
}
@@ -2681,17 +2682,17 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
// Inserting into a not-yet-started script runs the script's
// children-changed steps first, then the inserted nodes' own ready work
// (whatwg/html#10188: the outer script executes before an inner one).
// Ready work only happens on becoming connected, so a detached parent
// has none.
if (opts.run_ready and parent_is_connected) {
if (parent.is(Element.Html.Script)) |script| {
if (!script._executed) {
try self.nodeIsReady(false, parent);
}
}
}
// nodeIsReady resolves the node's owning frame itself (only for the few node
// types that have ready work), so pass the incumbent `self`.
if (opts.run_ready) {
// nodeIsReady resolves the node's owning frame itself (only for the few node
// types that have ready work), so pass the incumbent `self`.
try self.nodeIsReadySubtree(child);
}
@@ -2931,9 +2932,16 @@ fn nodeIsReadySubtree(self: *Frame, node: *Node) !void {
if (node._type != .element or node.firstChild() == null) {
return self.nodeIsReady(false, node);
}
// Scripts can mutate the tree. Safe to do this since nodeIsReady re-checks
// connectivity.
var elements: std.ArrayList(*Node) = .empty;
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
while (tw.next()) |el| {
try self.nodeIsReady(false, el.asNode());
try elements.append(self.call_arena, el.asNode());
}
for (elements.items) |el| {
try self.nodeIsReady(false, el);
}
}

View File

@@ -1429,8 +1429,6 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame
}, frame);
}
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
/// Validates all nodes, removes existing children, then appends new children.
// ParentNode.append/prepend with several nodes convert them into a fragment
// first, so the insertion happens as one operation: an earlier script must
// observe its later siblings inserted (and can remove them before they run).
@@ -1440,29 +1438,40 @@ pub fn appendNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void
_ = try self.appendChild(child, frame);
return;
}
const fragment = (try DocumentFragment.init(frame)).asNode();
const fragment = try DocumentFragment.init(frame);
const fragment_node = fragment.asNode();
// The fragment is internal — JS never sees it, and no mutation record
// targets it — so it can be reclaimed once its children have moved out.
// If conversion or insertion failed, nodes left inside stay parented to
// it (per spec), so it must live on.
defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
_ = try fragment.appendChild(child, frame);
_ = try fragment_node.appendChild(child, frame);
}
_ = try self.appendChild(fragment, frame);
_ = try self.appendChild(fragment_node, frame);
}
pub fn prependNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void {
const reference = self.firstChild();
if (nodes.len == 1) {
const child = try nodes[0].toNode(frame);
_ = try self.insertBefore(child, reference, frame);
_ = try self.insertBefore(child, self.firstChild(), frame);
return;
}
const fragment = (try DocumentFragment.init(frame)).asNode();
const fragment = try DocumentFragment.init(frame);
const fragment_node = fragment.asNode();
defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
_ = try fragment.appendChild(child, frame);
_ = try fragment_node.appendChild(child, frame);
}
_ = try self.insertBefore(fragment, reference, frame);
// The reference child is evaluated after converting nodes into the
// fragment: one of the arguments may be the current first child.
_ = try self.insertBefore(fragment_node, self.firstChild(), frame);
}
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
/// Validates all nodes, removes existing children, then appends new children.
pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void {
// First pass: validate all nodes and collect them
// We need to collect because DocumentFragments contribute their children, not themselves