fix: improve range parsing performance - #899
Open
martinrrm wants to merge 1 commit into
Open
Conversation
Replace comparator whitespace normalization with a linear string scanner while preserving existing parsing behavior. Add performance and compatibility coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
martinrrm
force-pushed
the
martinrrm/improve-range-parsing-performance
branch
from
September 1, 2026 20:50
4f4903c to
4bdfb83
Compare
dhei
approved these changes
Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change replaces the internal use of the global
COMPARATORTRIMregular expression inRange.parseRange()with a linear string scanner.The scanner preserves the existing comparator-normalization behavior, including loose parsing, unlimited leading
v/=compatibility, build-metadata stripping, and unusual whitespace combinations. Public APIs and exported regex definitions remain unchanged.Background
Range parsing currently normalizes whitespace between comparison operators and version expressions with:
COMPARATORTRIMcombines several large regex fragments and runs globally against the complete range. Inputs containing long sequences of characters accepted by the version prefix can cause the expression to repeatedly scan overlapping portions of the input.This work grows substantially faster than the input size, even when the range is ultimately invalid.
The normalization performed by
COMPARATORTRIMis comparatively small: it removes whitespace between a comparison operator and the version expression that follows it. Performing that operation with a string scanner avoids repeatedly invoking the complete range grammar.Implementation
Range.parseRange()now calls:The scanner:
x,X, or*.v,=, and whitespace prefix.<,<=,>,>=,=, or empty comparator.The implementation records removals rather than modifying the string during iteration, keeping indexes stable when a range contains multiple comparators.
It also mirrors the existing safe-regex whitespace behavior after build metadata has been removed. This matters because build stripping can create consecutive spaces, and the previous implementation removed at most one comparator-adjacent space during each match.
Why change the
COMPARATORTRIMcall site?LOOSEPLAIN,XRANGEPLAIN, andXRANGEPLAINLOOSEare shared parsing fragments used by many expressions. Their normal uses are anchored and only scan an input from one starting position.The expensive behavior came from composing those fragments into
COMPARATORTRIM, which is global and unanchored. That expression could retry the same prefix matching from every input position.Replacing the internal
COMPARATORTRIMoperation addresses the point where the repeated work occurs without changing the shared grammar.This approach also avoids changing:
LOOSEPLAINXRANGEPLAINXRANGEPLAINLOOSECOMPARATORTRIMsemver.resemver.srcvand=prefixesThe low-level regex definitions remain available with their existing values. Only the internal normalization performed by
Rangechanges implementation.Compatibility
This change is intended to preserve existing parsing behavior.
In particular, previously accepted inputs remain accepted:
The implementation also preserves existing strict and loose behavior for unusual cases involving equality, whitespace, and stripped build metadata:
No public API signatures, return types, canonical range results, or exception messages are intentionally changed.
Performance
Representative measurements for an invalid repeated-prefix input:
The new implementation scales linearly instead of repeatedly scanning overlapping suffixes.
A workload containing 20,000 ordinary unique ranges also showed no regression:
Very large valid prefixes incur a small linear scanning cost. A valid range with a 500,000-character prefix completed in approximately 10 ms, compared with approximately 5 ms previously, while producing the same result.
Validation
validRange()comparisons produced no differences from the previous implementation.Rangeresults, raw values, and thrown error messages produced no differences.npm-package-arginputs retained identicalversion,range, andtagclassifications.RangevalidRangesatisfiesminVersionminSatisfyingmaxSatisfying