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.
This commit is contained in:
Karl Seguin
2026-02-19 09:45:56 +08:00
parent bd29f168e0
commit 645da2e307
5 changed files with 83 additions and 67 deletions

View File

@@ -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; return self._x;
} }
pub fn getY(self: *DOMRect) f64 { pub fn getY(self: *const DOMRect) f64 {
return self._y; return self._y;
} }
pub fn getWidth(self: *DOMRect) f64 { pub fn getWidth(self: *const DOMRect) f64 {
return self._width; return self._width;
} }
pub fn getHeight(self: *DOMRect) f64 { pub fn getHeight(self: *const DOMRect) f64 {
return self._height; return self._height;
} }
pub fn getTop(self: *DOMRect) f64 { pub fn getTop(self: *const DOMRect) f64 {
return @min(self._y, self._y + self._height); 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); 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); 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); return @min(self._x, self._x + self._width);
} }

View File

@@ -555,8 +555,8 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
while (stack.items.len > 0) { while (stack.items.len > 0) {
const node = stack.pop() orelse break; const node = stack.pop() orelse break;
if (node.is(Element)) |element| { if (node.is(Element)) |element| {
if (try element.checkVisibility(page)) { if (element.checkVisibility(page)) {
const rect = try element.getBoundingClientRect(page); const rect = element.getBoundingClientRectForVisible(page);
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) { if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
topmost = element; topmost = element;
} }

View File

@@ -663,7 +663,7 @@ pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNo
return gop.value_ptr.*; 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); const gop = try page._element_styles.getOrPut(page.arena, self);
if (!gop.found_existing) { if (!gop.found_existing) {
gop.value_ptr.* = try CSSStyleProperties.init(self, false, page); 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.*; 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 { pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
const gop = try page._element_class_lists.getOrPut(page.arena, self); const gop = try page._element_class_lists.getOrPut(page.arena, self);
if (!gop.found_existing) { if (!gop.found_existing) {
@@ -943,14 +947,15 @@ pub fn parentElement(self: *Element) ?*Element {
return self._proto.parentElement(); return self._proto.parentElement();
} }
pub fn checkVisibility(self: *Element, page: *Page) !bool { pub fn checkVisibility(self: *Element, page: *Page) bool {
var current: ?*Element = self; var current: ?*Element = self;
while (current) |el| { while (current) |el| {
const style = try el.getStyle(page); if (el.getStyle(page)) |style| {
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) { if (std.mem.eql(u8, display, "none")) {
return false; return false;
}
} }
current = el.parentElement(); current = el.parentElement();
} }
@@ -958,11 +963,15 @@ pub fn checkVisibility(self: *Element, page: *Page) !bool {
return true; return true;
} }
fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, height: f64 } { fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
const style = try self.getStyle(page); var width: f64 = 5.0;
const decl = style.asCSSStyleDeclaration(); var height: f64 = 5.0;
var width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0;
var height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 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) { if (width == 5.0 or height == 5.0) {
const tag = self.getTag(); const tag = self.getTag();
@@ -987,52 +996,59 @@ fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, heigh
return .{ .width = width, .height = height }; return .{ .width = width, .height = height };
} }
pub fn getClientWidth(self: *Element, page: *Page) !f64 { pub fn getClientWidth(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
const dims = try self.getElementDimensions(page); const dims = self.getElementDimensions(page);
return dims.width; return dims.width;
} }
pub fn getClientHeight(self: *Element, page: *Page) !f64 { pub fn getClientHeight(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
const dims = try self.getElementDimensions(page); const dims = self.getElementDimensions(page);
return dims.height; return dims.height;
} }
pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return page._factory.create(DOMRect{ return .{
._x = 0.0, ._x = 0.0,
._y = 0.0, ._y = 0.0,
._width = 0.0, ._width = 0.0,
._height = 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 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 // Use sibling position for x coordinate to ensure siblings have different x values
const x = calculateSiblingPosition(self.asNode()); const x = calculateSiblingPosition(self.asNode());
return page._factory.create(DOMRect{ return .{
._x = x, ._x = x,
._y = y, ._y = y,
._width = dims.width, ._width = dims.width,
._height = dims.height, ._height = dims.height,
}); };
} }
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect { pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return &.{}; return &.{};
} }
const ptr = try self.getBoundingClientRect(page); const rects = try page.call_arena.alloc(DOMRect, 1);
return ptr[0..1]; rects[0] = self.getBoundingClientRectForVisible(page);
return rects;
} }
pub fn getScrollTop(self: *Element, page: *Page) u32 { 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)); 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 // In our dummy layout engine, content doesn't overflow
return self.getClientHeight(page); 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 // In our dummy layout engine, content doesn't overflow
return self.getClientWidth(page); return self.getClientWidth(page);
} }
pub fn getOffsetHeight(self: *Element, page: *Page) !f64 { pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
const dims = try self.getElementDimensions(page); const dims = self.getElementDimensions(page);
return dims.height; return dims.height;
} }
pub fn getOffsetWidth(self: *Element, page: *Page) !f64 { pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
const dims = try self.getElementDimensions(page); const dims = self.getElementDimensions(page);
return dims.width; return dims.width;
} }
pub fn getOffsetTop(self: *Element, page: *Page) !f64 { pub fn getOffsetTop(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
return calculateDocumentPosition(self.asNode()); return calculateDocumentPosition(self.asNode());
} }
pub fn getOffsetLeft(self: *Element, page: *Page) !f64 { pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
if (!try self.checkVisibility(page)) { if (!self.checkVisibility(page)) {
return 0.0; return 0.0;
} }
return calculateSiblingPosition(self.asNode()); return calculateSiblingPosition(self.asNode());
@@ -1541,7 +1557,7 @@ pub const JsApi = struct {
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{}); pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{}); pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});
pub const dataset = bridge.accessor(Element.getDataset, null, .{}); 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 attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
pub const hasAttribute = bridge.function(Element.hasAttribute, .{}); pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{}); pub const hasAttributes = bridge.function(Element.hasAttributes, .{});

