From 645da2e3076d24901a38a95b24d82aedaf9f83d0 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 19 Feb 2026 09:45:56 +0800 Subject: [PATCH] Reduce cost of various Element render-related properties. Added a get-only `getStyle` which doesn't lazily create a new style if none exists. This can be used in the (frequently used) `checkVisibility` to avoid an allocation. Added a specialized getBoundingClientRectForVisible which skips the checkVisibility check, since a few callers have already done their own visibility check. DOMRect is now off the heap. This avoids _a lot_ of allocation when a DOMRect is only needed for internal calculation, e.g. in Document.elementFromPoint. --- src/browser/webapi/DOMRect.zig | 16 ++-- src/browser/webapi/Document.zig | 4 +- src/browser/webapi/Element.zig | 96 ++++++++++++--------- src/browser/webapi/IntersectionObserver.zig | 26 +++--- src/cdp/domains/dom.zig | 8 +- 5 files changed, 83 insertions(+), 67 deletions(-) diff --git a/src/browser/webapi/DOMRect.zig b/src/browser/webapi/DOMRect.zig index b331da28..14f93bc0 100644 --- a/src/browser/webapi/DOMRect.zig +++ b/src/browser/webapi/DOMRect.zig @@ -36,35 +36,35 @@ pub fn init(x: f64, y: f64, width: f64, height: f64, page: *Page) !*DOMRect { }); } -pub fn getX(self: *DOMRect) f64 { +pub fn getX(self: *const DOMRect) f64 { return self._x; } -pub fn getY(self: *DOMRect) f64 { +pub fn getY(self: *const DOMRect) f64 { return self._y; } -pub fn getWidth(self: *DOMRect) f64 { +pub fn getWidth(self: *const DOMRect) f64 { return self._width; } -pub fn getHeight(self: *DOMRect) f64 { +pub fn getHeight(self: *const DOMRect) f64 { return self._height; } -pub fn getTop(self: *DOMRect) f64 { +pub fn getTop(self: *const DOMRect) f64 { return @min(self._y, self._y + self._height); } -pub fn getRight(self: *DOMRect) f64 { +pub fn getRight(self: *const DOMRect) f64 { return @max(self._x, self._x + self._width); } -pub fn getBottom(self: *DOMRect) f64 { +pub fn getBottom(self: *const DOMRect) f64 { return @max(self._y, self._y + self._height); } -pub fn getLeft(self: *DOMRect) f64 { +pub fn getLeft(self: *const DOMRect) f64 { return @min(self._x, self._x + self._width); } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index eb8f0c41..e92f0dcf 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -555,8 +555,8 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element while (stack.items.len > 0) { const node = stack.pop() orelse break; if (node.is(Element)) |element| { - if (try element.checkVisibility(page)) { - const rect = try element.getBoundingClientRect(page); + if (element.checkVisibility(page)) { + const rect = element.getBoundingClientRectForVisible(page); if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) { topmost = element; } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index b97c7eb5..320a93e4 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -663,7 +663,7 @@ pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNo return gop.value_ptr.*; } -pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties { +pub fn getOrCreateStyle(self: *Element, page: *Page) !*CSSStyleProperties { const gop = try page._element_styles.getOrPut(page.arena, self); if (!gop.found_existing) { gop.value_ptr.* = try CSSStyleProperties.init(self, false, page); @@ -671,6 +671,10 @@ pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties { return gop.value_ptr.*; } +fn getStyle(self: *Element, page: *Page) ?*CSSStyleProperties { + return page._element_styles.get(self); +} + pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList { const gop = try page._element_class_lists.getOrPut(page.arena, self); if (!gop.found_existing) { @@ -943,14 +947,15 @@ pub fn parentElement(self: *Element) ?*Element { return self._proto.parentElement(); } -pub fn checkVisibility(self: *Element, page: *Page) !bool { +pub fn checkVisibility(self: *Element, page: *Page) bool { var current: ?*Element = self; while (current) |el| { - const style = try el.getStyle(page); - const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); - if (std.mem.eql(u8, display, "none")) { - return false; + if (el.getStyle(page)) |style| { + const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); + if (std.mem.eql(u8, display, "none")) { + return false; + } } current = el.parentElement(); } @@ -958,11 +963,15 @@ pub fn checkVisibility(self: *Element, page: *Page) !bool { return true; } -fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, height: f64 } { - const style = try self.getStyle(page); - const decl = style.asCSSStyleDeclaration(); - var width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0; - var height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0; +fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } { + var width: f64 = 5.0; + var height: f64 = 5.0; + + if (self.getStyle(page)) |style| { + const decl = style.asCSSStyleDeclaration(); + width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0; + height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0; + } if (width == 5.0 or height == 5.0) { const tag = self.getTag(); @@ -987,52 +996,59 @@ fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, heigh return .{ .width = width, .height = height }; } -pub fn getClientWidth(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getClientWidth(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } - const dims = try self.getElementDimensions(page); + const dims = self.getElementDimensions(page); return dims.width; } -pub fn getClientHeight(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getClientHeight(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } - const dims = try self.getElementDimensions(page); + const dims = self.getElementDimensions(page); return dims.height; } -pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { - if (!try self.checkVisibility(page)) { - return page._factory.create(DOMRect{ +pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect { + if (!self.checkVisibility(page)) { + return .{ ._x = 0.0, ._y = 0.0, ._width = 0.0, ._height = 0.0, - }); + }; } + return self.getBoundingClientRectForVisible(page); +} + +// Some cases need a the BoundingClientRect but have already done the +// visibility check. +pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect { const y = calculateDocumentPosition(self.asNode()); - const dims = try self.getElementDimensions(page); + const dims = self.getElementDimensions(page); // Use sibling position for x coordinate to ensure siblings have different x values const x = calculateSiblingPosition(self.asNode()); - return page._factory.create(DOMRect{ + return .{ ._x = x, ._y = y, ._width = dims.width, ._height = dims.height, - }); + }; } pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect { - if (!try self.checkVisibility(page)) { + if (!self.checkVisibility(page)) { return &.{}; } - const ptr = try self.getBoundingClientRect(page); - return ptr[0..1]; + const rects = try page.call_arena.alloc(DOMRect, 1); + rects[0] = self.getBoundingClientRectForVisible(page); + return rects; } pub fn getScrollTop(self: *Element, page: *Page) u32 { @@ -1061,41 +1077,41 @@ pub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void { gop.value_ptr.x = @intCast(@max(0, value)); } -pub fn getScrollHeight(self: *Element, page: *Page) !f64 { +pub fn getScrollHeight(self: *Element, page: *Page) f64 { // In our dummy layout engine, content doesn't overflow return self.getClientHeight(page); } -pub fn getScrollWidth(self: *Element, page: *Page) !f64 { +pub fn getScrollWidth(self: *Element, page: *Page) f64 { // In our dummy layout engine, content doesn't overflow return self.getClientWidth(page); } -pub fn getOffsetHeight(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getOffsetHeight(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } - const dims = try self.getElementDimensions(page); + const dims = self.getElementDimensions(page); return dims.height; } -pub fn getOffsetWidth(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getOffsetWidth(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } - const dims = try self.getElementDimensions(page); + const dims = self.getElementDimensions(page); return dims.width; } -pub fn getOffsetTop(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getOffsetTop(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } return calculateDocumentPosition(self.asNode()); } -pub fn getOffsetLeft(self: *Element, page: *Page) !f64 { - if (!try self.checkVisibility(page)) { +pub fn getOffsetLeft(self: *Element, page: *Page) f64 { + if (!self.checkVisibility(page)) { return 0.0; } return calculateSiblingPosition(self.asNode()); @@ -1541,7 +1557,7 @@ pub const JsApi = struct { pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{}); pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{}); pub const dataset = bridge.accessor(Element.getDataset, null, .{}); - pub const style = bridge.accessor(Element.getStyle, null, .{}); + pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{}); pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{}); pub const hasAttribute = bridge.function(Element.hasAttribute, .{}); pub const hasAttributes = bridge.function(Element.hasAttributes, .{}); diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index a8f7ba1a..4431d224 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -177,19 +177,19 @@ fn calculateIntersection( target: *Element, page: *Page, ) !IntersectionData { - const target_rect = try target.getBoundingClientRect(page); + const target_rect = target.getBoundingClientRect(page); // Use root element's rect or viewport (simplified: assume 1920x1080) const root_rect = if (self._root) |root| - try root.getBoundingClientRect(page) + root.getBoundingClientRect(page) else // Simplified viewport - assume 1920x1080 for now - try page._factory.create(DOMRect{ + DOMRect{ ._x = 0.0, ._y = 0.0, ._width = 1920.0, ._height = 1080.0, - }); + }; // For a headless browser without real layout, we treat all elements as fully visible. // This avoids fingerprinting issues (massive viewports) and matches the behavior @@ -200,7 +200,7 @@ fn calculateIntersection( const intersection_ratio: f64 = if (has_parent) 1.0 else 0.0; // Intersection rect is the same as the target rect if visible, otherwise zero rect - const intersection_rect = if (has_parent) target_rect else &zero_rect; + const intersection_rect = if (has_parent) target_rect else zero_rect; return .{ .is_intersecting = is_intersecting, @@ -214,9 +214,9 @@ fn calculateIntersection( const IntersectionData = struct { is_intersecting: bool, intersection_ratio: f64, - intersection_rect: *DOMRect, - bounding_client_rect: *DOMRect, - root_bounds: *DOMRect, + intersection_rect: DOMRect, + bounding_client_rect: DOMRect, + root_bounds: DOMRect, }; fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool { @@ -241,17 +241,19 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) if (should_report) { const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" }); + errdefer page.releaseArena(arena); + const entry = try arena.create(IntersectionObserverEntry); entry.* = .{ ._page = page, ._arena = arena, ._target = target, ._time = page.window._performance.now(), - ._bounding_client_rect = data.bounding_client_rect, - ._intersection_rect = data.intersection_rect, - ._root_bounds = data.root_bounds, - ._intersection_ratio = data.intersection_ratio, ._is_intersecting = is_now_intersecting, + ._root_bounds = try page._factory.create(data.root_bounds), + ._intersection_rect = try page._factory.create(data.intersection_rect), + ._bounding_client_rect = try page._factory.create(data.bounding_client_rect), + ._intersection_ratio = data.intersection_ratio, }; try self._pending_entries.append(self._arena, entry); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index d6900ced..28bfb906 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -356,7 +356,7 @@ const BoxModel = struct { // shapeOutside: ?ShapeOutsideInfo, }; -fn rectToQuad(rect: *const DOMNode.Element.DOMRect) Quad { +fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad { return Quad{ rect._x, rect._y, @@ -434,9 +434,7 @@ fn getContentQuads(cmd: anytype) !void { // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? // Elements like SVGElement may have multiple quads. - const rect = try element.getBoundingClientRect(page); - const quad = rectToQuad(rect); - + const quad = rectToQuad(element.getBoundingClientRect(page)); return cmd.sendResult(.{ .quads = &.{quad} }, .{}); } @@ -455,7 +453,7 @@ fn getBoxModel(cmd: anytype) !void { // TODO implement for document or text const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement; - const rect = try element.getBoundingClientRect(page); + const rect = element.getBoundingClientRect(page); const quad = rectToQuad(rect); const zero = [_]f64{0.0} ** 8;