Repository navigation
perf(downgrader): skip decoding and unescaping plain $refs, fast path plain objects - #85
Conversation
…th plain objects `find` now calls `decodeURIComponent` only when the ref holds a `%`, and unescapes a token only when it holds a `~`. `isRecord` checks for `Object.prototype` first, so plain objects skip `Object.keys(proto)`. Output is unchanged; 3.1 to 3.0 is about 7% faster on `$ref`-heavy docs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiDmqeN32Hyhg994YCcgxd
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused five-line optimization in the downgrader that avoids unnecessary decoding, unescaping, and prototype-key allocation while retaining the existing paths when those operations are needed. It introduces no new capability or schema/API change and has a narrow, self-contained runtime impact. You can add or adjust custom eligibility rules. Learn more. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
isRecordfast path —proto === Object.prototypeshort-circuits before the generic null-prototype check inpackages/downgrader/src/shared.ts:52, avoiding theObject.keysarray allocation for plain same-realm objects. The added branch is a subset of what the generic branch already accepted (Object.getPrototypeOf(Object.prototype) === null, zero enumerable own keys), so the predicate is unchanged.finddecode skip —packages/downgrader/src/shared.ts:196only callsdecodeURIComponentwhen the ref contains%. A string without%has no percent-escapes, so decoding is the identity; refs with a malformed%still throw and are caught asundefinedexactly as before.findunescape skip —packages/downgrader/src/shared.ts:205only unescapes~1/~0when a token contains~, andreplaceAllon those literals is a no-op otherwise.
Verified locally: pnpm test passes 252/252 (snapshot tests pin converter output, so any behavior change would surface), and uncheck packages/downgrader/src/shared.ts passes oxlint, oxfmt, and tsc.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Merging this PR will improve performance by 20%1 warning
Performance Changes
Tip Curious why performance improved? Comment Comparing |

Requested by dinwwwh · project thread
Since #69, 3.1 to 3.0 resolves the target of every kept schema
$refto read its looseness, which made$ref-heavy docs 17-35% slower in wall time. CodSpeed's instruction counts don't show it. This takes back part of that with three small changes inpackages/downgrader/src/shared.ts:findcallsdecodeURIComponentonly when the ref contains%.findunescapes~1and~0only when a token contains~.isRecordchecksproto === Object.prototypefirst, so plain objects skipObject.keys(proto), which allocated an array on every call.Output is unchanged: the 252 existing tests pass, and the before and after builds give byte-identical JSON on every doc below.
Bench
Both versions bundled with esbuild and run in one Node 22 process with
--expose-gc, gc before each timed run, order shuffled each round, median of 100-200 rounds,downgradeSpecV31ToV30:The GitHub doc shifts between -10% and +1% across runs, since time on it goes mostly to allocation and GC. This doesn't do the bigger lazy-looseness rework.
Generated by Claude Code