diff --git a/lib/Onyx.js b/lib/Onyx.js index 0337555e..1d6778c7 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -6,7 +6,7 @@ import Storage from './storage'; import * as Logger from './Logger'; import cache from './OnyxCache'; import createDeferredTask from './createDeferredTask'; -import mergeWithCustomized from './mergeWithCustomized'; +import fastMerge from './fastMerge'; // Keeps track of the last connectionID that was used so we can keep incrementing it let lastConnectionID = 0; @@ -761,7 +761,9 @@ function applyMerge(key, data) { if (_.isObject(data) || _.every(mergeValues, _.isObject)) { // Object values are merged one after the other return _.reduce(mergeValues, (modifiedData, mergeValue) => { - const newData = mergeWithCustomized({}, modifiedData, mergeValue); + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method + const newData = Object.assign({}, fastMerge(modifiedData, mergeValue)); // We will also delete any object keys that are undefined or null. // Deleting keys is not supported by AsyncStorage so we do it this way. @@ -832,7 +834,7 @@ function initializeWithDefaultKeyStates() { .then((pairs) => { const asObject = _.object(pairs); - const merged = mergeWithCustomized(asObject, defaultKeyStates); + const merged = fastMerge(asObject, defaultKeyStates); cache.merge(merged); _.each(merged, (val, key) => keyChanged(key, val)); }); diff --git a/lib/OnyxCache.js b/lib/OnyxCache.js index 35a2eaaa..5b3fd8f7 100644 --- a/lib/OnyxCache.js +++ b/lib/OnyxCache.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import mergeWithCustomized from './mergeWithCustomized'; +import fastMerge from './fastMerge'; const isDefined = _.negate(_.isUndefined); @@ -110,7 +110,9 @@ class OnyxCache { * @param {Record} data - a map of (cache) key - values */ merge(data) { - this.storageMap = mergeWithCustomized({}, this.storageMap, data); + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method + this.storageMap = Object.assign({}, fastMerge(this.storageMap, data)); const storageKeys = this.getAllKeys(); const mergedKeys = _.keys(data); diff --git a/lib/fastMerge.js b/lib/fastMerge.js new file mode 100644 index 00000000..a79825d4 --- /dev/null +++ b/lib/fastMerge.js @@ -0,0 +1,66 @@ +// Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1 + +/** + * @param {mixed} val + * @returns {boolean} +*/ +function isMergeableObject(val) { + const nonNullObject = val != null ? typeof val === 'object' : false; + return (nonNullObject + && Object.prototype.toString.call(val) !== '[object RegExp]' + && Object.prototype.toString.call(val) !== '[object Date]'); +} + +/** + * @param {Object} target + * @param {Object} source + * @returns {Object} +*/ +function mergeObject(target, source) { + const destination = {}; + if (isMergeableObject(target)) { + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line rulesdir/prefer-underscore-method + const targetKeys = Object.keys(target); + for (let i = 0; i < targetKeys.length; ++i) { + const key = targetKeys[i]; + destination[key] = target[key]; + } + } + + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line rulesdir/prefer-underscore-method + const sourceKeys = Object.keys(source); + for (let i = 0; i < sourceKeys.length; ++i) { + const key = sourceKeys[i]; + if (source[key] === undefined) { + // eslint-disable-next-line no-continue + continue; + } + if (!isMergeableObject(source[key]) || !target[key]) { + destination[key] = source[key]; + } else { + // eslint-disable-next-line no-use-before-define + destination[key] = fastMerge(target[key], source[key]); + } + } + + return destination; +} + +/** + * @param {Object|Array} target + * @param {Object|Array} source + * @returns {Object|Array} +*/ +function fastMerge(target, source) { + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line rulesdir/prefer-underscore-method + const array = Array.isArray(source); + if (array) { + return source; + } + return mergeObject(target, source); +} + +export default fastMerge; diff --git a/lib/mergeWithCustomized.js b/lib/mergeWithCustomized.js deleted file mode 100644 index 4a7ffe42..00000000 --- a/lib/mergeWithCustomized.js +++ /dev/null @@ -1,26 +0,0 @@ -import lodashMergeWith from 'lodash/mergeWith'; - -/** - * When merging 2 objects into onyx that contain an array, we want to completely replace the array instead of the default - * behavior which is to merge each item by its index. - * ie: - * merge({a: [1]}, {a: [2,3]}): - * - In the default implementation would produce {a:[1,3]} - * - With this function would produce: {a: [2,3]} - * @param {*} objValue - * @param {*} srcValue - * @return {*} - */ -// eslint-disable-next-line rulesdir/prefer-early-return -function customizerForMergeWith(objValue, srcValue) { - // eslint-disable-next-line rulesdir/prefer-underscore-method - if (Array.isArray(objValue)) { - return srcValue; - } -} - -function mergeWithCustomized(...args) { - return lodashMergeWith(...args, customizerForMergeWith); -} - -export default mergeWithCustomized; diff --git a/lib/storage/providers/LocalForage.js b/lib/storage/providers/LocalForage.js index 64cded66..ea85a915 100644 --- a/lib/storage/providers/LocalForage.js +++ b/lib/storage/providers/LocalForage.js @@ -7,7 +7,7 @@ import localforage from 'localforage'; import _ from 'underscore'; import SyncQueue from '../../SyncQueue'; -import mergeWithCustomized from '../../mergeWithCustomized'; +import fastMerge from '../../fastMerge'; localforage.config({ name: 'OnyxDB', @@ -24,7 +24,10 @@ const provider = { return localforage.getItem(key) .then((existingValue) => { const newValue = _.isObject(existingValue) - ? mergeWithCustomized({}, existingValue, value) + + // lodash adds a small overhead so we don't use it here + // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method + ? Object.assign({}, fastMerge(existingValue, value)) : value; return localforage.setItem(key, newValue); });