Introduce StyleManager

A Page now has a StyleManager. The StyleManager currently answers two questions:
1 - Is an element hidden
2 - Does an element have pointer-events == none

This is used in calls such as element.checkVisibility which, on some pages, can
be called tens of thousands of times (often through other methods, like
element.getBoundingClientRect). This _can_ be a bottleneck.

The StyleManager keeps a list of rules. The rules include the selector,
specificity, and properties that we care about. Rules in a stylesheet that
contain no properties of interest are ignored. This is the first and likely
most significant optimization. Presumably, most CSS rules don't have a
display/visibility/opacity or pointer-events property.

The list is rules is cached until stylesheets are modified or delete. When this
happens, the StyleManager is flagged as "dirty" and rebuilt on-demand in the
next query.  This is our second major optimization.

For now, to check if an element is visible, we still need to scan all rules.
But having a pre-build subset of all the rules is a first step.

The next step might be to optimize the matching, or possibly optimizing common
cases (e.g. id and/or simple class selector)
This commit is contained in:
Karl Seguin
2026-03-17 13:40:51 +08:00
parent 43a70272c5
commit e29778d72b
12 changed files with 922 additions and 203 deletions

View File

@@ -77,10 +77,11 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse {
const wrapped = String.wrap(normalized);
const prop = self.findProperty(wrapped) orelse {
// Only return default values for computed styles
if (self._is_computed) {
return getDefaultPropertyValue(self, normalized);
return getDefaultPropertyValue(self, wrapped);
}
return "";
};
@@ -89,7 +90,7 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
const prop = self.findProperty(.wrap(normalized)) orelse return "";
return if (prop._important) "important" else "";
}
@@ -120,7 +121,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
// Find existing property
if (self.findProperty(normalized)) |existing| {
if (self.findProperty(.wrap(normalized))) |existing| {
existing._value = try String.init(page.arena, normalized_value, .{});
existing._important = important;
return;
@@ -144,7 +145,7 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, pag
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
const prop = self.findProperty(.wrap(normalized)) orelse return "";
// the value might not be on the heap (it could be inlined in the small string
// optimization), so we need to dupe it.
@@ -208,11 +209,11 @@ pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
}
}
fn findProperty(self: *const CSSStyleDeclaration, name: []const u8) ?*Property {
pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property {
var node = self._properties.first;
while (node) |n| {
const prop = Property.fromNodeLink(n);
if (prop._name.eqlSlice(name)) {
if (prop._name.eql(name)) {
return prop;
}
node = n.next;
@@ -617,26 +618,36 @@ fn isLengthProperty(name: []const u8) bool {
return length_properties.has(name);
}
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
if (std.mem.eql(u8, normalized_name, "visibility")) {
return "visible";
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []const u8 {
switch (name.len) {
5 => {
if (name.eql(comptime .wrap("color"))) {
const element = self._element orelse return "";
return getDefaultColor(element);
}
},
7 => {
if (name.eql(comptime .wrap("opacity"))) {
return "1";
}
if (name.eql(comptime .wrap("display"))) {
const element = self._element orelse return "";
return getDefaultDisplay(element);
}
},
10 => {
if (name.eql(comptime .wrap("visibility"))) {
return "visible";
}
},
16 => {
if (name.eqlSlice("background-color")) {
// transparent
return "rgba(0, 0, 0, 0)";
}
},
else => {},
}
if (std.mem.eql(u8, normalized_name, "opacity")) {
return "1";
}
if (std.mem.eql(u8, normalized_name, "display")) {
const element = self._element orelse return "";
return getDefaultDisplay(element);
}
if (std.mem.eql(u8, normalized_name, "color")) {
const element = self._element orelse return "";
return getDefaultColor(element);
}
if (std.mem.eql(u8, normalized_name, "background-color")) {
// transparent
return "rgba(0, 0, 0, 0)";
}
return "";
}