Skip to content

Commit ba4e5bc

Browse files
committed
perf(array): improve array remove method perf
1 parent 99fac77 commit ba4e5bc

File tree

6 files changed

+229
-8
lines changed

6 files changed

+229
-8
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"benchmark": "yarn build && yarn benchmark:base && yarn benchmark:object && yarn benchmark:array && yarn benchmark:class",
3232
"all-benchmark": "yarn build && NODE_ENV='production' ts-node test/benchmark/index.ts",
3333
"benchmark:reducer": "NODE_ENV='production' ts-node test/performance/benchmark-reducer.ts",
34+
"benchmark:reducer1": "NODE_ENV='production' node test/performance/benchmark-reducer1.mjs",
3435
"benchmark:base": "NODE_ENV='production' ts-node test/performance/benchmark.ts",
3536
"benchmark:object": "NODE_ENV='production' ts-node test/performance/benchmark-object.ts",
3637
"benchmark:array": "NODE_ENV='production' ts-node test/performance/benchmark-array.ts",
@@ -117,6 +118,7 @@
117118
"json2csv": "^5.0.7",
118119
"lodash": "^4.17.21",
119120
"lodash.clonedeep": "^4.5.0",
121+
"mitata": "^1.0.25",
120122
"prettier": "^3.3.3",
121123
"quickchart-js": "^3.1.2",
122124
"redux": "^5.0.1",

src/draft.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ import {
2929
finalizeSetValue,
3030
markFinalization,
3131
finalizePatches,
32+
isDraft,
33+
skipFinalization,
3234
} from './utils';
3335
import { checkReadable } from './unsafe';
3436
import { generatePatches } from './patch';
3537

3638
const draftsCache = new WeakSet<object>();
3739

