From 448eca0c32f919ef4be826cb39af34aca1916ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 12:02:48 +0900 Subject: [PATCH 1/5] StyleManager: optimize rule evaluation using SoA and early rejection --- src/browser/StyleManager.zig | 151 +++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 62 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 5582d23a..a7c8bb21 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -45,7 +45,7 @@ pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool); const StyleManager = @This(); const Tag = Element.Tag; -const RuleList = std.ArrayList(VisibilityRule); +const RuleList = std.MultiArrayList(VisibilityRule); page: *Page, @@ -103,22 +103,23 @@ fn rebuildIfDirty(self: *StyleManager) !void { const id_rules_count = self.id_rules.count(); const class_rules_count = self.class_rules.count(); const tag_rules_count = self.tag_rules.count(); - const other_rules_count = self.other_rules.items.len; + const other_rules_count = self.other_rules.len; self.page._session.arena_pool.resetRetain(self.arena); self.next_doc_order = 0; self.id_rules = .empty; - try self.id_rules.ensureUnusedCapacity(self.arena, id_rules_count); + try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count); self.class_rules = .empty; - try self.class_rules.ensureUnusedCapacity(self.arena, class_rules_count); + try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count); self.tag_rules = .empty; - try self.tag_rules.ensureUnusedCapacity(self.arena, tag_rules_count); + try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count); - self.other_rules = try .initCapacity(self.arena, other_rules_count); + self.other_rules = .{}; + try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count); const sheets = self.page.document._style_sheets orelse return; for (sheets._sheets.items) |sheet| { @@ -243,28 +244,52 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return spec > best_spec or (spec == best_spec and doc_order > best_doc_order); } - fn checkRule(ctx: @This(), rule: VisibilityRule) void { - // Skip rules that can't possibly beat current best for any property - const dominated = (rule.props.display_none == null or !beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and - (rule.props.visibility_hidden == null or !beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and - (rule.props.opacity_zero == null or !beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)); - if (dominated) return; + fn checkRules(ctx: @This(), rules: *const RuleList) void { + if (ctx.display_spec.* == INLINE_SPECIFICITY and + ctx.visibility_spec.* == INLINE_SPECIFICITY and + ctx.opacity_spec.* == INLINE_SPECIFICITY) + { + return; + } - if (matchesSelector(ctx.el, rule.selector, ctx.page)) { - if (rule.props.display_none != null and beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) { - ctx.display_none.* = rule.props.display_none; - ctx.display_spec.* = rule.specificity; - ctx.display_doc_order.* = rule.doc_order; + const len = rules.len; + const specificities = rules.items(.specificity); + const doc_orders = rules.items(.doc_order); + + for (0..len) |i| { + const spec = specificities[i]; + const doc_order = doc_orders[i]; + + if (!beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*) and + !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*) and + !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) + { + continue; } - if (rule.props.visibility_hidden != null and beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) { - ctx.visibility_hidden.* = rule.props.visibility_hidden; - ctx.visibility_spec.* = rule.specificity; - ctx.visibility_doc_order.* = rule.doc_order; - } - if (rule.props.opacity_zero != null and beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) { - ctx.opacity_zero.* = rule.props.opacity_zero; - ctx.opacity_spec.* = rule.specificity; - ctx.opacity_doc_order.* = rule.doc_order; + + const props = rules.items(.props)[i]; + const dominated = (props.display_none == null or !beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and + (props.visibility_hidden == null or !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and + (props.opacity_zero == null or !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)); + if (dominated) continue; + + const selector = rules.items(.selector)[i]; + if (matchesSelector(ctx.el, selector, ctx.page)) { + if (props.display_none != null and beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) { + ctx.display_none.* = props.display_none; + ctx.display_spec.* = spec; + ctx.display_doc_order.* = doc_order; + } + if (props.visibility_hidden != null and beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) { + ctx.visibility_hidden.* = props.visibility_hidden; + ctx.visibility_spec.* = spec; + ctx.visibility_doc_order.* = doc_order; + } + if (props.opacity_zero != null and beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) { + ctx.opacity_zero.* = props.opacity_zero; + ctx.opacity_spec.* = spec; + ctx.opacity_doc_order.* = doc_order; + } } } } @@ -285,9 +310,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (self.id_rules.get(id)) |rules| { - for (rules.items) |rule| { - ctx.checkRule(rule); - } + ctx.checkRules(&rules); } } @@ -295,22 +318,16 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); while (it.next()) |class| { if (self.class_rules.get(class)) |rules| { - for (rules.items) |rule| { - ctx.checkRule(rule); - } + ctx.checkRules(&rules); } } } if (self.tag_rules.get(el.getTag())) |rules| { - for (rules.items) |rule| { - ctx.checkRule(rule); - } + ctx.checkRules(&rules); } - for (self.other_rules.items) |rule| { - ctx.checkRule(rule); - } + ctx.checkRules(&self.other_rules); return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false); } @@ -366,28 +383,44 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { var best_doc_order: u32 = 0; // Helper to check a single rule - const checkRule = struct { + const checkRules = struct { fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool { return spec > b_spec or (spec == b_spec and doc_order > b_doc_order); } - fn check(rule: VisibilityRule, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void { - if (rule.props.pointer_events_none == null or !beats(rule.specificity, rule.doc_order, spec.*, doc_order.*)) { - return; - } - if (matchesSelector(elem, rule.selector, p)) { - res.* = rule.props.pointer_events_none; - spec.* = rule.specificity; - doc_order.* = rule.doc_order; + fn check(rules: *const RuleList, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void { + if (spec.* == INLINE_SPECIFICITY) return; + + const len = rules.len; + const specificities = rules.items(.specificity); + const doc_orders = rules.items(.doc_order); + + for (0..len) |i| { + const rule_spec = specificities[i]; + const rule_doc_order = doc_orders[i]; + + if (!beats(rule_spec, rule_doc_order, spec.*, doc_order.*)) { + continue; + } + + const props = rules.items(.props)[i]; + if (props.pointer_events_none == null) { + continue; + } + + const selector = rules.items(.selector)[i]; + if (matchesSelector(elem, selector, p)) { + res.* = props.pointer_events_none; + spec.* = rule_spec; + doc_order.* = rule_doc_order; + } } } }.check; if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (self.id_rules.get(id)) |rules| { - for (rules.items) |rule| { - checkRule(rule, &result, &best_spec, &best_doc_order, el, page); - } + checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); } } @@ -395,22 +428,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); while (it.next()) |class| { if (self.class_rules.get(class)) |rules| { - for (rules.items) |rule| { - checkRule(rule, &result, &best_spec, &best_doc_order, el, page); - } + checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); } } } if (self.tag_rules.get(el.getTag())) |rules| { - for (rules.items) |rule| { - checkRule(rule, &result, &best_spec, &best_doc_order, el, page); - } + checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); } - for (self.other_rules.items) |rule| { - checkRule(rule, &result, &best_spec, &best_doc_order, el, page); - } + checkRules(&self.other_rules, &result, &best_spec, &best_doc_order, el, page); return result orelse false; } @@ -461,17 +488,17 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void { switch (bucket_key) { .id => |id| { const gop = try self.id_rules.getOrPut(self.arena, id); - if (!gop.found_existing) gop.value_ptr.* = .empty; + if (!gop.found_existing) gop.value_ptr.* = .{}; try gop.value_ptr.append(self.arena, rule); }, .class => |class| { const gop = try self.class_rules.getOrPut(self.arena, class); - if (!gop.found_existing) gop.value_ptr.* = .empty; + if (!gop.found_existing) gop.value_ptr.* = .{}; try gop.value_ptr.append(self.arena, rule); }, .tag => |tag| { const gop = try self.tag_rules.getOrPut(self.arena, tag); - if (!gop.found_existing) gop.value_ptr.* = .empty; + if (!gop.found_existing) gop.value_ptr.* = .{}; try gop.value_ptr.append(self.arena, rule); }, .other => { From 3e2be5b31742266c66c0e7af302082001ba7f2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 12:13:52 +0900 Subject: [PATCH 2/5] StyleManager: vectorize rule specificity checks with SIMD --- src/browser/StyleManager.zig | 232 +++++++++++++++++++++-------------- 1 file changed, 143 insertions(+), 89 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index a7c8bb21..72a0a322 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -166,25 +166,25 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt /// Check if a single element (not ancestors) is hidden. fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool { - // Track best match per property (value + specificity) - // Initialize spec to INLINE_SPECIFICITY for properties we don't care about - this makes - // the loop naturally skip them since no stylesheet rule can have specificity >= INLINE_SPECIFICITY + // Track best match per property (value + priority) + // Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes + // the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY var display_none: ?bool = null; - var display_spec: u32 = 0; + var display_priority: u64 = 0; var visibility_hidden: ?bool = null; - var visibility_spec: u32 = 0; + var visibility_priority: u64 = 0; var opacity_zero: ?bool = null; - var opacity_spec: u32 = 0; + var opacity_priority: u64 = 0; - // Check inline styles FIRST - they use INLINE_SPECIFICITY so no stylesheet can beat them + // Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| { if (property._value.eql(comptime .wrap("none"))) { return true; // Early exit for hiding value } display_none = false; - display_spec = INLINE_SPECIFICITY; + display_priority = INLINE_PRIORITY; } if (options.check_visibility) { @@ -193,13 +193,13 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return true; } visibility_hidden = false; - visibility_spec = INLINE_SPECIFICITY; + visibility_priority = INLINE_PRIORITY; } } else { // This can't be beat. Setting this means that, when checking rules // we no longer have to check if options.check_visibility is enabled. - // We can just compare the specificity. - visibility_spec = INLINE_SPECIFICITY; + // We can just compare the priority. + visibility_priority = INLINE_PRIORITY; } if (options.check_opacity) { @@ -208,87 +208,118 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return true; } opacity_zero = false; - opacity_spec = INLINE_SPECIFICITY; + opacity_priority = INLINE_PRIORITY; } } else { - opacity_spec = INLINE_SPECIFICITY; + opacity_priority = INLINE_PRIORITY; } - if (display_spec == INLINE_SPECIFICITY and visibility_spec == INLINE_SPECIFICITY and opacity_spec == INLINE_SPECIFICITY) { + if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) { return false; } self.rebuildIfDirty() catch return false; - // Track doc_order for tie-breaking (0 = inline style, which always wins) - var display_doc_order: u32 = 0; - var visibility_doc_order: u32 = 0; - var opacity_doc_order: u32 = 0; - // Helper to check a single rule const Ctx = struct { display_none: *?bool, - display_spec: *u32, - display_doc_order: *u32, + display_priority: *u64, visibility_hidden: *?bool, - visibility_spec: *u32, - visibility_doc_order: *u32, + visibility_priority: *u64, opacity_zero: *?bool, - opacity_spec: *u32, - opacity_doc_order: *u32, + opacity_priority: *u64, el: *Element, page: *Page, - // Returns true if (spec, doc_order) beats (best_spec, best_doc_order) - fn beats(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool { - return spec > best_spec or (spec == best_spec and doc_order > best_doc_order); - } - fn checkRules(ctx: @This(), rules: *const RuleList) void { - if (ctx.display_spec.* == INLINE_SPECIFICITY and - ctx.visibility_spec.* == INLINE_SPECIFICITY and - ctx.opacity_spec.* == INLINE_SPECIFICITY) + if (ctx.display_priority.* == INLINE_PRIORITY and + ctx.visibility_priority.* == INLINE_PRIORITY and + ctx.opacity_priority.* == INLINE_PRIORITY) { return; } const len = rules.len; - const specificities = rules.items(.specificity); - const doc_orders = rules.items(.doc_order); + const priorities = rules.items(.priority); - for (0..len) |i| { - const spec = specificities[i]; - const doc_order = doc_orders[i]; + const vec_len = std.simd.suggestVectorLength(u64) orelse 4; + var i: usize = 0; - if (!beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*) and - !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*) and - !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) - { + while (i + vec_len <= len) { + const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; + const min_priority = @min(@min(ctx.display_priority.*, ctx.visibility_priority.*), ctx.opacity_priority.*); + const min_p_vec: @Vector(vec_len, u64) = @splat(min_priority); + + const cmp = p_chunk > min_p_vec; + const any_can_beat = @reduce(.Or, cmp); + + if (!any_can_beat) { + i += vec_len; + continue; + } + + for (0..vec_len) |j| { + const idx = i + j; + const p = priorities[idx]; + + if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { + continue; + } + + const props = rules.items(.props)[idx]; + const dominated = (props.display_none == null or p <= ctx.display_priority.*) and + (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and + (props.opacity_zero == null or p <= ctx.opacity_priority.*); + + if (dominated) continue; + + const selector = rules.items(.selector)[idx]; + if (matchesSelector(ctx.el, selector, ctx.page)) { + if (props.display_none != null and p > ctx.display_priority.*) { + ctx.display_none.* = props.display_none; + ctx.display_priority.* = p; + } + if (props.visibility_hidden != null and p > ctx.visibility_priority.*) { + ctx.visibility_hidden.* = props.visibility_hidden; + ctx.visibility_priority.* = p; + } + if (props.opacity_zero != null and p > ctx.opacity_priority.*) { + ctx.opacity_zero.* = props.opacity_zero; + ctx.opacity_priority.* = p; + } + } + } + i += vec_len; + } + + // Remainder + while (i < len) : (i += 1) { + const p = priorities[i]; + + if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { continue; } const props = rules.items(.props)[i]; - const dominated = (props.display_none == null or !beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and - (props.visibility_hidden == null or !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and - (props.opacity_zero == null or !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)); + const dominated = (props.display_none == null or p <= ctx.display_priority.*) and + (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and + (props.opacity_zero == null or p <= ctx.opacity_priority.*); + if (dominated) continue; const selector = rules.items(.selector)[i]; if (matchesSelector(ctx.el, selector, ctx.page)) { - if (props.display_none != null and beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) { + if (props.display_none != null and p > ctx.display_priority.*) { ctx.display_none.* = props.display_none; - ctx.display_spec.* = spec; - ctx.display_doc_order.* = doc_order; + ctx.display_priority.* = p; } - if (props.visibility_hidden != null and beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) { + if (props.visibility_hidden != null and p > ctx.visibility_priority.*) { ctx.visibility_hidden.* = props.visibility_hidden; - ctx.visibility_spec.* = spec; - ctx.visibility_doc_order.* = doc_order; + ctx.visibility_priority.* = p; } - if (props.opacity_zero != null and beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) { + if (props.opacity_zero != null and p > ctx.opacity_priority.*) { ctx.opacity_zero.* = props.opacity_zero; - ctx.opacity_spec.* = spec; - ctx.opacity_doc_order.* = doc_order; + ctx.opacity_priority.* = p; } } } @@ -296,14 +327,11 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp }; const ctx = Ctx{ .display_none = &display_none, - .display_spec = &display_spec, - .display_doc_order = &display_doc_order, + .display_priority = &display_priority, .visibility_hidden = &visibility_hidden, - .visibility_spec = &visibility_spec, - .visibility_doc_order = &visibility_doc_order, + .visibility_priority = &visibility_priority, .opacity_zero = &opacity_zero, - .opacity_spec = &opacity_spec, - .opacity_doc_order = &opacity_doc_order, + .opacity_priority = &opacity_priority, .el = el, .page = self.page, }; @@ -379,27 +407,58 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { self.rebuildIfDirty() catch return false; var result: ?bool = null; - var best_spec: u32 = 0; - var best_doc_order: u32 = 0; + var best_priority: u64 = 0; // Helper to check a single rule const checkRules = struct { - fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool { - return spec > b_spec or (spec == b_spec and doc_order > b_doc_order); - } - - fn check(rules: *const RuleList, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void { - if (spec.* == INLINE_SPECIFICITY) return; + fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void { + if (current_priority.* == INLINE_PRIORITY) return; const len = rules.len; - const specificities = rules.items(.specificity); - const doc_orders = rules.items(.doc_order); + const priorities = rules.items(.priority); - for (0..len) |i| { - const rule_spec = specificities[i]; - const rule_doc_order = doc_orders[i]; + const vec_len = std.simd.suggestVectorLength(u64) orelse 4; + var i: usize = 0; - if (!beats(rule_spec, rule_doc_order, spec.*, doc_order.*)) { + while (i + vec_len <= len) { + const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; + const min_p_vec: @Vector(vec_len, u64) = @splat(current_priority.*); + + const cmp = p_chunk > min_p_vec; + const any_can_beat = @reduce(.Or, cmp); + + if (!any_can_beat) { + i += vec_len; + continue; + } + + for (0..vec_len) |j| { + const idx = i + j; + const priority = priorities[idx]; + + if (priority <= current_priority.*) { + continue; + } + + const props = rules.items(.props)[idx]; + if (props.pointer_events_none == null) { + continue; + } + + const selector = rules.items(.selector)[idx]; + if (matchesSelector(elem, selector, p)) { + res.* = props.pointer_events_none; + current_priority.* = priority; + } + } + i += vec_len; + } + + // Remainder + while (i < len) : (i += 1) { + const priority = priorities[i]; + + if (priority <= current_priority.*) { continue; } @@ -411,8 +470,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { const selector = rules.items(.selector)[i]; if (matchesSelector(elem, selector, p)) { res.* = props.pointer_events_none; - spec.* = rule_spec; - doc_order.* = rule_doc_order; + current_priority.* = priority; } } } @@ -420,7 +478,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (self.id_rules.get(id)) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } } @@ -428,16 +486,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); while (it.next()) |class| { if (self.class_rules.get(class)) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } } } if (self.tag_rules.get(el.getTag())) |rules| { - checkRules(&rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&rules, &result, &best_priority, el, page); } - checkRules(&self.other_rules, &result, &best_spec, &best_doc_order, el, page); + checkRules(&self.other_rules, &result, &best_priority, el, page); return result orelse false; } @@ -479,8 +537,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void { const rule = VisibilityRule{ .props = props, .selector = selector, - .specificity = computeSpecificity(selector), - .doc_order = self.next_doc_order, + .priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order), }; self.next_doc_order += 1; @@ -656,11 +713,8 @@ const VisibilityRule = struct { selector: Selector.Selector, // Single selector, not a list props: VisibilityProperties, - // Packed specificity: (id_count << 20) | (class_count << 10) | element_count - specificity: u32, - - // Document order for tie-breaking equal specificity (higher = later in document) - doc_order: u32, + // Packed priority: (specificity << 32) | doc_order + priority: u64, }; const CheckVisibilityOptions = struct { @@ -668,8 +722,8 @@ const CheckVisibilityOptions = struct { check_visibility: bool = false, }; -// Inline styles always win over stylesheets - use max u32 as sentinel -const INLINE_SPECIFICITY: u32 = std.math.maxInt(u32); +// Inline styles always win over stylesheets - use max u64 as sentinel +const INLINE_PRIORITY: u64 = std.math.maxInt(u64); fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty { const style = el.getOrCreateStyle(page) catch |err| { From 1353f76bf17cda59107223f0498f4ed7c8510c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 12:25:32 +0900 Subject: [PATCH 3/5] StyleManager: defer JS CSS rule allocation by lazy parsing --- src/browser/StyleManager.zig | 79 +++++++++++++++++++++-- src/browser/webapi/css/CSSStyleSheet.zig | 11 +++- src/browser/webapi/element/html/Style.zig | 7 +- 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 72a0a322..1b384efc 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -74,12 +74,77 @@ pub fn deinit(self: *StyleManager) void { self.page.releaseArena(self.arena); } -pub fn sheetAdded(self: *StyleManager, sheet: *CSSStyleSheet) !void { - const css_rules = sheet._css_rules orelse return; +fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void { + if (sheet._css_rules) |css_rules| { + for (css_rules._rules.items) |rule| { + const style_rule = rule.is(CSSStyleRule) orelse continue; + try self.addRule(style_rule); + } + return; + } - for (css_rules._rules.items) |rule| { - const style_rule = rule.is(CSSStyleRule) orelse continue; - try self.addRule(style_rule); + const owner_node = sheet.getOwnerNode() orelse return; + if (owner_node.is(Element.Html.Style)) |style| { + const text = try style.asNode().getTextContentAlloc(self.arena); + var it = CssParser.parseStylesheet(text); + while (it.next()) |parsed_rule| { + try self.addRawRule(parsed_rule.selector, parsed_rule.block); + } + } +} + +fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []const u8) !void { + if (selector_text.len == 0) return; + + var props = VisibilityProperties{}; + var it = CssParser.parseDeclarationsList(block_text); + while (it.next()) |decl| { + const name = decl.name; + const val = decl.value; + if (std.ascii.eqlIgnoreCase(name, "display")) { + props.display_none = std.ascii.eqlIgnoreCase(val, "none"); + } else if (std.ascii.eqlIgnoreCase(name, "visibility")) { + props.visibility_hidden = std.ascii.eqlIgnoreCase(val, "hidden") or std.ascii.eqlIgnoreCase(val, "collapse"); + } else if (std.ascii.eqlIgnoreCase(name, "opacity")) { + props.opacity_zero = std.ascii.eqlIgnoreCase(val, "0"); + } else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) { + props.pointer_events_none = std.ascii.eqlIgnoreCase(val, "none"); + } + } + + if (!props.isRelevant()) return; + + const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return; + for (selectors) |selector| { + const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first; + const bucket_key = getBucketKey(rightmost) orelse continue; + const rule = VisibilityRule{ + .props = props, + .selector = selector, + .priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order), + }; + self.next_doc_order += 1; + + switch (bucket_key) { + .id => |id| { + const gop = try self.id_rules.getOrPut(self.arena, id); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.append(self.arena, rule); + }, + .class => |class| { + const gop = try self.class_rules.getOrPut(self.arena, class); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.append(self.arena, rule); + }, + .tag => |tag| { + const gop = try self.tag_rules.getOrPut(self.arena, tag); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.append(self.arena, rule); + }, + .other => { + try self.other_rules.append(self.arena, rule); + }, + } } } @@ -123,8 +188,8 @@ fn rebuildIfDirty(self: *StyleManager) !void { const sheets = self.page.document._style_sheets orelse return; for (sheets._sheets.items) |sheet| { - self.sheetAdded(sheet) catch |err| { - log.err(.browser, "StyleManager sheetAdded", .{ .err = err }); + self.parseSheet(sheet) catch |err| { + log.err(.browser, "StyleManager parseSheet", .{ .err = err }); return err; }; } diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index 6a37ecb5..f46e35df 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -46,8 +46,17 @@ pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void { pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList { if (self._css_rules) |rules| return rules; + const rules = try CSSRuleList.init(page); self._css_rules = rules; + + if (self.getOwnerNode()) |owner| { + if (owner.is(Element.Html.Style)) |style| { + const text = try style.asNode().getTextContentAlloc(page.call_arena); + try self.replaceSync(text, page); + } + } + return rules; } @@ -89,7 +98,7 @@ pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise return page.js.local.?.resolvePromise(self); } -pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void { +pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) anyerror!void { const rules = try self.getCssRules(page); rules.clear(); diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index fc52cf38..e6cac8c3 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -95,9 +95,6 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet { const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page); self._sheet = sheet; - const text = try self.asNode().getTextContentAlloc(page.call_arena); - try sheet.replaceSync(text, page); - const sheets = try page.document.getStyleSheets(page); try sheets.add(sheet, page); @@ -106,9 +103,9 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet { pub fn styleAddedCallback(self: *Style, page: *Page) !void { // Force stylesheet initialization so rules are parsed immediately - if (self.getSheet(page) catch null) |sheet| { + if (self.getSheet(page) catch null) |_| { // Notify StyleManager about the new stylesheet - page._style_manager.sheetAdded(sheet) catch {}; + page._style_manager.sheetModified(); } // if we're planning on navigating to another page, don't trigger load event. From 35cdc3c348350ef9eb282bfcbc230ad956e8bd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 12:38:15 +0900 Subject: [PATCH 4/5] StyleManager: simplify rule evaluation by removing SIMD complexity --- src/browser/StyleManager.zig | 119 ++++------------------------------- 1 file changed, 11 insertions(+), 108 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 1b384efc..d66162f4 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -304,76 +304,25 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp return; } - const len = rules.len; const priorities = rules.items(.priority); + const props_list = rules.items(.props); + const selectors = rules.items(.selector); - const vec_len = std.simd.suggestVectorLength(u64) orelse 4; - var i: usize = 0; - - while (i + vec_len <= len) { - const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; - const min_priority = @min(@min(ctx.display_priority.*, ctx.visibility_priority.*), ctx.opacity_priority.*); - const min_p_vec: @Vector(vec_len, u64) = @splat(min_priority); - - const cmp = p_chunk > min_p_vec; - const any_can_beat = @reduce(.Or, cmp); - - if (!any_can_beat) { - i += vec_len; - continue; - } - - for (0..vec_len) |j| { - const idx = i + j; - const p = priorities[idx]; - - if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { - continue; - } - - const props = rules.items(.props)[idx]; - const dominated = (props.display_none == null or p <= ctx.display_priority.*) and - (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and - (props.opacity_zero == null or p <= ctx.opacity_priority.*); - - if (dominated) continue; - - const selector = rules.items(.selector)[idx]; - if (matchesSelector(ctx.el, selector, ctx.page)) { - if (props.display_none != null and p > ctx.display_priority.*) { - ctx.display_none.* = props.display_none; - ctx.display_priority.* = p; - } - if (props.visibility_hidden != null and p > ctx.visibility_priority.*) { - ctx.visibility_hidden.* = props.visibility_hidden; - ctx.visibility_priority.* = p; - } - if (props.opacity_zero != null and p > ctx.opacity_priority.*) { - ctx.opacity_zero.* = props.opacity_zero; - ctx.opacity_priority.* = p; - } - } - } - i += vec_len; - } - - // Remainder - while (i < len) : (i += 1) { - const p = priorities[i]; - + for (priorities, props_list, selectors) |p, props, selector| { + // Fast skip using packed u64 priority if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) { continue; } - const props = rules.items(.props)[i]; + // Logic for property dominance const dominated = (props.display_none == null or p <= ctx.display_priority.*) and (props.visibility_hidden == null or p <= ctx.visibility_priority.*) and (props.opacity_zero == null or p <= ctx.opacity_priority.*); if (dominated) continue; - const selector = rules.items(.selector)[i]; if (matchesSelector(ctx.el, selector, ctx.page)) { + // Update best priorities if (props.display_none != null and p > ctx.display_priority.*) { ctx.display_none.* = props.display_none; ctx.display_priority.* = p; @@ -479,60 +428,14 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void { if (current_priority.* == INLINE_PRIORITY) return; - const len = rules.len; const priorities = rules.items(.priority); + const props_list = rules.items(.props); + const selectors = rules.items(.selector); - const vec_len = std.simd.suggestVectorLength(u64) orelse 4; - var i: usize = 0; + for (priorities, props_list, selectors) |priority, props, selector| { + if (priority <= current_priority.*) continue; + if (props.pointer_events_none == null) continue; - while (i + vec_len <= len) { - const p_chunk: @Vector(vec_len, u64) = priorities[i..][0..vec_len].*; - const min_p_vec: @Vector(vec_len, u64) = @splat(current_priority.*); - - const cmp = p_chunk > min_p_vec; - const any_can_beat = @reduce(.Or, cmp); - - if (!any_can_beat) { - i += vec_len; - continue; - } - - for (0..vec_len) |j| { - const idx = i + j; - const priority = priorities[idx]; - - if (priority <= current_priority.*) { - continue; - } - - const props = rules.items(.props)[idx]; - if (props.pointer_events_none == null) { - continue; - } - - const selector = rules.items(.selector)[idx]; - if (matchesSelector(elem, selector, p)) { - res.* = props.pointer_events_none; - current_priority.* = priority; - } - } - i += vec_len; - } - - // Remainder - while (i < len) : (i += 1) { - const priority = priorities[i]; - - if (priority <= current_priority.*) { - continue; - } - - const props = rules.items(.props)[i]; - if (props.pointer_events_none == null) { - continue; - } - - const selector = rules.items(.selector)[i]; if (matchesSelector(elem, selector, p)) { res.* = props.pointer_events_none; current_priority.* = priority; From 1feb121ba7dc375ff66254c7b061fa0e10b07d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 20 Mar 2026 16:50:00 +0900 Subject: [PATCH 5/5] CSSStyleSheet: use explicit CSSError --- src/browser/webapi/css/CSSStyleSheet.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index f46e35df..675e87f9 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -9,6 +9,14 @@ const Parser = @import("../../css/Parser.zig"); const CSSStyleSheet = @This(); +pub const CSSError = error{ + OutOfMemory, + IndexSizeError, + WriteFailed, + StringTooLarge, + SyntaxError, +}; + _href: ?[]const u8 = null, _title: []const u8 = "", _disabled: bool = false, @@ -93,12 +101,12 @@ pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void { page._style_manager.sheetModified(); } -pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { +pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!js.Promise { try self.replaceSync(text, page); return page.js.local.?.resolvePromise(self); } -pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) anyerror!void { +pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!void { const rules = try self.getCssRules(page); rules.clear();