StyleManager: defer JS CSS rule allocation by lazy parsing

This commit is contained in:
Adrià Arrufat
2026-03-20 12:25:32 +09:00
parent 3e2be5b317
commit 1353f76bf1
3 changed files with 84 additions and 13 deletions

View File

@@ -74,12 +74,77 @@ pub fn deinit(self: *StyleManager) void {
self.page.releaseArena(self.arena); self.page.releaseArena(self.arena);
} }
pub fn sheetAdded(self: *StyleManager, sheet: *CSSStyleSheet) !void { fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
const css_rules = sheet._css_rules orelse return; 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 owner_node = sheet.getOwnerNode() orelse return;
const style_rule = rule.is(CSSStyleRule) orelse continue; if (owner_node.is(Element.Html.Style)) |style| {
try self.addRule(style_rule); 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; const sheets = self.page.document._style_sheets orelse return;
for (sheets._sheets.items) |sheet| { for (sheets._sheets.items) |sheet| {
self.sheetAdded(sheet) catch |err| { self.parseSheet(sheet) catch |err| {
log.err(.browser, "StyleManager sheetAdded", .{ .err = err }); log.err(.browser, "StyleManager parseSheet", .{ .err = err });
return err; return err;
}; };
} }

View File

@@ -46,8 +46,17 @@ pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void {
pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList { pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {
if (self._css_rules) |rules| return rules; if (self._css_rules) |rules| return rules;
const rules = try CSSRuleList.init(page); const rules = try CSSRuleList.init(page);
self._css_rules = rules; 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; return rules;
} }
@@ -89,7 +98,7 @@ pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise
return page.js.local.?.resolvePromise(self); 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); const rules = try self.getCssRules(page);
rules.clear(); rules.clear();

View File

@@ -95,9 +95,6 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page); const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
self._sheet = sheet; self._sheet = sheet;
const text = try self.asNode().getTextContentAlloc(page.call_arena);
try sheet.replaceSync(text, page);
const sheets = try page.document.getStyleSheets(page); const sheets = try page.document.getStyleSheets(page);
try sheets.add(sheet, 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 { pub fn styleAddedCallback(self: *Style, page: *Page) !void {
// Force stylesheet initialization so rules are parsed immediately // 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 // 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. // if we're planning on navigating to another page, don't trigger load event.