View File

@@ -177,19 +177,19 @@ fn calculateIntersection(
target: *Element, target: *Element,
page: *Page, page: *Page,
) !IntersectionData { ) !IntersectionData {
const target_rect = try target.getBoundingClientRect(page); const target_rect = target.getBoundingClientRect(page);
// Use root element's rect or viewport (simplified: assume 1920x1080) // Use root element's rect or viewport (simplified: assume 1920x1080)
const root_rect = if (self._root) |root| const root_rect = if (self._root) |root|
try root.getBoundingClientRect(page) root.getBoundingClientRect(page)
else else
// Simplified viewport - assume 1920x1080 for now // Simplified viewport - assume 1920x1080 for now
try page._factory.create(DOMRect{ DOMRect{
._x = 0.0, ._x = 0.0,
._y = 0.0, ._y = 0.0,
._width = 1920.0, ._width = 1920.0,
._height = 1080.0, ._height = 1080.0,
}); };
// For a headless browser without real layout, we treat all elements as fully visible. // For a headless browser without real layout, we treat all elements as fully visible.
// This avoids fingerprinting issues (massive viewports) and matches the behavior // 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; 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 // 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 .{ return .{
.is_intersecting = is_intersecting, .is_intersecting = is_intersecting,
@@ -214,9 +214,9 @@ fn calculateIntersection(
const IntersectionData = struct { const IntersectionData = struct {
is_intersecting: bool, is_intersecting: bool,
intersection_ratio: f64, intersection_ratio: f64,
intersection_rect: *DOMRect, intersection_rect: DOMRect,
bounding_client_rect: *DOMRect, bounding_client_rect: DOMRect,
root_bounds: *DOMRect, root_bounds: DOMRect,
}; };
fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool { fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool {
@@ -241,17 +241,19 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
if (should_report) { if (should_report) {
const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" }); const arena = try page.getArena(.{ .debug = "IntersectionObserverEntry" });
errdefer page.releaseArena(arena);
const entry = try arena.create(IntersectionObserverEntry); const entry = try arena.create(IntersectionObserverEntry);
entry.* = .{ entry.* = .{
._page = page, ._page = page,
._arena = arena, ._arena = arena,
._target = target, ._target = target,
._time = page.window._performance.now(), ._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, ._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); try self._pending_entries.append(self._arena, entry);

View File

@@ -356,7 +356,7 @@ const BoxModel = struct {
// shapeOutside: ?ShapeOutsideInfo, // shapeOutside: ?ShapeOutsideInfo,
}; };
fn rectToQuad(rect: *const DOMNode.Element.DOMRect) Quad { fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad {
return Quad{ return Quad{
rect._x, rect._x,
rect._y, 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 = ""? // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""?
// Elements like SVGElement may have multiple quads. // Elements like SVGElement may have multiple quads.
const rect = try element.getBoundingClientRect(page); const quad = rectToQuad(element.getBoundingClientRect(page));
const quad = rectToQuad(rect);
return cmd.sendResult(.{ .quads = &.{quad} }, .{}); return cmd.sendResult(.{ .quads = &.{quad} }, .{});
} }
@@ -455,7 +453,7 @@ fn getBoxModel(cmd: anytype) !void {
// TODO implement for document or text // TODO implement for document or text
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement; 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 quad = rectToQuad(rect);
const zero = [_]f64{0.0} ** 8; const zero = [_]f64{0.0} ** 8;