diff --git a/ts/state/ducks/network.ts b/ts/state/ducks/network.ts index 2406e3a05..392ca14e2 100644 --- a/ts/state/ducks/network.ts +++ b/ts/state/ducks/network.ts @@ -3,6 +3,7 @@ import { SocketStatus } from '../../types/SocketStatus'; import { trigger } from '../../shims/events'; +import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; // State @@ -89,11 +90,12 @@ export function reducer( if (action.type === CHECK_NETWORK_STATUS) { const { isOnline, socketStatus } = action.payload; - return { - ...state, + // This action is dispatched frequently. We avoid allocating a new object if nothing + // has changed to avoid an unnecessary re-render. + return assignWithNoUnnecessaryAllocation(state, { isOnline, socketStatus, - }; + }); } if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) { diff --git a/ts/test-both/util/assignWithNoUnnecessaryAllocation_test.ts b/ts/test-both/util/assignWithNoUnnecessaryAllocation_test.ts new file mode 100644 index 000000000..2a3689213 --- /dev/null +++ b/ts/test-both/util/assignWithNoUnnecessaryAllocation_test.ts @@ -0,0 +1,101 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; + +describe('assignWithNoUnnecessaryAllocation', () => { + interface Person { + name?: string; + age?: number; + } + + it('returns the same object if there are no modifications', () => { + const empty = {}; + assert.strictEqual(assignWithNoUnnecessaryAllocation(empty, {}), empty); + + const obj = { + foo: 'bar', + baz: 'qux', + und: undefined, + }; + assert.strictEqual(assignWithNoUnnecessaryAllocation(obj, {}), obj); + assert.strictEqual( + assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' }), + obj + ); + assert.strictEqual( + assignWithNoUnnecessaryAllocation(obj, { baz: 'qux' }), + obj + ); + assert.strictEqual( + assignWithNoUnnecessaryAllocation(obj, { und: undefined }), + obj + ); + }); + + it('returns a new object if there are modifications', () => { + const empty: Person = {}; + assert.deepEqual( + assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' }), + { name: 'Bert' } + ); + assert.deepEqual(assignWithNoUnnecessaryAllocation(empty, { age: 8 }), { + age: 8, + }); + assert.deepEqual( + assignWithNoUnnecessaryAllocation(empty, { name: undefined }), + { + name: undefined, + } + ); + + const obj: Person = { name: 'Ernie' }; + assert.deepEqual( + assignWithNoUnnecessaryAllocation(obj, { name: 'Big Bird' }), + { + name: 'Big Bird', + } + ); + assert.deepEqual(assignWithNoUnnecessaryAllocation(obj, { age: 9 }), { + name: 'Ernie', + age: 9, + }); + assert.deepEqual( + assignWithNoUnnecessaryAllocation(obj, { age: undefined }), + { + name: 'Ernie', + age: undefined, + } + ); + }); + + it('only performs a shallow comparison', () => { + const obj = { foo: { bar: 'baz' } }; + assert.notStrictEqual( + assignWithNoUnnecessaryAllocation(obj, { foo: { bar: 'baz' } }), + obj + ); + }); + + it("doesn't modify the original object when there are no modifications", () => { + const empty = {}; + assignWithNoUnnecessaryAllocation(empty, {}); + assert.deepEqual(empty, {}); + + const obj = { foo: 'bar' }; + assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' }); + assert.deepEqual(obj, { foo: 'bar' }); + }); + + it("doesn't modify the original object when there are modifications", () => { + const empty: Person = {}; + assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' }); + assert.deepEqual(empty, {}); + + const obj = { foo: 'bar' }; + assignWithNoUnnecessaryAllocation(obj, { foo: 'baz' }); + assert.deepEqual(obj, { foo: 'bar' }); + }); +}); diff --git a/ts/util/assignWithNoUnnecessaryAllocation.ts b/ts/util/assignWithNoUnnecessaryAllocation.ts new file mode 100644 index 000000000..049c9faee --- /dev/null +++ b/ts/util/assignWithNoUnnecessaryAllocation.ts @@ -0,0 +1,32 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { has } from 'lodash'; + +/** + * This function is like `Object.assign` but won't create a new object if we don't need + * to. This is purely a performance optimization. + * + * This is useful in places where we don't want to create a new object unnecessarily, + * like in reducers where we might cause an unnecessary re-render. + * + * See the tests for the specifics of how this works. + */ +// We want this to work with any object, so we allow `object` here. +// eslint-disable-next-line @typescript-eslint/ban-types +export function assignWithNoUnnecessaryAllocation( + obj: Readonly, + source: Readonly> +): T { + // We want to bail early so we use `for .. in` instead of `Object.keys` or similar. + // eslint-disable-next-line no-restricted-syntax + for (const key in source) { + if (!has(source, key)) { + continue; + } + if (!(key in obj) || obj[key] !== source[key]) { + return { ...obj, ...source }; + } + } + return obj; +}