From 883dfc135561bd931571d77e3e9dcf8bfdc18456 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 7 Jun 2026 12:12:57 +0000 Subject: [PATCH] perf(parser): skip parse_call_expression_rest when no call follows (#23063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What In `parse_lhs_expression_or_higher`, skip `parse_call_expression_rest` unless the current token is `(` or `?.`, and inline the single-use `parse_member_expression_or_higher` so the `LeftHandSideExpression` parse reads top-to-bottom (`primary` → `member_expression` → `lhs`). ## Why `parse_member_expression_rest` already consumes every `MemberExpression` continuation (`.`, `?.`, `[]`, tagged template, TS `!` / ``). The only ways to extend a fully-parsed `MemberExpression` into an LHS are `Arguments` (`(`) or an `OptionalChain` (`?.`) — the `.`/`[]`/template extensions live under `CallExpression`, so they only apply after a call. Previously `parse_lhs_expression_or_higher` always called `parse_call_expression_rest`, whose loop re-runs `parse_member_expression_rest` (the #1 hotspot per profiling) on every primary before checking for `?.`/`(`. So every leaf expression paid for a redundant member-rest scan plus call-rest overhead. A `sample` profile showed the expression LHS chain at ~50% of parse time. ## Correctness Behavior-preserving. When the current token is neither `(` nor `?.`, `parse_call_expression_rest` is a no-op. `a()` is unaffected (`` consumed in member-rest, leaving `cur == (`). The inlined helper had a single caller and identical `span`. estree (AST + spans + tokens) byte-identical to `main`; `allocs_parser.snap` byte-identical (pure work reduction, no memory effect). ## Reference typescript-go (and TS's own JS parser) use the same shape with the same redundant member-rest re-entry — the reference leaves this unoptimized. Grammar excerpt + `parser.go` cross-reference in the PR comment. ## Benchmark Interleaved A/B, two saved release binaries, 2.5 MB expression-dense file, 12 alternating reps: - `main`: ~22.6 ms/parse (median) - this branch: ~19.7 ms/parse (median) ≈ **13% faster on expression-heavy code**; every branch run beat every main run. Neutral on mixed workloads (saving is per-primary). --- crates/oxc_parser/src/js/expression.rs | 33 +++++++++---------- .../allocs_parser.snap | 8 ++--- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index 9050fc2aca4bb..181d346d3cf43 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -728,8 +728,22 @@ impl<'a, C: Config> ParserImpl<'a, C> { pub(crate) fn parse_lhs_expression_or_higher(&mut self) -> Expression<'a> { let span = self.start_span(); let mut in_optional_chain = false; - let lhs = self.parse_member_expression_or_higher(&mut in_optional_chain); - let lhs = self.parse_call_expression_rest(span, lhs, &mut in_optional_chain); + // `MemberExpression` + let primary = self.parse_primary_expression(); + let member_expression = self.parse_member_expression_rest( + span, + primary, + &mut in_optional_chain, + /* allow_optional_chain */ true, + ); + // A fully-parsed `MemberExpression` only extends into a `LeftHandSideExpression` via + // `Arguments` (`(`) or an `OptionalChain` (`?.`); see . + // So skip `parse_call_expression_rest` (and its redundant member-rest re-scan) otherwise. + let lhs = if matches!(self.cur_kind(), Kind::LParen | Kind::QuestionDot) { + self.parse_call_expression_rest(span, member_expression, &mut in_optional_chain) + } else { + member_expression + }; if !in_optional_chain { return lhs; } @@ -763,21 +777,6 @@ impl<'a, C: Config> ParserImpl<'a, C> { } } - /// Section 13.3 Member Expression - fn parse_member_expression_or_higher( - &mut self, - in_optional_chain: &mut bool, - ) -> Expression<'a> { - let span = self.start_span(); - let lhs = self.parse_primary_expression(); - self.parse_member_expression_rest( - span, - lhs, - in_optional_chain, - /* allow_optional_chain */ true, - ) - } - /// Section 13.3 Super Call fn parse_super(&mut self) -> Expression<'a> { let span = self.start_span(); diff --git a/tasks/track_memory_allocations/allocs_parser.snap b/tasks/track_memory_allocations/allocs_parser.snap index f8d9a0f2139b4..a9df0db3996ca 100644 --- a/tasks/track_memory_allocations/allocs_parser.snap +++ b/tasks/track_memory_allocations/allocs_parser.snap @@ -1,8 +1,8 @@ File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs --------------------------------------------------------------------------------------------------------------------------- -checker.ts | 2.92 MB || 5303 | 10 || 265746 | 22860 +checker.ts | 2.92 MB || 2725 | 10 || 264366 | 22860 -App.tsx | 415.34 kB || 632 | 13 || 44517 | 3538 +App.tsx | 415.34 kB || 520 | 13 || 44464 | 3537 RadixUIAdoptionSection.jsx | 2.52 kB || 1 | 0 || 367 | 66 @@ -10,7 +10,7 @@ pdf.mjs | 567.30 kB || 518 | 67 | antd.js | 6.69 MB || 5392 | 221 || 528577 | 55355 -binder.ts | 193.08 kB || 281 | 0 || 16699 | 1476 +binder.ts | 193.08 kB || 141 | 0 || 16620 | 1476 -kitchen-sink.tsx | 732.90 kB || 5027 | 160 || 139162 | 11919 +kitchen-sink.tsx | 732.90 kB || 2991 | 160 || 138059 | 11905