Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
Run results for:
Testing immutable libs
Trying to look for alternatives for fastMerge
Go to the benchmark
Embed
Embed Benchmark Result
Run details:
User agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Browser:
Chrome 140
Operating system:
Mac OS X 10.15.7
Device Platform:
Desktop
Date tested:
7 months ago
Test name
Executions per second
Onyx fastMerge
111.3 Ops/sec
Optimized fastMerge
112.9 Ops/sec
Mutative
24.2 Ops/sec
Immutable-js
17.9 Ops/sec
ImmerJS
21.0 Ops/sec
HTML Preparation code:
<!--your preparation HTML code goes here--> <script src='https://unpkg.com/fast-equals@5.2.2/dist/min/index.js'></script> <script src='https://unpkg.com/immer@6.0.3/dist/immer.umd.production.min.js'></script> <script src='https://unpkg.com/lodash@4.17.21/lodash.min.js'></script> <script src="https://unpkg.com/mutative@1.3.0/dist/mutative.umd.production.min.js"></script> <script src="https://unpkg.com/immutable@5.1.3/dist/immutable.min.js"></script>
Script Preparation code:
function generateDeepObject(levels, breadth, currentLevel = 0) { if (currentLevel >= levels) { return { value: `leaf_${Math.random()}`, number: Math.random() * 1000, nullValue: Math.random() > 0.7 ? null : `value_${Math.random()}`, array: Array.from({length: 5}, (_, i) => i) }; } const obj = {}; for (let i = 0; i < breadth; i++) { obj[`key_${currentLevel}_${i}`] = generateDeepObject(levels, breadth, currentLevel + 1); // Add some null values if (Math.random() > 0.8) { obj[`null_key_${currentLevel}_${i}`] = null; } } return obj; }
Tests:
Onyx fastMerge
const ONYX_INTERNALS__REPLACE_OBJECT_MARK = 'ONYX_INTERNALS__REPLACE_OBJECT_MARK'; function fastMergeOriginal(target, source, options, metadata, basePath = []) { if (!metadata) { metadata = { replaceNullPatches: [], }; } if (Array.isArray(source) || source === null || source === undefined) { return {result: source, replaceNullPatches: metadata.replaceNullPatches}; } const optionsWithDefaults = { shouldRemoveNestedNulls: options?.shouldRemoveNestedNulls ?? false, objectRemovalMode: options?.objectRemovalMode ?? 'none', }; const mergedValue = mergeObjectOriginal(target, source, optionsWithDefaults, metadata, basePath); return {result: mergedValue, replaceNullPatches: metadata.replaceNullPatches}; } function mergeObjectOriginal(target, source, options, metadata, basePath) { const destination = {}; const targetObject = isMergeableObjectOriginal(target) ? target : undefined; if (targetObject) { Object.keys(targetObject).forEach((key) => { const targetProperty = targetObject?.[key]; const sourceProperty = source?.[key]; const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && (targetProperty === null || sourceProperty === null); if (targetProperty === undefined || shouldOmitNullishProperty) { return; } destination[key] = targetProperty; }); } Object.keys(source).forEach((key) => { let targetProperty = targetObject?.[key]; const sourceProperty = source[key]; const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && sourceProperty === null; if (sourceProperty === undefined || shouldOmitNullishProperty) { return; } if (!isMergeableObjectOriginal(sourceProperty)) { destination[key] = sourceProperty; return; } if (options.objectRemovalMode === 'mark' && targetProperty === null) { targetProperty = {[ONYX_INTERNALS__REPLACE_OBJECT_MARK]: true}; metadata.replaceNullPatches.push([[...basePath, key], {...sourceProperty}]); } if (options.objectRemovalMode === 'replace' && sourceProperty[ONYX_INTERNALS__REPLACE_OBJECT_MARK]) { const sourcePropertyWithoutMark = {...sourceProperty}; delete sourcePropertyWithoutMark.ONYX_INTERNALS__REPLACE_OBJECT_MARK; destination[key] = sourcePropertyWithoutMark; return; } destination[key] = fastMergeOriginal(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result; }); return destination; } function isMergeableObjectOriginal(value) { const isNonNullObject = value != null ? typeof value === 'object' : false; return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value); } const data1 = generateDeepObject(5, 3); const data2 = generateDeepObject(5, 3); const arrayData1 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 10}, (_, j) => `tag_${j}`), metadata: { created: new Date(), updated: Math.random() > 0.5 ? null : new Date(), author: Math.random() > 0.3 ? `user_${i}` : null } })); const arrayData2 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 8}, (_, j) => `new_tag_${j}`), metadata: { created: new Date(), updated: new Date(), author: `new_user_${i}`, deleted: Math.random() > 0.7 ? null : `reason_${i}` } })); // Test fastMergeOriginal(data1, data2, { shouldRemoveNestedNulls: true }); fastMergeOriginal(arrayData1, arrayData2, { shouldRemoveNestedNulls: true });
Optimized fastMerge
function fastMergeOptimized(target, source, options, metadata = { replaceNullPatches: [] }, basePath = []) { if (source === null || source === undefined || Array.isArray(source)) { return { result: source, replaceNullPatches: metadata.replaceNullPatches }; } const optionsWithDefaults = { shouldRemoveNestedNulls: options?.shouldRemoveNestedNulls ?? false, objectRemovalMode: options?.objectRemovalMode ?? 'none', }; const mergedValue = mergeObjectOptimized(target, source, optionsWithDefaults, metadata, basePath); return { result: mergedValue, replaceNullPatches: metadata.replaceNullPatches }; } function mergeObjectOptimized(target, source, options, metadata, basePath) { const destination = Object.create(null); const targetObject = isMergeableObjectOptimized(target) ? target : undefined; const { shouldRemoveNestedNulls, objectRemovalMode } = options; if (targetObject) { const targetKeys = Object.keys(targetObject); for (let i = 0; i < targetKeys.length; i++) { const key = targetKeys[i]; const targetProperty = targetObject[key]; const sourceProperty = source[key]; const shouldOmit = shouldRemoveNestedNulls && (targetProperty === null || sourceProperty === null); if (targetProperty !== undefined && !shouldOmit) { destination[key] = targetProperty; } } } const sourceKeys = Object.keys(source); for (let i = 0; i < sourceKeys.length; i++) { const key = sourceKeys[i]; let targetProperty = targetObject?.[key]; const sourceProperty = source[key]; if (sourceProperty === undefined) continue; const shouldOmitNullish = shouldRemoveNestedNulls && sourceProperty === null; if (shouldOmitNullish) continue; if (!isMergeableObjectOptimized(sourceProperty)) { destination[key] = sourceProperty; continue; } if (objectRemovalMode === 'mark' && targetProperty === null) { targetProperty = { [ONYX_INTERNALS__REPLACE_OBJECT_MARK]: true }; metadata.replaceNullPatches.push([basePath.concat(key), { ...sourceProperty }]); } if (objectRemovalMode === 'replace' && sourceProperty[ONYX_INTERNALS__REPLACE_OBJECT_MARK]) { const { [ONYX_INTERNALS__REPLACE_OBJECT_MARK]: _, ...cleanSource } = sourceProperty; destination[key] = cleanSource; continue; } const newPath = basePath.length === 0 ? [key] : basePath.concat(key); destination[key] = fastMergeOptimized(targetProperty, sourceProperty, options, metadata, newPath).result; } return destination; } const objectCtorString = Object.prototype.constructor.toString(); function isMergeableObjectOptimized(value) { if (value === null || typeof value !== 'object') return false; const proto = Object.getPrototypeOf(value); if (proto === null) return true; return proto.constructor && proto.constructor.toString() === objectCtorString; } const data1 = generateDeepObject(5, 3); const data2 = generateDeepObject(5, 3); const arrayData1 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 10}, (_, j) => `tag_${j}`), metadata: { created: new Date(), updated: Math.random() > 0.5 ? null : new Date(), author: Math.random() > 0.3 ? `user_${i}` : null } })); const arrayData2 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 8}, (_, j) => `new_tag_${j}`), metadata: { created: new Date(), updated: new Date(), author: `new_user_${i}`, deleted: Math.random() > 0.7 ? null : `reason_${i}` } })); // Test fastMergeOptimized(data1, data2, { shouldRemoveNestedNulls: true }); fastMergeOptimized(arrayData1, arrayData2, { shouldRemoveNestedNulls: true });
Mutative
function mutativeDeepMerge(base, updates) { return Mutative.create(base, draft => { function applyUpdates(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) { const sourceVal = source[key]; const targetVal = target[key]; if (isMergeableObject(sourceVal) && isMergeableObject(targetVal)) { applyUpdates(targetVal, sourceVal); } else { target[key] = sourceVal; } } } } applyUpdates(draft, updates); }); } function isMergeableObject(value) { const isNonNullObject = value != null ? typeof value === 'object' : false; return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value); } const data1 = generateDeepObject(5, 3); const data2 = generateDeepObject(5, 3); const arrayData1 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 10}, (_, j) => `tag_${j}`), metadata: { created: new Date(), updated: Math.random() > 0.5 ? null : new Date(), author: Math.random() > 0.3 ? `user_${i}` : null } })); const arrayData2 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 8}, (_, j) => `new_tag_${j}`), metadata: { created: new Date(), updated: new Date(), author: `new_user_${i}`, deleted: Math.random() > 0.7 ? null : `reason_${i}` } })); mutativeDeepMerge(data1, data2); mutativeDeepMerge(arrayData1, arrayData2);
Immutable-js
function immutableDeepMerge(base, updates) { const baseImmutable = Immutable.fromJS(base); const updatesImmutable = Immutable.fromJS(updates); const merged = Immutable.mergeDeep(baseImmutable, updatesImmutable); return merged.toJS(); } const data1 = generateDeepObject(5, 3); const data2 = generateDeepObject(5, 3); const arrayData1 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 10}, (_, j) => `tag_${j}`), metadata: { created: new Date(), updated: Math.random() > 0.5 ? null : new Date(), author: Math.random() > 0.3 ? `user_${i}` : null } })); const arrayData2 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 8}, (_, j) => `new_tag_${j}`), metadata: { created: new Date(), updated: new Date(), author: `new_user_${i}`, deleted: Math.random() > 0.7 ? null : `reason_${i}` } })); immutableDeepMerge(data1, data2); immutableDeepMerge(arrayData1, arrayData2);
ImmerJS
function immerDeepMerge(base, updates) { return immer.produce(base, draft => { function applyUpdates(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) { const sourceVal = source[key]; const targetVal = target[key]; // If both are objects (not arrays, null, or special objects), merge recursively if (isMergeableObject(sourceVal) && isMergeableObject(targetVal)) { applyUpdates(targetVal, sourceVal); } else { // Otherwise replace the value target[key] = sourceVal; } } } } applyUpdates(draft, updates); }); } function isMergeableObject(value) { const isNonNullObject = value != null ? typeof value === 'object' : false; return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value); } const data1 = generateDeepObject(5, 3); const data2 = generateDeepObject(5, 3); const arrayData1 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 10}, (_, j) => `tag_${j}`), metadata: { created: new Date(), updated: Math.random() > 0.5 ? null : new Date(), author: Math.random() > 0.3 ? `user_${i}` : null } })); const arrayData2 = Array.from({length: 1000}, (_, i) => ({ id: i, nested: generateDeepObject(3, 2), tags: Array.from({length: 8}, (_, j) => `new_tag_${j}`), metadata: { created: new Date(), updated: new Date(), author: `new_user_${i}`, deleted: Math.random() > 0.7 ? null : `reason_${i}` } })); immerDeepMerge(data1, data2); immerDeepMerge(arrayData1, arrayData2);