From 2d10997afab5e9cf04357fcb37b0f4ef2cc17a9d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 18:14:01 +0000 Subject: [PATCH 1/2] perf: cut redundant allocations and scans on hot parsing paths Profiled with --prof on Tailwind's dist CSS (3.5MB, ~560k nodes) to find real hot spots rather than guessing: - ValueNodeParser.is_whitespace_inline() scanned every character of a token to check if it was all-whitespace. The tokenizer already emits whitespace runs as a single TOKEN_WHITESPACE token (comments never end up inside one - verified the tokenizer splits a whitespace run around a comment into two separate TOKEN_WHITESPACE tokens), so a direct token_type check is behaviorally identical and O(1) instead of O(n). This runs on every token during value/function/media-feature parsing. - DIMENSION nodes' .value and .unit getters each called parse_dimension(), which allocates an object plus two substrings, to get half of what they needed. Split out dimension_number_end() (the split-point scan, no allocation) so each getter slices only its own part. - parse_function_node() (every var()/calc()/rgb()/url()/... in a value) and parse-selector.ts's parenthesized-pseudo-class handling (:not(), :is(), :nth-child(), ...) each allocated a substring just to compare it against a handful of literal function/pseudo names. Added str_equals_range() for allocation-free offset-based comparison and used it in both places. Added a regression test for comments between value tokens and function arguments, the scenario the whitespace-check change depends on. All 1428 existing + new tests pass; no API changes. Co-Authored-By: Claude Sonnet 5 --- src/css-node.ts | 10 +++++++--- src/parse-dimension.ts | 17 ++++++++++++++--- src/parse-selector.ts | 29 ++++++++++++++--------------- src/parse-value.test.ts | 23 +++++++++++++++++++++++ src/string-utils.ts | 28 ++++++++++++++++++++++++++++ src/value-node-parser.ts | 29 +++++++++++++++-------------- 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/src/css-node.ts b/src/css-node.ts index e2dac57..6544a3d 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -70,7 +70,7 @@ import { str_starts_with, str_equals, } from './string-utils' -import { parse_dimension } from './parse-dimension' +import { dimension_number_end } from './parse-dimension' // Type name lookup table - maps numeric type to CSSTree-compatible strings export const TYPE_NAMES = { @@ -401,7 +401,10 @@ export class CSSNode { } if (type === DIMENSION) { - return parse_dimension(text).value + // Slice only the numeric part — avoids parse_dimension's extra unit substring + object + let num_str = text.substring(0, dimension_number_end(text)) + // oxlint-disable-next-line prefer-number-coercion + return num_str ? Number.parseFloat(num_str) : 0 } if (type === NUMBER) { @@ -523,7 +526,8 @@ export class CSSNode { /** Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") */ get unit(): string | undefined { if (this.type !== DIMENSION) return undefined - return parse_dimension(this.text).unit + let text = this.text + return text.substring(dimension_number_end(text)) } /** Numerator for ratio values, e.g. the Number "16" in `aspect-ratio: 16/9` */ diff --git a/src/parse-dimension.ts b/src/parse-dimension.ts index cf8f47f..059dd41 100644 --- a/src/parse-dimension.ts +++ b/src/parse-dimension.ts @@ -1,8 +1,13 @@ import { is_digit, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_PERIOD } from './string-utils' -/** Parse a dimension string into value and unit, e.g. "100px" → { value: 100, unit: "px" } or "2%" → { value: 2, unit: "%" } */ -export function parse_dimension(text: string): { value: number; unit: string } { - // Find where the numeric part ends +/** + * Find where a dimension's numeric part ends, e.g. 3 for "100px" or 2 for "2%". + * No allocation — callers that only need `value` or only `unit` can slice just + * the part they want instead of going through `parse_dimension`'s object + two + * substrings for both. + * @internal + */ +export function dimension_number_end(text: string): number { let num_end = 0 for (let i = 0; i < text.length; i++) { let ch = text.charCodeAt(i) @@ -39,6 +44,12 @@ export function parse_dimension(text: string): { value: number; unit: string } { } } + return num_end +} + +/** Parse a dimension string into value and unit, e.g. "100px" → { value: 100, unit: "px" } or "2%" → { value: 2, unit: "%" } */ +export function parse_dimension(text: string): { value: number; unit: string } { + let num_end = dimension_number_end(text) let num_str = text.substring(0, num_end) let unit = text.substring(num_end) // oxlint-disable-next-line prefer-number-coercion diff --git a/src/parse-selector.ts b/src/parse-selector.ts index a51ec23..d62419b 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -43,7 +43,7 @@ import { } from './parse-utils' import { is_whitespace, - str_equals, + str_equals_range, CHAR_PLUS, CHAR_TILDE, CHAR_GREATER_THAN, @@ -791,16 +791,15 @@ export class SelectorParser { // Parse the content inside the parentheses if (content_end > content_start) { - // Check if this is an nth-* pseudo-class - let func_name_substr = this.source.substring(func_name_start, func_name_end) - - if (this.is_nth_pseudo(func_name_substr)) { + // Check if this is an nth-* pseudo-class (offset-based — avoids allocating a + // substring for every parenthesized pseudo-class, e.g. :not(), :is(), :nth-child()) + if (this.is_nth_pseudo(func_name_start, func_name_end)) { // Parse as An+B expression let child = this.parse_nth_expression(content_start, content_end) if (child !== null) { this.arena.set_first_child(node, child) } - } else if (str_equals('lang', func_name_substr)) { + } else if (str_equals_range(this.source, func_name_start, func_name_end, 'lang')) { // Parse as :lang() - comma-separated language identifiers this.parse_lang_identifiers(content_start, content_end, node) } else { @@ -811,7 +810,7 @@ export class SelectorParser { // Recursively parse the content as a selector // Only :has() accepts relative selectors (starting with combinator) - let allow_relative = str_equals('has', func_name_substr) + let allow_relative = str_equals_range(this.source, func_name_start, func_name_end, 'has') let child_selector = this.parse_selector( content_start, content_end, @@ -834,15 +833,15 @@ export class SelectorParser { return node } - // Check if pseudo-class name is an nth-* pseudo - private is_nth_pseudo(name: string): boolean { + // Check if pseudo-class name (source[start, end)) is an nth-* pseudo + private is_nth_pseudo(start: number, end: number): boolean { return ( - str_equals('nth-child', name) || - str_equals('nth-last-child', name) || - str_equals('nth-of-type', name) || - str_equals('nth-last-of-type', name) || - str_equals('nth-col', name) || - str_equals('nth-last-col', name) + str_equals_range(this.source, start, end, 'nth-child') || + str_equals_range(this.source, start, end, 'nth-last-child') || + str_equals_range(this.source, start, end, 'nth-of-type') || + str_equals_range(this.source, start, end, 'nth-last-of-type') || + str_equals_range(this.source, start, end, 'nth-col') || + str_equals_range(this.source, start, end, 'nth-last-col') ) } diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index 1588c24..d2b6912 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -1013,6 +1013,29 @@ describe('Value Node Types', () => { expect((decl!.first_child! as Value).children[0].text).toBe('red') expect(decl?.is_important).toBe(true) }) + + test('should skip comments between value tokens', () => { + const root = parse('body { margin: 10px /* comment */ 20px; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + + const children = (decl!.first_child! as Value).children + expect(children).toHaveLength(2) + expect(children[0].type).toBe(DIMENSION) + expect(children[0].text).toBe('10px') + expect(children[1].type).toBe(DIMENSION) + expect(children[1].text).toBe('20px') + }) + + test('should skip comments between function arguments', () => { + const root = parse('body { color: rgb(255 /* red */, 0, /* green */ 0); }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const func = (decl!.first_child! as Value).children[0] as Function + + expect(func.type).toBe(FUNCTION) + const args = func.children + expect(args.map((n) => n.type)).toEqual([NUMBER, OPERATOR, NUMBER, OPERATOR, NUMBER]) + expect(args.map((n) => n.text)).toEqual(['255', ',', '0', ',', '0']) + }) }) }) diff --git a/src/string-utils.ts b/src/string-utils.ts index d6b04f4..616b02c 100644 --- a/src/string-utils.ts +++ b/src/string-utils.ts @@ -77,6 +77,34 @@ export function str_equals(a: string, b: string): boolean { return true } +/** + * Case-insensitive equality between `source[start, end)` and `literal`, without allocating + * a substring first. Same semantics as `str_equals(literal, source.substring(start, end))`. + * `literal` MUST be lowercase. + */ +export function str_equals_range( + source: string, + start: number, + end: number, + literal: string, +): boolean { + if (end - start !== literal.length) { + return false + } + + for (let i = 0; i < literal.length; i++) { + let ch = source.charCodeAt(start + i) + // normalize ASCII uppercase A-Z → a-z + ch |= 32 + + if (ch !== literal.charCodeAt(i)) { + return false + } + } + + return true +} + /** Case-insensitive ASCII prefix check without allocations. `prefix` MUST be lowercase. */ export function str_starts_with(str: string, prefix: string): boolean { if (str.length < prefix.length) { diff --git a/src/value-node-parser.ts b/src/value-node-parser.ts index bf77f68..ad3615d 100644 --- a/src/value-node-parser.ts +++ b/src/value-node-parser.ts @@ -35,15 +35,16 @@ import { TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_UNICODE_RANGE, + TOKEN_WHITESPACE, type TokenType, } from './token-types' import { - is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_ASTERISK, CHAR_FORWARD_SLASH, str_equals, + str_equals_range, } from './string-utils' import { ConditionParser } from './parse-condition' @@ -123,15 +124,12 @@ export class ValueNodeParser { return first_node } - // Helper to check if token is all whitespace (inline for hot paths) + // Helper to check if the current token is a whitespace run. next_token_fast(false) + // always tokenizes consecutive whitespace as a single TOKEN_WHITESPACE token (it only + // skips whitespace inline when called with skip_whitespace=true), so a direct type + // check is equivalent to scanning the token's characters but O(1) instead of O(n). private is_whitespace_inline(): boolean { - if (this.lexer.token_start >= this.lexer.token_end) return false - for (let i = this.lexer.token_start; i < this.lexer.token_end; i++) { - if (!is_whitespace(this.source.charCodeAt(i))) { - return false - } - } - return true + return this.lexer.token_type === TOKEN_WHITESPACE } private parse_value_node(): number | null { @@ -248,17 +246,20 @@ export class ValueNodeParser { // The lexer's TOKEN_FUNCTION includes the '(' at the end let name_end = end - 1 // Exclude the '(' - // Get function name to check for special handling - let func_name_substr = this.source.substring(start, name_end) + // Check function name for special handling without allocating a substring — most + // function calls (calc(), var(), rgb(), translate(), ...) aren't any of these, and + // this runs once per FUNCTION token, one of the most common tokens in a value. // Dispatch to dedicated parser for if() - if (str_equals('if', func_name_substr)) { + if (str_equals_range(this.source, start, name_end, 'if')) { return this.parse_if_function_node(start, end) } + let is_url = str_equals_range(this.source, start, name_end, 'url') + // Create URL or function node based on function name (length will be set later) let node = this.arena.create_node( - str_equals('url', func_name_substr) ? URL : FUNCTION, + is_url ? URL : FUNCTION, start, 0, // length unknown yet this.lexer.token_line, @@ -271,7 +272,7 @@ export class ValueNodeParser { // Don't parse contents to preserve URLs with dots, base64, inline SVGs, etc. // Users can extract the full URL from the function's text property // Note: Quoted urls like url("...") or url('...') parse normally - if (str_equals('url', func_name_substr) || str_equals('src', func_name_substr)) { + if (is_url || str_equals_range(this.source, start, name_end, 'src')) { // Peek at the next token to see if it's a string // If it's a string, parse normally. Otherwise, skip parsing children. let save_pos = this.lexer.save_position() From d0b7406ed521d65b5f5c606df7c7dcea65c11e0f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 18:40:22 +0000 Subject: [PATCH 2/2] perf: inline is_whitespace_inline() call sites in value parsing is_whitespace_inline() was reduced to a single field comparison last commit, so the wrapping method call was pure overhead on a path that runs for every token during value parsing. Inlined all 9 call sites as direct token_type === TOKEN_WHITESPACE checks, reusing the local token_type/tt/t variable already captured in scope where one exists instead of re-reading the field through this.lexer. Also fixes a real TS narrowing conflict this surfaced: two spots read this.lexer.token_type directly right after assigning it to a local (t) and checking that local, which left TS unable to prove the raw field read could still be TOKEN_WHITESPACE. Using the local everywhere sidesteps it and matches the surrounding code's existing style. All 1428 tests pass; no behavior change. Co-Authored-By: Claude Sonnet 5 --- src/value-node-parser.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/value-node-parser.ts b/src/value-node-parser.ts index ad3615d..2cf780b 100644 --- a/src/value-node-parser.ts +++ b/src/value-node-parser.ts @@ -104,7 +104,7 @@ export class ValueNodeParser { if (token_type === TOKEN_EOF) break // Skip whitespace tokens (they're separators, not value nodes) - if (this.is_whitespace_inline()) { + if (token_type === TOKEN_WHITESPACE) { continue } @@ -124,14 +124,6 @@ export class ValueNodeParser { return first_node } - // Helper to check if the current token is a whitespace run. next_token_fast(false) - // always tokenizes consecutive whitespace as a single TOKEN_WHITESPACE token (it only - // skips whitespace inline when called with skip_whitespace=true), so a direct type - // check is equivalent to scanning the token's characters but O(1) instead of O(n). - private is_whitespace_inline(): boolean { - return this.lexer.token_type === TOKEN_WHITESPACE - } - private parse_value_node(): number | null { let token_type = this.lexer.token_type let start = this.lexer.token_start @@ -279,7 +271,7 @@ export class ValueNodeParser { this.lexer.next_token_fast(false) // Skip whitespace - while (this.is_whitespace_inline() && this.lexer.pos < this.end) { + while (this.lexer.token_type === TOKEN_WHITESPACE && this.lexer.pos < this.end) { this.lexer.next_token_fast(false) } @@ -346,7 +338,7 @@ export class ValueNodeParser { } // Skip whitespace - if (this.is_whitespace_inline()) continue + if (token_type === TOKEN_WHITESPACE) continue // Parse argument node let arg_node = this.parse_value_node() @@ -415,7 +407,7 @@ export class ValueNodeParser { } // Skip whitespace and any stray separators between branches - if (this.is_whitespace_inline() || tt === TOKEN_SEMICOLON || tt === TOKEN_COLON) continue + if (tt === TOKEN_WHITESPACE || tt === TOKEN_SEMICOLON || tt === TOKEN_COLON) continue // ── Condition ────────────────────────────────────────────────────── let branch_start = this.lexer.token_start @@ -435,7 +427,7 @@ export class ValueNodeParser { let t = this.lexer.token_type if (t === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break - if (this.is_whitespace_inline()) continue + if (t === TOKEN_WHITESPACE) continue if (t === TOKEN_COLON) { colon_found = true break @@ -463,7 +455,7 @@ export class ValueNodeParser { let t = this.lexer.token_type if (t === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break - if (this.is_whitespace_inline()) continue + if (t === TOKEN_WHITESPACE) continue if (t === TOKEN_SEMICOLON) break // end of this branch @@ -544,7 +536,7 @@ export class ValueNodeParser { while (this.lexer.pos < this.end) { this.lexer.next_token_fast(false) if (this.lexer.token_start >= this.end) return TOKEN_EOF - if (this.is_whitespace_inline()) continue + if (this.lexer.token_type === TOKEN_WHITESPACE) continue return this.lexer.token_type } return TOKEN_EOF @@ -764,7 +756,7 @@ export class ValueNodeParser { if (this.lexer.token_start >= this.end) break let token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break - if (this.is_whitespace_inline()) continue + if (token_type === TOKEN_WHITESPACE) continue let node = this.parse_value_node() if (node !== null) nodes.push(node) } @@ -811,7 +803,7 @@ export class ValueNodeParser { } // Skip whitespace - if (this.is_whitespace_inline()) continue + if (token_type === TOKEN_WHITESPACE) continue // Parse child node // Note: We don't track paren_depth for LEFT_PAREN or TOKEN_FUNCTION here