Repository navigation
Conversation
isPrimitive runs on every value. Its last check, isSymbol, called Object.prototype.toString(...).slice(8, -1) for every object. A typeof switch plus a read of @@toStringTag gives the same answer: toString reports "Symbol" for an object only through that tag. NaN stays non-primitive, as before.
…tePath forEach no longer allocates an [key, value] pair per entry. escapeKey skips both regex replaces when the key has no dot and no backslash. validatePath scans the path once and only runs the three includes() checks (which pick the error message) when a suspicious segment exists.
Without a backslash, both path modes reduce to splitting on dots. An indexOf + slice loop does that without String#split, which V8 runs slowly on short keys from JSON.parse.
transformValue tried 4 composite and 10 simple rules in a row for every value. Each simple rule accepts one typeof only, so a switch on typeof keeps the same order and the same result. The class, symbol and custom rules are skipped when their registry is empty. A custom transformer is looked up once per value instead of three times, from a cached list instead of Object.values().
The walker copied the whole path ([...path, index]) and the ancestor list ([...objectsInThisPath, object]) for every child, and kept two maps per object (identities and seen). Now paths are linked nodes that become arrays only for objects seen twice, one map holds identity and result, an unfinished entry marks an ancestor (the cycle check), and annotations pass through a context instead of a result object per value. The exported walker() keeps its signature.
deserialize() without inPlace deep-copies the json first. copy-anything 4.0.5 calls Object.prototype.toString twice per value (is-what) and reduce()s over getOwnPropertyNames and getOwnPropertySymbols. The local copy gives the same result: arrays and plain objects are cloned (enumerable own keys and symbols, __proto__ skipped), everything else is returned by reference. copy-anything 4.1.x keeps shared references in the copy, so the output matches 4.0.5, the version pinned in package-lock.json. This branch no longer imports copy-anything. Its entry in package.json is unchanged.
Date#toISOString costs about 470 ns in V8; the same string built from getUTC* calls costs about 128 ns. new Date(string) costs about 206 ns; Date.UTC on the parsed digits costs about 151 ns. Both fall back to the native call outside the strict YYYY-MM-DDTHH:mm:ss.sssZ shape with a 4-digit year and in-range fields. date.test.ts compares both against the native functions on 60,000 inputs.
…ules Each visited object allocated a path node, a seen entry, a paths array and a result object. Now one Seen object is the first path node and holds the cached result. Plain objects and arrays with Array.prototype skip the typed-array and instanceof rules, because none of them can match.
A plain container copied every annotation of its children into its own record with a longer key, so an annotation at depth d was copied d times (about 17,000 copies for a 2,519-annotation React Query cache). Children now hand up a pending list and keys are joined once, at the root or at the nearest transformed ancestor. The list keeps object key order: array indices ascending first, then insertion order.
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.
Faster
serialize,stringifyanddeserialize, with unchanged output.On 200 benchmark cases,
serializeis 3.5x faster,stringify2.8x anddeserialize1.8x (geomean, unthrottled Chrome). Every case returns the same bytes asmain, and the same values after deserializing.Results
Unthrottled Chrome, 200 cases, one run. Speedup over
main. Identical output in 200 of 200 cases.Throttled Chrome (Brave at 3.79x CPU), 10 independent runs. Mean ± sd of ms per call.
v2.2.3is the release col-web uses. Every branch output is identical tomainin every run.Official
benchmark.json Node 26, 3 interleaved pairs. Median of ops/s.Method: each run starts a fresh browser. A fixed JS loop measures the real CPU throttle before the run. Each case alternates between the versions in short rounds, so drift affects all of them alike. The harness and the commands are on the perf/benchmarks branch.
What changed
isPrimitiveandisSymbolno longer callObject.prototype.toString.transformValuedispatches bytypeof, and empty registries are skipped.parsePathuses anindexOfloop when a key has no backslash.escapeKeyandvalidatePathhave fast paths.toISOStringis built in JS, and a strict ISO parser reads the value back. Anything outside the strictYYYY-MM-DDTHH:mm:ss.sssZshape goes to the native call.date.test.tscompares both against the native functions on 60,000 inputs.deserializewithoutinPlace: a local deep copy replacescopy-anything. It gives the same result ascopy-anything4.0.5.Correctness
npm testpasses on Node 18, 20, 22, 24 and 26: 88 passed, 1 skipped, 1 todo (mainhas 86 passing). Each commit passes on its own.main: 20,000 random values and 40,000 malformed payloads. Output is identical on Bun and on Node.copy-anythingstays independencies, as onmain. This branch no longer imports it. Removing it is a follow-up.Behaviour note.
copy-anything4.0.5 and 4.1.x copy differently when the input todeserializecontains shared object references.package-lock.jsonpins 4.0.5, but consumers who installed 4.1.x got 4.1.x behaviour. The local copy follows 4.0.5 in every install, so the output no longer depends on the installed version. JSON that comes fromparsehas no shared references, so this matters only for objects passed todeserializedirectly.Open PRs ravionhq#344, ravionhq#351, ravionhq#352, ravionhq#355, ravionhq#360 and ravionhq#363 change some of the same files. This branch does not depend on them.
Related
Relates to ravionhq#68 ("Find Performance Bottleneck"). Covered here:
isSymbolcheck that Find Performance Bottleneck ravionhq/superjson#68 discusses no longer callsObject.prototype.toString.setDeepstill walks each path, so this is a partial answer to the traversal point in Find Performance Bottleneck ravionhq/superjson#68.Not covered here: skipping the deep copy in
deserialize(the copy is cheaper, not removed), and optional slow features (ravionhq#85).