From 3eaf5f2ebbe5b6c583ab300e2c8fda9a28b87306 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sun, 5 Apr 2026 18:11:43 -0400 Subject: [PATCH] enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GlobalCache.getResults() called unshift() on every cache-miss denormalization, which is O(n) because it shifts all existing elements. Pre-allocate slot 0 with a placeholder and fill it in-place, turning the operation into O(1). Benchmarks showed 1.3–3.2% improvement on cold-denormalize paths (denormalizeLong variants). Made-with: Cursor --- .changeset/globalcache-preallocate.md | 7 +++++++ packages/normalizr/src/memo/globalCache.ts | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 .changeset/globalcache-preallocate.md diff --git a/.changeset/globalcache-preallocate.md b/.changeset/globalcache-preallocate.md new file mode 100644 index 000000000000..06bc3b4cd58e --- /dev/null +++ b/.changeset/globalcache-preallocate.md @@ -0,0 +1,7 @@ +--- +'@data-client/normalizr': patch +--- + +Improve denormalization performance by pre-allocating the dependency tracking slot + +Replace `Array.prototype.unshift()` in `GlobalCache.getResults()` with a pre-allocated slot at index 0, avoiding O(n) element shifting on every cache-miss denormalization. diff --git a/packages/normalizr/src/memo/globalCache.ts b/packages/normalizr/src/memo/globalCache.ts index 85ffe0df16a6..8b62dce07210 100644 --- a/packages/normalizr/src/memo/globalCache.ts +++ b/packages/normalizr/src/memo/globalCache.ts @@ -6,8 +6,13 @@ import type { INVALID } from '../denormalize/symbol.js'; import type { EntityInterface, EntityPath } from '../interface.js'; import type { DenormGetEntity } from './types.js'; +const PLACEHOLDER_DEP: Dep = { + path: { key: '', pk: '' }, + entity: undefined, +}; + export default class GlobalCache implements Cache { - private dependencies: Dep[] = []; + private dependencies: Dep[] = [PLACEHOLDER_DEP]; private cycleCache: Map> = new Map(); private cycleIndex = -1; private localCache: Map> = new Map(); @@ -120,10 +125,10 @@ export default class GlobalCache implements Cache { if (paths === undefined) { data = computeValue(); - // we want to do this before we add our 'input' entry + // we want to do this before we fill our 'input' entry paths = this.paths(); - // for the first entry, `path` is ignored so empty members is fine - this.dependencies.unshift({ path: { key: '', pk: '' }, entity: input }); + // fill pre-allocated slot 0 with the input reference + this.dependencies[0] = { path: { key: '', pk: '' }, entity: input }; this._resultCache.set(this.dependencies, data); } else { paths.shift(); @@ -132,7 +137,12 @@ export default class GlobalCache implements Cache { } protected paths() { - return this.dependencies.map(dep => dep.path); + const deps = this.dependencies; + const paths = new Array(deps.length - 1); + for (let i = 1; i < deps.length; i++) { + paths[i - 1] = deps[i].path; + } + return paths; } }