Repository navigation
perf: hoist per-call regular expressions in hot validation paths - #569
Merged
Merged
Conversation
Regex literals inside a function body allocate a fresh RegExp object on every evaluation. Move the ones on hot validation paths to module scope: `Hex.assert` (strict), `Value.format`/`Value.from`, `TypedData` type dependency and field validation, `Siwe.isUri`, frame nonce RPC decoding, and the `ox/zod` Hex, Address, quantity and Solidity type schemas. All of them are non-global and non-sticky, so sharing them holds no `lastIndex` state. `isArgOfType` also reads the integer bit size from the `integerTypeRegex` capture instead of running a second `replace` regex.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Published pnpm add ox@pr-569
pnpm add ox@0.0.0-pr-569-0b24e69 |
Merged
gakonst
added a commit
that referenced
this pull request
Nov 3, 2026
Backports #569 and #570 from main. #569: Regex literals inside a function body allocate a fresh RegExp on every evaluation. Move the ones on hot validation paths to module scope: `Hex.assert` (strict), `Value.format`/`Value.from`, `TypedData` type dependency and field validation, `Siwe.isUri`, frame nonce RPC decoding, and the integer and fixed-bytes checks in ABI overload resolution. All of them are non-global and non-sticky, so sharing them holds no `lastIndex` state. `isArgOfType` also reads the integer bit size from the integer type regex capture instead of running a second `replace` regex. 0.x has no `ox/zod` entrypoint, and keeps its own `Value.from` decimal pattern, so behavior is unchanged. #570: `AbiItem.getSignatureHash` keccak-hashed the signature on every call for ABI items without a precomputed `hash`. Memoize the hash in a bounded (8192-entry) LRU map keyed by the normalized signature string, next to the checksum cache in a new `internal/caches.ts`, so `Caches.clear()` resets it. Only `Caches.checksum` stays public. 0.x has no `Engine`, so keccak256 cannot be swapped and the engine test is not ported.
This branch had an error being deployed
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.
A regular expression literal inside a function body creates a new
RegExpobject every time it is evaluated. This PR moves the remaining per-call literals on validation paths to module scope, the same changeinternal/abiItem.tsandinternal/abiParameters.tsalready made for their regexes. All of the moved expressions are non-global and non-sticky.test,matchandreplacenever writelastIndexon them, so sharing one instance carries no state between calls.Changes
Hex.assert(strict mode, also used byHash.validate,Frame,FrameSignature,TxEnvelopeEip8141): hoisted the hex character regex.Value.format/Value.from: hoisted the decimal-number and trailing-zeros regexes.TypedData: hoisted the array-suffix regex (findTypeDependencies,validateField) and the struct-name regexes (validateReference).Siwe.isUri(also used bySiwe.createMessageforuriand each resource): hoisted its 9 regexes, including the ones inisAuthorityandsplitUri. Siwe's exported regexes are unchanged, and the new constants are not exported.internal/frameNonce.fromRpc: hoisted the canonical quantity regex (evaluated once per nonce key).ox/zod: hoisted therefineregexes inHex(every hex field of every decoded RPC object),Address, theFrameRequestquantity, and theSoliditytype schemas andisType.internal/abiItem.isArgOfType: reads the integer bit size from theintegerTypeRegexmatch it already runs, instead of a secondreplace(/^u?int/, '')regex.isArgOfTypecases for the bareuint/intbounds, which exercise the default-256 branch of that change.No public API or type changes.
Measurements
replace(/(0+)$/), and 42.1–44.2 vs 38.6–39.5 ns forreplace(/(\[[0-9]*\])+$/). GC scavenges during each run dropped from 95–766 to 0–21. So each call saves one allocation and about 3 ns.Hash.validate,Value.format,TypedData.encodeType/validate,Siwe.createMessage, andox/zodHex/Addressarrays of 64): base and patched results were within noise of each other. The machine running them had a load average of 80–115, so the change is a per-call allocation removal, not a measurable speedup of any single function.Verification
pnpm check:types(tsc -b) passed.vp check(format and type-aware lint) passed on all changed files.SKIP_GLOBAL_SETUP=1 pnpm test --project coreon the touched modules passed:Hex,Value,TypedData,Siwe,AbiItem,Hash,Frame,FrameRpc,FrameRequest,FrameSignatureand all ofsrc/zod. That is 62 files and 922 tests.AbiEventandTransactionRequestfailed to connect (ECONNREFUSED 127.0.0.1:8545). Thetmp_framesproject needs its frames RPC node. CI runs both with those services.vp check --fixover the whole repo did not finish within its time limit on the loaded machine, so I ran the same check on the changed files and committed with the hook skipped.Considered and not changed
AbiItem.fromAbiwith a selector hashes the signature of every unprepared ABI item on each call. Caching hashes per ABI item object (for example in aWeakMap) would be a much larger win for decoding against large ABIs. It would also add a new global cache, which goes against theCaches/Engineregistry rules, and it would return stale results if an ABI item is mutated. That is left for maintainers to decide.virtualMasterPool.resolveNode: the two dynamic imports run once and one is a Node builtin, so overlapping them saves nothing.internal/ens.ts(a/gmregex) andtempo/Tick.ts: these are not hot, so I left them as they are.