From 04f41313a68d23a9372fd1db822dc068cff2da1d Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Feb 2023 12:29:58 -0500 Subject: [PATCH] Use a WeakMap cache for query arg serialization for perf --- .../src/query/defaultSerializeQueryArgs.ts | 37 ++++++++--- .../tests/defaultSerializeQueryArgs.test.ts | 66 +++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts index 61bd2e55b4..e4988ead2c 100644 --- a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts +++ b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts @@ -2,21 +2,38 @@ import type { QueryCacheKey } from './core/apiState' import type { EndpointDefinition } from './endpointDefinitions' import { isPlainObject } from '@reduxjs/toolkit' +const cache: WeakMap | undefined = WeakMap + ? new WeakMap() + : undefined + export const defaultSerializeQueryArgs: SerializeQueryArgs = ({ endpointName, queryArgs, }) => { + let serialized = '' + + const cached = cache?.get(queryArgs) + + if (typeof cached === 'string') { + serialized = cached + } else { + const stringified = JSON.stringify(queryArgs, (key, value) => + isPlainObject(value) + ? Object.keys(value) + .sort() + .reduce((acc, key) => { + acc[key] = (value as any)[key] + return acc + }, {}) + : value + ) + if (isPlainObject(queryArgs)) { + cache?.set(queryArgs, stringified) + } + serialized = stringified + } // Sort the object keys before stringifying, to prevent useQuery({ a: 1, b: 2 }) having a different cache key than useQuery({ b: 2, a: 1 }) - return `${endpointName}(${JSON.stringify(queryArgs, (key, value) => - isPlainObject(value) - ? Object.keys(value) - .sort() - .reduce((acc, key) => { - acc[key] = (value as any)[key] - return acc - }, {}) - : value - )})` + return `${endpointName}(${serialized})` } export type SerializeQueryArgs = (_: { diff --git a/packages/toolkit/src/query/tests/defaultSerializeQueryArgs.test.ts b/packages/toolkit/src/query/tests/defaultSerializeQueryArgs.test.ts index 0204022846..9ec8a07dd7 100644 --- a/packages/toolkit/src/query/tests/defaultSerializeQueryArgs.test.ts +++ b/packages/toolkit/src/query/tests/defaultSerializeQueryArgs.test.ts @@ -44,3 +44,69 @@ test('nested object arg is sorted recursively', () => { `"test({\\"age\\":5,\\"name\\":{\\"first\\":\\"Banana\\",\\"last\\":\\"Split\\"}})"` ) }) + +test('Fully serializes a deeply nested object', () => { + const nestedObj = { + a: { + a1: { + a11: { + a111: 1, + }, + }, + }, + b: { + b2: { + b21: 3, + }, + b1: { + b11: 2, + }, + }, + } + + const res = defaultSerializeQueryArgs({ + endpointDefinition, + endpointName, + queryArgs: nestedObj, + }) + expect(res).toMatchInlineSnapshot( + `"test({\\"a\\":{\\"a1\\":{\\"a11\\":{\\"a111\\":1}}},\\"b\\":{\\"b1\\":{\\"b11\\":2},\\"b2\\":{\\"b21\\":3}}})"` + ) +}) + +test('Caches results for plain objects', () => { + const testData = Array.from({ length: 10000 }).map((_, i) => { + return { + albumId: i, + id: i, + title: 'accusamus beatae ad facilis cum similique qui sunt', + url: 'https://via.placeholder.com/600/92c952', + thumbnailUrl: 'https://via.placeholder.com/150/92c952', + } + }) + + const data = { + testData, + } + + const runWithTimer = (data: any) => { + const start = Date.now() + const res = defaultSerializeQueryArgs({ + endpointDefinition, + endpointName, + queryArgs: data, + }) + const end = Date.now() + const duration = end - start + return [res, duration] as const + } + + const [res1, time1] = runWithTimer(data) + const [res2, time2] = runWithTimer(data) + + expect(res1).toBe(res2) + expect(time2).toBeLessThanOrEqual(time1) + // Locally, stringifying 10K items takes 25-30ms. + // Assuming the WeakMap cache hit, this _should_ be 0 + expect(time2).toBeLessThan(2) +})