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;
}
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);
}

View File

@@ -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;
}

View File

@@ -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,26 +947,31 @@ 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);
if (el.getStyle(page)) |style| {
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) {
return false;
}
}
current = el.parentElement();
}
return true;
}
fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, height: f64 } {
const style = try self.getStyle(page);
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();
var width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0;
var height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0;
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, .{});

View File

@@ -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);

View File

@@ -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;