40+
let arrayHandling = false;
41+
3842
const proxyHandler: ProxyHandler<ProxyDraft> = {
3943
get(target: ProxyDraft, key: string | number | symbol, receiver: any) {
4044
const copy = target.copy?.[key];
@@ -88,6 +92,26 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
8892

8993
if (!has(source, key)) {
9094
const desc = getDescriptor(source, key);
95+
if (target.type === DraftType.Array) {
96+
if (
97+
[
98+
'splice',
99+
'push',
100+
'pop',
101+
'shift',
102+
'unshift',
103+
'sort',
104+
'reverse',
105+
].includes(key as string)
106+
) {
107+
return function (this: any, ...args: any[]) {
108+
arrayHandling = true;
109+
const result = desc!.value.apply(this, args);
110+
arrayHandling = false;
111+
return result;
112+
};
113+
}
114+
}
91115
return desc
92116
? `value` in desc
93117
? desc.value
@@ -103,7 +127,7 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
103127
return value;
104128
}
105129
// Ensure that the assigned values are not drafted
106-
if (value === peek(target.original, key)) {
130+
if (value === peek(target.original, key) && !arrayHandling) {
107131
ensureShallowCopy(target);
108132
target.copy![key] = createDraft({
109133
original: target.original[key],
@@ -122,6 +146,11 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
122146
}
123147
return target.copy![key];
124148
}
149+
if (arrayHandling && !isDraft(value)) {
150+
skipFinalization.add(value);
151+
} else if (skipFinalization.has(value)) {
152+
skipFinalization.delete(value);
153+
}
125154
return value;
126155
},
127156
set(target: ProxyDraft, key: string | number | symbol, value: any) {
@@ -319,10 +348,10 @@ export function finalizeDraft<T>(
319348
const state = hasReturnedValue
320349
? returnedValue[0]
321350
: proxyDraft
322-
? proxyDraft.operated
323-
? proxyDraft.copy
324-
: proxyDraft.original
325-
: result;
351+
? proxyDraft.operated
352+
? proxyDraft.copy
353+
: proxyDraft.original
354+
: result;
326355
if (proxyDraft) revokeProxy(proxyDraft);
327356
if (enableAutoFreeze) {
328357
deepFreeze(state, state, proxyDraft?.options.updatedValues);

src/utils/finalize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from './draft';
1313
import { forEach } from './forEach';
1414

15+
export const skipFinalization = new WeakSet();
16+
1517
export function handleValue(
1618
target: any,
1719
handledSet: WeakSet<any>,
@@ -21,7 +23,8 @@ export function handleValue(
2123
isDraft(target) ||
2224
!isDraftable(target, options) ||
2325
handledSet.has(target) ||
24-
Object.isFrozen(target)
26+
Object.isFrozen(target) ||
27+
skipFinalization.has(target)
2528
)
2629
return;
2730
const isSet = target instanceof Set;

test/performance/benchmark-reducer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const MAX_ITERATIONS = 100;
194194
}
195195
console.timeEnd('immer:autoFreeze:nextAction');
196196
}
197+
console.log('---------------------------------');
197198
{
198199
setAutoFreeze(false);
199200
const initialState = createInitialState();
@@ -207,7 +208,7 @@ const MAX_ITERATIONS = 100;
207208
}
208209
console.timeEnd('immer:nextAction');
209210
}
210-
211+
console.log('---------------------------------');
211212
{
212213
const initialState = createInitialState();
213214
console.time('mutative:autoFreeze');
@@ -220,7 +221,7 @@ const MAX_ITERATIONS = 100;
220221
}
221222
console.timeEnd('mutative:autoFreeze:nextAction');
222223
}
223-
224+
console.log('---------------------------------');
224225
{
225226
const initialState = createInitialState();
226227
console.time('vanilla');
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { produce as produce10, setAutoFreeze as setAutoFreeze10 } from 'immer';
2+
import { create as produceMutative } from '../../dist/mutative.esm.mjs';
3+
import { bench, run, summary } from 'mitata';
4+
5+
function createInitialState() {
6+
const initialState = {
7+
largeArray: Array.from({ length: 10000 }, (_, i) => ({
8+
id: i,
9+
value: Math.random(),
10+
nested: { key: `key-${i}`, data: Math.random() },
11+
moreNested: {
12+
items: Array.from({ length: 100 }, (_, i) => ({
13+
id: i,
14+
name: String(i),
15+
})),
16+
},
17+
})),
18+
otherData: Array.from({ length: 10000 }, (_, i) => ({
19+
id: i,
20+
name: `name-${i}`,
21+
isActive: i % 2 === 0,
22+
})),
23+
};
24+
return initialState;
25+
}
26+
27+
const MAX = 1;
28+
29+
const add = (index) => ({
30+
type: 'test/addItem',
31+
payload: { id: index, value: index, nested: { data: index } },
32+
});
33+
const remove = (index) => ({ type: 'test/removeItem', payload: index });
34+
const update = (index) => ({
35+
type: 'test/updateItem',
36+
payload: { id: index, value: index, nestedData: index },
37+
});
38+
const concat = (index) => ({
39+
type: 'test/concatArray',
40+
payload: Array.from({ length: 500 }, (_, i) => ({ id: i, value: index })),
41+
});
42+
43+
const actions = {
44+
add,
45+
remove,
46+
update,
47+
concat,
48+
};
49+
50+
const immerProducers = {
51+
immer10: produce10,
52+
mutative: produceMutative,
53+
};
54+
55+
const setAutoFreezes = {
56+
vanilla: () => {},
57+
immer10: setAutoFreeze10,
58+
mutative: () => {},
59+
};
60+
61+
const vanillaReducer = (state = createInitialState(), action) => {
62+
switch (action.type) {
63+
case 'test/addItem':
64+
return {
65+
...state,
66+
largeArray: [...state.largeArray, action.payload],
67+
};
68+
case 'test/removeItem': {
69+
const newArray = state.largeArray.slice();
70+
newArray.splice(action.payload, 1);
71+
return {
72+
...state,
73+
largeArray: newArray,
74+
};
75+
}
76+
case 'test/updateItem': {
77+
return {
78+
...state,
79+
largeArray: state.largeArray.map((item) =>
80+
item.id === action.payload.id
81+
? {
82+
...item,
83+
value: action.payload.value,
84+
nested: { ...item.nested, data: action.payload.nestedData },
85+
}
86+
: item
87+
),
88+
};
89+
}
90+
case 'test/concatArray': {
91+
const length = state.largeArray.length;
92+
const newArray = action.payload.concat(state.largeArray);
93+
newArray.length = length;
94+
return {
95+
...state,
96+
largeArray: newArray,
97+
};
98+
}
99+
default:
100+
return state;
101+
}
102+
};
103+
104+
const createImmerReducer = (produce) => {
105+
const immerReducer = (state = createInitialState(), action) =>
106+
produce(state, (draft) => {
107+
switch (action.type) {
108+
case 'test/addItem':
109+
draft.largeArray.push(action.payload);
110+
break;
111+
case 'test/removeItem':
112+
draft.largeArray.splice(action.payload, 1);
113+
break;
114+
case 'test/updateItem': {
115+
const item = draft.largeArray.find(
116+
(item) => item.id === action.payload.id
117+
);
118+
item.value = action.payload.value;
119+
item.nested.data = action.payload.nestedData;
120+
break;
121+
}
122+
case 'test/concatArray': {
123+
const length = state.largeArray.length;
124+
const newArray = action.payload.concat(state.largeArray);
125+
newArray.length = length;
126+
draft.largeArray = newArray;
127+
break;
128+
}
129+
}
130+
});
131+
132+
return immerReducer;
133+
};
134+
135+
function mapValues(obj, fn) {
136+
const result = {};
137+
for (const key in obj) {
138+
result[key] = fn(obj[key]);
139+
}
140+
return result;
141+
}
142+
143+
const reducers = {
144+
vanilla: vanillaReducer,
145+
...mapValues(immerProducers, createImmerReducer),
146+
};
147+
148+
function createBenchmarks() {
149+
for (const action in actions) {
150+
summary(function () {
151+
bench(`$action: $version (freeze: $freeze)`, function* (args) {
152+
const version = args.get('version');
153+
const freeze = args.get('freeze');
154+
const action = args.get('action');
155+
156+
const initialState = createInitialState();
157+
158+
function benchMethod() {
159+
setAutoFreezes[version](freeze);
160+
for (let i = 0; i < MAX; i++) {
161+
reducers[version](initialState, actions[action](i));
162+
}
163+
setAutoFreezes[version](false);
164+
}
165+
166+
yield benchMethod;
167+
}).args({
168+
version: Object.keys(reducers),
169+
freeze: [false, true],
170+
action: [action],
171+
});
172+
});
173+
}
174+
}
175+
176+
async function main() {
177+
createBenchmarks();
178+
await run();
179+
}
180+
181+
main();

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5412,6 +5412,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
54125412
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
54135413
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
54145414

5415+
mitata@^1.0.25:
5416+
version "1.0.25"
5417+
resolved "https://registry.yarnpkg.com/mitata/-/mitata-1.0.25.tgz#918d0d04d2be0aeae7152cc7d8373b3a727b1b4f"
5418+
integrity sha512-0v5qZtVW5vwj9FDvYfraR31BMDcRLkhSFWPTLaxx/Z3/EvScfVtAAWtMI2ArIbBcwh7P86dXh0lQWKiXQPlwYA==
5419+
54155420
54165421
version "2.1.2"
54175422
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"

0 commit comments

Comments
 (0)