Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
Run results for:
Immutability comparison
immerjs, Proxy, fastMerge, deepEqual
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/141.0.0.0 Safari/537.36
Browser:
Chrome 141
Operating system:
Mac OS X 10.15.7
Device Platform:
Desktop
Date tested:
7 months ago
Test name
Executions per second
mutation + equal
18343.5 Ops/sec
Immerjs + reference check
23248.9 Ops/sec
manual immutable (no lib)
35786.9 Ops/sec
Proxy
36086.3 Ops/sec
deepClone + deepEqual
4993.1 Ops/sec
Onyx fast merge
33167.6 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 Preparation code:
// Complex nested state structure (5 levels deep) const createComplexState = () => ({ user: { profile: { personal: { details: { info: { name: 'John Doe', age: 30, preferences: ['reading', 'coding', 'music'] } } } } }, app: { ui: { theme: 'dark', layout: 'grid' } }, data: { items: [ { id: 1, name: 'Item 1', meta: { category: 'A', tags: ['tag1'] } }, { id: 2, name: 'Item 2', meta: { category: 'B', tags: ['tag2'] } } ] } }); // Simple state structure (1-2 levels) const createSimpleState = () => ({ counter: 0, flag: true, items: ['a', 'b', 'c'] }); // Store original references for comparison var originalComplexState = createComplexState(); var originalSimpleState = createSimpleState(); // States for manual mutation testing var mutableComplexState = createComplexState(); var mutableSimpleState = createSimpleState();
Tests:
mutation + equal
// Create realistic Onyx state (30 keys, 5 levels, 300 items) const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); // ShallowEqual implementation (like Onyx uses) function shallowEqual(a, b) { if (a === b) return true; if (!a || !b) return false; if (typeof a !== 'object' || typeof b !== 'object') return a === b; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (let i = 0; i < keysA.length; i++) { if (a[keysA[i]] !== b[keysA[i]]) return false; } return true; } // Store before state const beforeState = createRealisticState(); const prevValue = beforeState; // Mutate (simulate merge/set) const afterState = createRealisticState(); afterState.user.profile.personal.details.info.age = 31; afterState.reports[0].name = 'Modified Report'; afterState.policy.settings.approval.rules.level1.amount = 200; // Check for changes (what useOnyx does) const hasChanged = !shallowEqual(prevValue, afterState);
Immerjs + reference check
const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); const originalState = createRealisticState(); // Create new immutable state with Immer const newState = immer.produce(originalState, draft => { draft.user.profile.personal.details.info.age = 31; draft.reports[0].name = 'Modified Report'; draft.policy.settings.approval.rules.level1.amount = 200; }); // Fast reference equality check const hasChanged = originalState !== newState; const userChanged = originalState.user !== newState.user; const reportsChanged = originalState.reports !== newState.reports; // This is what useOnyx would do - just check if reference changed const shouldUpdate = hasChanged;
manual immutable (no lib)
const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); const originalState = createRealisticState(); // Manual immutable updates - only recreate changed paths const newState = { ...originalState, user: { ...originalState.user, profile: { ...originalState.user.profile, personal: { ...originalState.user.profile.personal, details: { ...originalState.user.profile.personal.details, info: { ...originalState.user.profile.personal.details.info, age: 31 } } } } }, reports: [ { ...originalState.reports[0], name: 'Modified Report' }, ...originalState.reports.slice(1) ], policy: { ...originalState.policy, settings: { ...originalState.policy.settings, approval: { ...originalState.policy.settings.approval, rules: { ...originalState.policy.settings.approval.rules, level1: { ...originalState.policy.settings.approval.rules.level1, amount: 200 } } } } } }; // Fast reference equality check const hasChanged = originalState !== newState;
Proxy
const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); // Create Proxy with change tracking function createChangeTrackingProxy(obj, pathPrefix = '') { let isDirty = false; const metadata = { isDirty: () => isDirty }; function makeProxy(target, path) { return new Proxy(target, { set(obj, prop, value) { if (obj[prop] !== value) { isDirty = true; } obj[prop] = value; return true; }, get(obj, prop) { if (prop === '__metadata__') return metadata; const value = obj[prop]; if (value && typeof value === 'object' && !Array.isArray(value)) { return makeProxy(value, `${path}.${prop}`); } return value; } }); } return makeProxy(obj, pathPrefix); } const state = createChangeTrackingProxy(createRealisticState()); // Mutate through proxy state.user.profile.personal.details.info.age = 31; state.reports[0].name = 'Modified Report'; state.policy.settings.approval.rules.level1.amount = 200; // Check if dirty const hasChanged = state.__metadata__.isDirty();
deepClone + deepEqual
const fastEquals = window['fast-equals']; const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); // Store deep clone of before state const beforeState = JSON.parse(JSON.stringify(createRealisticState())); // Mutate const afterState = createRealisticState(); afterState.user.profile.personal.details.info.age = 31; afterState.reports[0].name = 'Modified Report'; afterState.policy.settings.approval.rules.level1.amount = 200; // Deep equality check const hasChanged = !fastEquals.deepEqual(beforeState, afterState);
Onyx fast merge
// Onyx merge utilities const ONYX_INTERNALS__REPLACE_OBJECT_MARK = 'ONYX_INTERNALS__REPLACE_OBJECT_MARK'; function isMergeableObject(value) { const isNonNullObject = value != null ? typeof value === 'object' : false; return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value); } function mergeObject(target, source, options, metadata, basePath) { const destination = {}; const targetObject = isMergeableObject(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 (!isMergeableObject(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] = fastMerge(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result; }); return destination; } function fastMerge(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 = mergeObject(target, source, optionsWithDefaults, metadata, basePath); return {result: mergedValue, replaceNullPatches: metadata.replaceNullPatches}; } function shallowEqual(a, b) { if (a === b) return true; if (!a || !b) return false; if (typeof a !== 'object' || typeof b !== 'object') return a === b; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (let i = 0; i < keysA.length; i++) { if (a[keysA[i]] !== b[keysA[i]]) return false; } return true; } const createRealisticState = () => ({ user: { accountID: 12345, profile: { personal: { details: { info: { name: 'John Doe', age: 30, email: 'john@example.com' } } } } }, reports: Array.from({ length: 300 }, (_, i) => ({ reportID: `r${i}`, name: `Report ${i}`, participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } })), transactions: Array.from({ length: 300 }, (_, i) => ({ transactionID: `t${i}`, amount: Math.random() * 1000, currency: 'USD' })), policy: { settings: { approval: { rules: { level1: { amount: 100 }, level2: { amount: 500 } } } } } }); // Simulate what Onyx does: merge updates into existing state const originalState = createRealisticState(); // Updates to merge (like what comes from Onyx.merge) const updates = { user: { profile: { personal: { details: { info: { age: 31 } } } } }, reports: [ { reportID: 'r0', name: 'Modified Report', participants: [1, 2, 3], lastMessage: { text: 'Hello', timestamp: Date.now() } } ], policy: { settings: { approval: { rules: { level1: { amount: 200 } } } } } }; // Merge using Onyx fastMerge const mergeResult = fastMerge(originalState, updates); const newState = mergeResult.result; // Check for changes using shallowEqual (what useOnyx does) const hasChanged = !shallowEqual(originalState, newState); const userChanged = !shallowEqual(originalState.user, newState.user); const reportsChanged = !shallowEqual(originalState.reports, newState.reports);