Skip to content

perf(createSelector): gather input selector results without extra allocations - #772

Open
veksa wants to merge 1 commit into
reduxjs:masterfrom
veksa:perf/create-selector-input-gathering
Open

perf(createSelector): gather input selector results without extra allocations#772
veksa wants to merge 1 commit into
reduxjs:masterfrom
veksa:perf/create-selector-input-gathering

Conversation

@veksa

@veksa veksa commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

dependenciesChecker calls collectInputSelectorResults(dependencies, arguments) on every dependency recomputation. Inlining that loop is worth about a fifth of the call, for two separate reasons:

  • arguments stops escaping. Passing it to another function forces V8 to materialize it on the heap every call. Used only as the second operand of .apply, it can stay in the frame. The dev-mode block below still hands it around, but that block is compiled out of production builds, which is the only build this matters in.
  • The array is allocated at its final size instead of being grown from [] by push. This is the larger half. I assumed it would be the smaller one.

I also tried arity-specialized array literals on top of this — [d0(...)], [d0(...), d1(...)], and so on. They measured no better than a single new Array(length) loop at 1 and 3 inputs, and at 6 inputs the ladder was slightly worse than the fallback, so there's one path for every dependency count instead of a cutoff I couldn't justify.

dependencies and its length are still read per call rather than hoisted, so a selector whose dependencies array is mutated behaves as before.

Measured with the harness from #770 (paired against master in one process, min of 15 interleaved rounds, ns/call):

case before after
1 input, (state, props) 177.9 143.4 1.24x
3 inputs, (state, props) 213.6 182.8 1.17x
6 inputs, (state, props) 313.1 277.1 1.13x
same as first, argsMemoize: lruMemoize 77.7 70.5 1.10x
1 input, (state) 11.9 11.6 noise
nested (2 output selectors) 12.1 12.0 noise

The last two rows are selectors called with the state alone, so they hit the argument cache and never reach this code — nothing to gain there. The win shrinks as the input count grows, which makes sense: the allocation saved is fixed, the input selector calls aren't.

Recomputation counts are identical everywhere, so it's the same work throughout.

A profile is what pointed here, and it also killed my first two guesses — arity dispatch and the array allocation looked like the obvious targets, and dependenciesChecker was 1.3% of self time. The array turned out to matter anyway, but through allocation and GC rather than through the code being slow.

@netlify

netlify Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploy Preview for reselect-docs canceled.

Name Link
🔨 Latest commit 394247f
🔍 Latest deploy log https://app.netlify.com/projects/reselect-docs/deploys/6a7596f7005c9f00083d6763

@codesandbox-ci

codesandbox-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant