Skip to content

Commit 55091ed

Browse files
EvanHahn-Signaljosh-signal
authored andcommitted
Avoid unnecessary re-render on CHECK_NETWORK_STATUS
1 parent b70b7a2 commit 55091ed

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

ts/state/ducks/network.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { SocketStatus } from '../../types/SocketStatus';
55
import { trigger } from '../../shims/events';
6+
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
67

78
// State
89

@@ -89,11 +90,12 @@ export function reducer(
8990
if (action.type === CHECK_NETWORK_STATUS) {
9091
const { isOnline, socketStatus } = action.payload;
9192

92-
return {
93-
...state,
93+
// This action is dispatched frequently. We avoid allocating a new object if nothing
94+
// has changed to avoid an unnecessary re-render.
95+
return assignWithNoUnnecessaryAllocation(state, {
9496
isOnline,
9597
socketStatus,
96-
};
98+
});
9799
}
98100

99101
if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2020 Signal Messenger, LLC
2+
// SPDX-License-Identifier: AGPL-3.0-only
3+
4+
import { assert } from 'chai';
5+
6+
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
7+
8+
describe('assignWithNoUnnecessaryAllocation', () => {
9+
interface Person {
10+
name?: string;
11+
age?: number;
12+
}
13+
14+
it('returns the same object if there are no modifications', () => {
15+
const empty = {};
16+
assert.strictEqual(assignWithNoUnnecessaryAllocation(empty, {}), empty);
17+
18+
const obj = {
19+
foo: 'bar',
20+
baz: 'qux',
21+
und: undefined,
22+
};
23+
assert.strictEqual(assignWithNoUnnecessaryAllocation(obj, {}), obj);
24+
assert.strictEqual(
25+
assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' }),
26+
obj
27+
);
28+
assert.strictEqual(
29+
assignWithNoUnnecessaryAllocation(obj, { baz: 'qux' }),
30+
obj
31+
);
32+
assert.strictEqual(
33+
assignWithNoUnnecessaryAllocation(obj, { und: undefined }),
34+
obj
35+
);
36+
});
37+
38+
it('returns a new object if there are modifications', () => {
39+
const empty: Person = {};
40+
assert.deepEqual(
41+
assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' }),
42+
{ name: 'Bert' }
43+
);
44+
assert.deepEqual(assignWithNoUnnecessaryAllocation(empty, { age: 8 }), {
45+
age: 8,
46+
});
47+
assert.deepEqual(
48+
assignWithNoUnnecessaryAllocation(empty, { name: undefined }),
49+
{
50+
name: undefined,
51+
}
52+
);
53+
54+
const obj: Person = { name: 'Ernie' };
55+
assert.deepEqual(
56+
assignWithNoUnnecessaryAllocation(obj, { name: 'Big Bird' }),
57+
{
58+
name: 'Big Bird',
59+
}
60+
);
61+
assert.deepEqual(assignWithNoUnnecessaryAllocation(obj, { age: 9 }), {
62+
name: 'Ernie',
63+
age: 9,
64+
});
65+
assert.deepEqual(
66+
assignWithNoUnnecessaryAllocation(obj, { age: undefined }),
67+
{
68+
name: 'Ernie',
69+
age: undefined,
70+
}
71+
);
72+
});
73+
74+
it('only performs a shallow comparison', () => {
75+
const obj = { foo: { bar: 'baz' } };
76+
assert.notStrictEqual(
77+
assignWithNoUnnecessaryAllocation(obj, { foo: { bar: 'baz' } }),
78+
obj
79+
);
80+
});
81+
82+
it("doesn't modify the original object when there are no modifications", () => {
83+
const empty = {};
84+
assignWithNoUnnecessaryAllocation(empty, {});
85+
assert.deepEqual(empty, {});
86+
87+
const obj = { foo: 'bar' };
88+
assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' });
89+
assert.deepEqual(obj, { foo: 'bar' });
90+
});
91+
92+
it("doesn't modify the original object when there are modifications", () => {
93+
const empty: Person = {};
94+
assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' });
95+
assert.deepEqual(empty, {});
96+
97+
const obj = { foo: 'bar' };
98+
assignWithNoUnnecessaryAllocation(obj, { foo: 'baz' });
99+
assert.deepEqual(obj, { foo: 'bar' });
100+
});
101+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2020 Signal Messenger, LLC
2+
// SPDX-License-Identifier: AGPL-3.0-only
3+
4+
import { has } from 'lodash';
5+
6+
/**
7+
* This function is like `Object.assign` but won't create a new object if we don't need
8+
* to. This is purely a performance optimization.
9+
*
10+
* This is useful in places where we don't want to create a new object unnecessarily,
11+
* like in reducers where we might cause an unnecessary re-render.
12+
*
13+
* See the tests for the specifics of how this works.
14+
*/
15+
// We want this to work with any object, so we allow `object` here.
16+
// eslint-disable-next-line @typescript-eslint/ban-types
17+
export function assignWithNoUnnecessaryAllocation<T extends object>(
18+
obj: Readonly<T>,
19+
source: Readonly<Partial<T>>
20+
): T {
21+
// We want to bail early so we use `for .. in` instead of `Object.keys` or similar.
22+
// eslint-disable-next-line no-restricted-syntax
23+
for (const key in source) {
24+
if (!has(source, key)) {
25+
continue;
26+
}
27+
if (!(key in obj) || obj[key] !== source[key]) {
28+
return { ...obj, ...source };
29+
}
30+
}
31+
return obj;
32+
}

0 commit comments

Comments
 (0)