Skip to content

Commit 7d28629

Browse files
authored
enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift() (#3876)
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
1 parent f5797b4 commit 7d28629

2 files changed

Lines changed: 22 additions & 5 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@data-client/normalizr': patch
3+
---
4+
5+
Improve denormalization performance by pre-allocating the dependency tracking slot
6+
7+
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.

packages/normalizr/src/memo/globalCache.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import type { INVALID } from '../denormalize/symbol.js';
66
import type { EntityInterface, EntityPath } from '../interface.js';
77
import type { DenormGetEntity } from './types.js';
88

9+
const PLACEHOLDER_DEP: Dep<EntityPath> = {
10+
path: { key: '', pk: '' },
11+
entity: undefined,
12+
};
13+
914
export default class GlobalCache implements Cache {
10-
private dependencies: Dep<EntityPath>[] = [];
15+
private dependencies: Dep<EntityPath>[] = [PLACEHOLDER_DEP];
1116
private cycleCache: Map<string, Map<string, number>> = new Map();
1217
private cycleIndex = -1;
1318
private localCache: Map<string, Map<string, any>> = new Map();
@@ -120,10 +125,10 @@ export default class GlobalCache implements Cache {
120125

121126
if (paths === undefined) {
122127
data = computeValue();
123-
// we want to do this before we add our 'input' entry
128+
// we want to do this before we fill our 'input' entry
124129
paths = this.paths();
125-
// for the first entry, `path` is ignored so empty members is fine
126-
this.dependencies.unshift({ path: { key: '', pk: '' }, entity: input });
130+
// fill pre-allocated slot 0 with the input reference
131+
this.dependencies[0] = { path: { key: '', pk: '' }, entity: input };
127132
this._resultCache.set(this.dependencies, data);
128133
} else {
129134
paths.shift();
@@ -132,7 +137,12 @@ export default class GlobalCache implements Cache {
132137
}
133138

134139
protected paths() {
135-
return this.dependencies.map(dep => dep.path);
140+
const deps = this.dependencies;
141+
const paths = new Array(deps.length - 1);
142+
for (let i = 1; i < deps.length; i++) {
143+
paths[i - 1] = deps[i].path;
144+
}
145+
return paths;
136146
}
137147
}
138148

0 commit comments

Comments
 (0)