Skip to content

Conversation

markerikson
Copy link
Collaborator

@markerikson markerikson commented Sep 1, 2025

This PR significantly rewrites our existing internal RTKQ middleware subscription tracking and polling trigger systems to greatly improve perf, especially in the many-subscriptions-one-cache-entry case.

Fixes #5052

Background

This rewrite was the result of working on #5061 to abort pending requests when cache entries are removed, and #5052 that demonstrated slow perf with thousands of subscriptions to the same cache entry.

Polling

In profiling the many-subscriptions case, I saw that updatePollingInterval was running after every single additional subscription (which we signify internally as a /rejected action with condition: true. That's because any new subscription could have included subscription options for a new polling value, and we need to find the lowest polling interval.

However, there's no reason to be doing that recalculation synchronously every time. We don't even kick off the actual polling request immediately - we set a timer. So, instead it's better to debounce this and do a single updatePollingInterval calculation on the next tick.

Additionally, we were using plain JS Records as the lookup table for polling items, and iterating keys in those objects to get the polling value entries. Switching those to be Maps isn't worse and probably helps.

The only way I could figure out to test this was to hack in a test-only polling update counter and exfiltrate that via the middleware. Stupidly ugly but worked.

Subscriptions

Originally, our internal subscription system used plain object Records as the key/value lookups for subscriptions by request ID, as a holdover from some of the earlier implementations. However, that meant that any time we needed to check for size or "is there at least 1 subscription", we had to do a for...in check to iterate keys and count them. Based on perf profiling, this turned out to be relatively expensive.

This was made trickier because we added a pseudo-subscription for ${requestId}_running in #3709 when we added cache entries for non-subscribed initiated queries, and needed to prevent cache collection of those.

When I reworked the logic in cacheCollection.ts in #3709 to add abort handling, I had to special-case the _running situation as we iterated keys.

This can all be avoided if we just move the currentQueries map from the buildInitiate() closure into the middleware InternalState object and make it available to all the middleware, then use that to check if there's a running query for this cache key.

Additionally, changing all of the subscription lookups from a Record to a Map lets us check subscriptions.size without any key iterating. This did require touching all the places we accessed the subscriptions accordingly (including making sure we switched from Object.keys(subscriptions) to subscriptions.keys()).

Changes

  • Added a test to verify the existing subscriptionsUpdated serialization behavior
  • Rewrote the subscription system:
    • Moved the middleware InternalState object to be initialized in module.ts so it can be passed to both buildInitiate() and buildMiddleware()
    • Moved the runningQueries/runningMutations maps to be in InternalState
    • Changed subscriptions to be nested Maps instead of Records, including all accesses to the subscription data
    • Changed the subscriptionsUpdated serialization to stringify the maps accordingly so we still get the right plain JS objects for use in the reducer
    • Removed the ${requestId}_running handling and replaced it with a direct check against runningQueries for that request ID
  • Rewrote the polling system:
    • Changed it to also use a Map instead of a Record
    • Debounced the polling updates with a setTimeout(0) so we only do one set of updates per tick
    • Added a hacky polling update counter in test environments
    • Restructured a couple tests to fix some shared state leakage I was seeing

Results

Using the thousands-of-subscribed-components example from #5052 , and just unchecking and re-checking the "Render children" box to unmount and remount the components:

Perf before, dev:

image

Perf after, dev:

image

You can see we've eliminated the major perf bottlenecks in polling.ts and cacheCollection.ts, and we're left with just the React dev effect overhead.

Perf before, prod:

image

Perf after, prod

image

Similarly, total execution time dropped significantly and RTKQ is no longer at the top of the list.

# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts
#	packages/toolkit/src/query/core/buildMiddleware/index.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/index.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/polling.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
#	packages/toolkit/src/query/tests/polling.test.tsx
@markerikson markerikson requested a review from phryneas September 1, 2025 16:48
Copy link

codesandbox bot commented Sep 1, 2025

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@markerikson markerikson requested review from EskiMojo14 and aryaemami59 and removed request for aryaemami59 September 1, 2025 16:48
Copy link

codesandbox-ci bot commented Sep 1, 2025

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.

Latest deployment of this branch, based on commit 86b9072:

Sandbox Source
@examples-query-react/basic Configuration
@examples-query-react/advanced Configuration
@examples-action-listener/counter Configuration
rtk-esm-cra Configuration

Copy link

github-actions bot commented Sep 1, 2025

size-limit report 📦

Path Size
1. entry point: @reduxjs/toolkit/query/react (modern.mjs) 15.22 KB (+0.75% 🔺)
1. entry point: @reduxjs/toolkit/query (cjs, production.min.cjs) 24.29 KB (+0.45% 🔺)
1. entry point: @reduxjs/toolkit/query/react (cjs, production.min.cjs) 26.62 KB (+0.78% 🔺)
2. entry point: @reduxjs/toolkit/query (without dependencies) (cjs, production.min.cjs) 10.97 KB (+1.4% 🔺)
3. createApi (.modern.mjs) 15.65 KB (+0.94% 🔺)
3. createApi (react) (.modern.mjs) 17.62 KB (+0.67% 🔺)

Copy link

netlify bot commented Sep 1, 2025

Deploy Preview for redux-starter-kit-docs ready!

Name Link
🔨 Latest commit 86b9072
🔍 Latest deploy log https://app.netlify.com/projects/redux-starter-kit-docs/deploys/68b64f80d8414d00081ba3a1
😎 Deploy Preview https://deploy-preview-5064--redux-starter-kit-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@markerikson
Copy link
Collaborator Author

Tests are passing, we're just seeing PR failures due to TS next deprecating the node10 module resolution algorithm

# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts
#	packages/toolkit/src/query/core/buildMiddleware/index.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/index.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
# Conflicts:
#	packages/toolkit/src/query/core/buildMiddleware/polling.ts
#	packages/toolkit/src/query/core/buildMiddleware/types.ts
#	packages/toolkit/src/query/tests/polling.test.tsx
@markerikson markerikson force-pushed the feature/5052-subscription-perf branch from 6ff3680 to 7513bfa Compare September 1, 2025 17:34
@markerikson markerikson merged commit d2bbb8d into master Sep 2, 2025
120 checks passed
@aryaemami59 aryaemami59 deleted the feature/5052-subscription-perf branch September 16, 2025 03:13
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.

Performance issue with many subscriptions via useQuery vs. useQueryState

3 participants