From ae1841c2af531c36ea1a41d1e4042838a4d73040 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 10:16:02 +0000 Subject: [PATCH] perf: reuse ANplusBParser instance and replace temp Lexers with save/restore - Move ANplusBParser from per-call allocation in parse_nth_expression() to a field on SelectorParser, initialized once in the constructor. This avoids allocating a new Lexer for every :nth-child()/:nth-of-type() etc. encountered. - Replace the temporary `new Lexer()` in SelectorParser.parse_lang_identifiers() and AtRulePreludeParser.parse_feature_value() with lexer.save_position() / restore_position(). The saved LexerPosition object is a plain struct with no heap cost beyond the object itself, and avoids re-allocating a full Lexer + re-initializing its source reference for each :lang() argument range and each media-feature value range. https://claude.ai/code/session_01CQeKNnXidD5EQVJY4xBMMp --- src/parse-atrule-prelude.ts | 9 +++------ src/parse-selector.ts | 16 ++++++---------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index f543449..57a029d 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -877,13 +877,10 @@ export class AtRulePreludeParser { // Helper: Parse feature value portion into typed nodes private parse_feature_value(start: number, end: number): number[] { - // Use a temporary lexer for this range to avoid corrupting main lexer position state - let temp_lexer = new Lexer(this.source) - temp_lexer.seek(start, this.lexer.line, this.lexer.column) + const saved_position = this.lexer.save_position() + this.lexer.seek(start, this.lexer.line, this.lexer.column) let nodes: number[] = [] - let saved_lexer = this.lexer - this.lexer = temp_lexer while (this.lexer.pos < end) { this.lexer.next_token_fast(false) @@ -904,7 +901,7 @@ export class AtRulePreludeParser { if (node !== null) nodes.push(node) } - this.lexer = saved_lexer + this.lexer.restore_position(saved_position) return nodes } diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 7cebc44..a4807a1 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -71,13 +71,14 @@ export class SelectorParser { private arena: CSSDataArena private source: string private selector_end: number + private anplusb_parser: ANplusBParser constructor(arena: CSSDataArena, source: string) { this.arena = arena this.source = source - // Create a lexer instance for selector parsing this.lexer = new Lexer(source) this.selector_end = 0 + this.anplusb_parser = new ANplusBParser(arena, source) } // Parse a selector range into selector nodes (standalone use) @@ -858,14 +859,10 @@ export class SelectorParser { // Parse :lang() content - comma-separated language identifiers // Accepts both quoted strings: :lang("en", "fr") and unquoted: :lang(en, fr) private parse_lang_identifiers(start: number, end: number, parent_node: number): void { - // Use a temporary lexer for this range to avoid corrupting main lexer position state - let temp_lexer = new Lexer(this.source) - temp_lexer.seek(start, this.lexer.line, this.lexer.column) + const saved_position = this.lexer.save_position() + this.lexer.seek(start, this.lexer.line, this.lexer.column) - // Save current parser state let saved_selector_end = this.selector_end - let saved_lexer = this.lexer - this.lexer = temp_lexer this.selector_end = end let first_child: number | null = null @@ -913,9 +910,8 @@ export class SelectorParser { this.arena.set_first_child(parent_node, first_child) } - // Restore parser state this.selector_end = saved_selector_end - this.lexer = saved_lexer + this.lexer.restore_position(saved_position) } // Parse An+B expression for nth-* pseudo-classes @@ -925,7 +921,7 @@ export class SelectorParser { // e.g., "2n+1 of .active, .disabled" let of_index = this.find_of_keyword(start, end) - let anplusb_parser = new ANplusBParser(this.arena, this.source) + let anplusb_parser = this.anplusb_parser if (of_index === -1) { // Just An+B, no "of" clause