perf: reduce props proxy closure allocations - #3391
Draft
DaniGuardiola wants to merge 2 commits into
Draft
DaniGuardiola wants to merge 2 commits into
DaniGuardiola wants to merge 2 commits into
Conversation
> 🤖🔧 ai generated
> 🤖🔧 ai generated
|
ryansolid
added a commit
that referenced
this pull request
Sep 13, 2026
…target Each merge()/omit() proxy allocated a target holding three fresh closures (get/has/keys) over the instance's sources and forwarded every trap call through them. Keep the sources/keys on the target under symbol keys and share one handler per primitive instead: a props proxy is a Proxy plus a one- or two-slot object, and a read goes straight from the trap to the sources. The state keys are never reported by ownKeys nor answered by get/has; a string defineProperty on the proxy lands on the target without touching them (previously it could overwrite the target's `get` method). Behaviour is unchanged: `$SOURCES` still answers the flat list on a merge proxy (mergeSources/spread) and undefined on an omit proxy (#3014); the has/ownKeys filtering is the same. Port of #3391 (main) to next. Interleaved A/B, old vs new dist in one process, on battery (±20% noise): retained 200-instance merge+omit batch 0.87–0.93×, nested merge over a merge proxy 0.65–0.75×, Object.keys / spread 0.90–0.98×; single construct and reads within noise in process-isolated alternating runs. Co-authored-by: Cursor <cursoragent@cursor.com>
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
Replace per-instance proxy target closures in
mergePropsandsplitPropswith private-symbol data read by shared handlers. Proxy counts and public types stay unchanged. Local benchmarks showed 19–46% lower median creation time across four runs.What changes?
The proxy handlers are already shared today. However, each output proxy's target contains three newly created methods (
get,has,keys) closing over that instance's data. This change stores the data on the target and moves that work into shared functions.Simplified target construction:
For 10,000 merge instances, both versions allocate 10,000 proxies and targets, but the new version avoids 30,000 instance-specific methods. Reads also skip the forwarding call from handler to target method.
splitPropsuses the same approach for source, selected keys, and remainder state.Private symbols prevent
Object.definePropertyon names such assourceorsourcesfrom overwriting internal state. The regression test covers that isolation, symbol visibility, and live reads after store updates.Performance
Local comparison on Apple M4 Pro, Node 25.9.0 / V8 14.1, Vitest 4.1.10 with jsdom. Four fresh processes, alternating implementation order. Each operation handles 10,000 retained instances backed by distinct real stores; source setup is outside timing. Reads access four fields per instance.
Times are milliseconds per batch, reported as the median of four per-run means:
Creation improved in every paired run, but reductions varied substantially: 14–40% for merge, 13–54% for split, and 13–56% for the chain. Read results were less consistent, including one noisy merge-read result (8.36 ms, ±23% RME) that did not recur. These are exploratory local measurements, not browser-wide performance guarantees; allocation, GC, and engine optimization effects have not been separated.
The PR includes the six retained-instance benchmarks for reproducing the workload across revisions; the local comparison harness and saved results are not included.
How did you test this change?
pnpm run buildpnpm test— 492 runtime tests passed, plus type, Babel preset, and import checks.pnpm --dir packages/solid exec vitest bench test/props-retained.bench.ts --run— all 6 cases passed.