From 45fb7da20b1a2c0b2bca3a3d65a1fa6b3977ba6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 23 Feb 2026 12:25:41 +0900 Subject: [PATCH 1/5] perf: Optimize cache lookups by directly iterating internal collections instead of using getAll --- packages/query-core/src/mutationCache.ts | 24 +++++++++++++---- packages/query-core/src/queryCache.ts | 33 ++++++++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index d204c904e5f..46cf3fdcc63 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -211,13 +211,22 @@ export class MutationCache extends Subscribable { ): Mutation | undefined { const defaultedFilters = { exact: true, ...filters } - return this.getAll().find((mutation) => - matchMutation(defaultedFilters, mutation), - ) as Mutation | undefined + for (const mutation of this.#mutations) { + if (matchMutation(defaultedFilters, mutation)) { + return mutation as Mutation + } + } + return undefined } findAll(filters: MutationFilters = {}): Array { - return this.getAll().filter((mutation) => matchMutation(filters, mutation)) + const result: Array = [] + for (const mutation of this.#mutations) { + if (matchMutation(filters, mutation)) { + result.push(mutation) + } + } + return result } notify(event: MutationCacheNotifyEvent) { @@ -229,7 +238,12 @@ export class MutationCache extends Subscribable { } resumePausedMutations(): Promise { - const pausedMutations = this.getAll().filter((x) => x.state.isPaused) + const pausedMutations: Array = [] + for (const mutation of this.#mutations) { + if (mutation.state.isPaused) { + pausedMutations.push(mutation) + } + } return notifyManager.batch(() => Promise.all( diff --git a/packages/query-core/src/queryCache.ts b/packages/query-core/src/queryCache.ts index dd7123eaac8..4d55a9bf5fd 100644 --- a/packages/query-core/src/queryCache.ts +++ b/packages/query-core/src/queryCache.ts @@ -185,16 +185,27 @@ export class QueryCache extends Subscribable { ): Query | undefined { const defaultedFilters = { exact: true, ...filters } - return this.getAll().find((query) => - matchQuery(defaultedFilters, query), - ) as Query | undefined + let found: Query | undefined + for (const query of this.#queries.values()) { + if (matchQuery(defaultedFilters, query)) { + found = query + break + } + } + return found as Query | undefined } findAll(filters: QueryFilters = {}): Array { - const queries = this.getAll() - return Object.keys(filters).length > 0 - ? queries.filter((query) => matchQuery(filters, query)) - : queries + if (Object.keys(filters).length === 0) { + return [...this.#queries.values()] + } + const result: Array = [] + for (const query of this.#queries.values()) { + if (matchQuery(filters, query)) { + result.push(query) + } + } + return result } notify(event: QueryCacheNotifyEvent): void { @@ -207,17 +218,17 @@ export class QueryCache extends Subscribable { onFocus(): void { notifyManager.batch(() => { - this.getAll().forEach((query) => { + for (const query of this.#queries.values()) { query.onFocus() - }) + } }) } onOnline(): void { notifyManager.batch(() => { - this.getAll().forEach((query) => { + for (const query of this.#queries.values()) { query.onOnline() - }) + } }) } } From 1c5622de44ab30fd275b5bc56346deec24d4f729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 23 Feb 2026 15:14:58 +0900 Subject: [PATCH 2/5] perf: fast-path exact queryKey lookups via hash map in find/findAll --- packages/query-core/src/mutationCache.ts | 48 +++++++++++++++++++++++- packages/query-core/src/queryCache.ts | 23 +++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index 46cf3fdcc63..fc517da4692 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -1,6 +1,6 @@ import { notifyManager } from './notifyManager' import { Mutation } from './mutation' -import { matchMutation, noop } from './utils' +import { hashKey, matchMutation, noop } from './utils' import { Subscribable } from './subscribable' import type { MutationObserver } from './mutationObserver' import type { @@ -93,12 +93,14 @@ type MutationCacheListener = (event: MutationCacheNotifyEvent) => void export class MutationCache extends Subscribable { #mutations: Set> #scopes: Map>> + #byMutationKey: Map>> #mutationId: number constructor(public config: MutationCacheConfig = {}) { super() this.#mutations = new Set() this.#scopes = new Map() + this.#byMutationKey = new Map() this.#mutationId = 0 } @@ -131,6 +133,13 @@ export class MutationCache extends Subscribable { this.#scopes.set(scope, [mutation]) } } + const { mutationKey } = mutation.options + if (mutationKey) { + const hash = hashKey(mutationKey) + const keyed = this.#byMutationKey.get(hash) + if (keyed) keyed.push(mutation) + else this.#byMutationKey.set(hash, [mutation]) + } this.notify({ type: 'added', mutation }) } @@ -150,6 +159,19 @@ export class MutationCache extends Subscribable { } } } + const { mutationKey } = mutation.options + if (mutationKey) { + const hash = hashKey(mutationKey) + const keyed = this.#byMutationKey.get(hash) + if (keyed) { + if (keyed.length > 1) { + const index = keyed.indexOf(mutation) + if (index !== -1) keyed.splice(index, 1) + } else { + this.#byMutationKey.delete(hash) + } + } + } } // Currently we notify the removal even if the mutation was already removed. @@ -194,6 +216,7 @@ export class MutationCache extends Subscribable { }) this.#mutations.clear() this.#scopes.clear() + this.#byMutationKey.clear() }) } @@ -211,6 +234,20 @@ export class MutationCache extends Subscribable { ): Mutation | undefined { const defaultedFilters = { exact: true, ...filters } + if (defaultedFilters.exact && defaultedFilters.mutationKey) { + const candidates = this.#byMutationKey.get( + hashKey(defaultedFilters.mutationKey), + ) + if (!candidates) return undefined + const { mutationKey: _m, ...filtersWithoutKey } = defaultedFilters + for (const mutation of candidates) { + if (matchMutation(filtersWithoutKey as MutationFilters, mutation)) { + return mutation as Mutation + } + } + return undefined + } + for (const mutation of this.#mutations) { if (matchMutation(defaultedFilters, mutation)) { return mutation as Mutation @@ -220,6 +257,15 @@ export class MutationCache extends Subscribable { } findAll(filters: MutationFilters = {}): Array { + if (filters.exact && filters.mutationKey) { + const candidates = this.#byMutationKey.get(hashKey(filters.mutationKey)) + if (!candidates) return [] + const { mutationKey: _m, ...filtersWithoutKey } = filters + return candidates.filter((m) => + matchMutation(filtersWithoutKey as MutationFilters, m), + ) + } + const result: Array = [] for (const mutation of this.#mutations) { if (matchMutation(filters, mutation)) { diff --git a/packages/query-core/src/queryCache.ts b/packages/query-core/src/queryCache.ts index 4d55a9bf5fd..0820ce7b698 100644 --- a/packages/query-core/src/queryCache.ts +++ b/packages/query-core/src/queryCache.ts @@ -1,4 +1,4 @@ -import { hashQueryKeyByOptions, matchQuery } from './utils' +import { hashKey, hashQueryKeyByOptions, matchQuery } from './utils' import { Query } from './query' import { notifyManager } from './notifyManager' import { Subscribable } from './subscribable' @@ -186,6 +186,18 @@ export class QueryCache extends Subscribable { const defaultedFilters = { exact: true, ...filters } let found: Query | undefined + + if (defaultedFilters.exact) { + const candidate = this.#queries.get(hashKey(defaultedFilters.queryKey)) + if (candidate) { + const { queryKey: _q, ...filtersWithoutKey } = defaultedFilters + found = matchQuery(filtersWithoutKey as QueryFilters, candidate) + ? candidate + : undefined + return found as Query | undefined + } + } + for (const query of this.#queries.values()) { if (matchQuery(defaultedFilters, query)) { found = query @@ -199,6 +211,15 @@ export class QueryCache extends Subscribable { if (Object.keys(filters).length === 0) { return [...this.#queries.values()] } + + if (filters.exact && filters.queryKey) { + const candidate = this.#queries.get(hashKey(filters.queryKey)) + if (candidate) { + const { queryKey: _q, ...filtersWithoutKey } = filters + return matchQuery(filtersWithoutKey as QueryFilters, candidate) ? [candidate] : [] + } + } + const result: Array = [] for (const query of this.#queries.values()) { if (matchQuery(filters, query)) { From b300f30eb5fd1f08ce2e37ee49063d111f982eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Tue, 24 Feb 2026 00:24:17 +0900 Subject: [PATCH 3/5] chore: add changeset --- .changeset/perf-cache-lookup-optimization.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perf-cache-lookup-optimization.md diff --git a/.changeset/perf-cache-lookup-optimization.md b/.changeset/perf-cache-lookup-optimization.md new file mode 100644 index 00000000000..117b22d96cf --- /dev/null +++ b/.changeset/perf-cache-lookup-optimization.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-core": patch +--- + +Optimize `find`/`findAll` lookups in `QueryCache` and `MutationCache` by using index maps for O(1) key-based access when `exact: true` and a key filter are provided, instead of iterating all entries. From 7a878945f9921e80b64bda9e7e1f148020310052 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:29:45 +0000 Subject: [PATCH 4/5] ci: apply automated fixes --- packages/query-core/src/mutationCache.ts | 7 ++++++- packages/query-core/src/queryCache.ts | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index fc517da4692..419d31457e5 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -242,7 +242,12 @@ export class MutationCache extends Subscribable { const { mutationKey: _m, ...filtersWithoutKey } = defaultedFilters for (const mutation of candidates) { if (matchMutation(filtersWithoutKey as MutationFilters, mutation)) { - return mutation as Mutation + return mutation as Mutation< + TData, + TError, + TVariables, + TOnMutateResult + > } } return undefined diff --git a/packages/query-core/src/queryCache.ts b/packages/query-core/src/queryCache.ts index 0820ce7b698..f5afc1bbdcb 100644 --- a/packages/query-core/src/queryCache.ts +++ b/packages/query-core/src/queryCache.ts @@ -216,7 +216,9 @@ export class QueryCache extends Subscribable { const candidate = this.#queries.get(hashKey(filters.queryKey)) if (candidate) { const { queryKey: _q, ...filtersWithoutKey } = filters - return matchQuery(filtersWithoutKey as QueryFilters, candidate) ? [candidate] : [] + return matchQuery(filtersWithoutKey as QueryFilters, candidate) + ? [candidate] + : [] } } From b018ef1fd058d4ac3f54d7b5589f122ec08ad330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Tue, 24 Feb 2026 00:48:14 +0900 Subject: [PATCH 5/5] fix: prevent deleting wrong mutationKey bucket on remove --- packages/query-core/src/mutationCache.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index 419d31457e5..25684ffc8c7 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -164,11 +164,10 @@ export class MutationCache extends Subscribable { const hash = hashKey(mutationKey) const keyed = this.#byMutationKey.get(hash) if (keyed) { - if (keyed.length > 1) { - const index = keyed.indexOf(mutation) - if (index !== -1) keyed.splice(index, 1) - } else { - this.#byMutationKey.delete(hash) + const index = keyed.indexOf(mutation) + if (index !== -1) { + keyed.splice(index, 1) + if (keyed.length === 0) this.#byMutationKey.delete(hash) } } }