[Fiber] Bail out of diffing wide objects and arrays (#34742)
Sebastian "Sebbie" Silbermann committed
Oct 6, 2025 at 01:13 UTC
1be3ce9996f05ceb74fd8c11f08a84a5e57098f3
2 files changed
+395
-3
packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
new
+327
@@ -0,0 +1,327 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+let React;
12
+let ReactNoop;
13
+let Scheduler;
14
+let act;
15
+let useEffect;
16
+
17
+describe('ReactPerformanceTracks', () => {
18
+ beforeEach(() => {
19
+ Object.defineProperty(performance, 'measure', {
20
+ value: jest.fn(),
21
+ configurable: true,
22
+ });
23
+ console.timeStamp = () => {};
24
+ jest.spyOn(console, 'timeStamp').mockImplementation(() => {});
25
+
26
+ jest.resetModules();
27
+
28
+ React = require('react');
29
+ ReactNoop = require('react-noop-renderer');
30
+ Scheduler = require('scheduler');
31
+ act = require('internal-test-utils').act;
32
+ useEffect = React.useEffect;
33
+ });
34
+
35
+ // @gate __DEV__ && enableComponentPerformanceTrack
36
+ it('shows a hint if an update is triggered by a deeply equal object', async () => {
37
+ const App = function App({items}) {
38
+ Scheduler.unstable_advanceTime(10);
39
+ useEffect(() => {}, [items]);
40
+ };
41
+
42
+ Scheduler.unstable_advanceTime(1);
43
+ const items = ['one', 'two'];
44
+ await act(() => {
45
+ ReactNoop.render(<App items={items} />);
46
+ });
47
+
48
+ expect(performance.measure.mock.calls).toEqual([
49
+ [
50
+ 'Mount',
51
+ {
52
+ detail: {
53
+ devtools: {
54
+ color: 'warning',
55
+ properties: null,
56
+ tooltipText: 'Mount',
57
+ track: 'Components ⚛',
58
+ },
59
+ },
60
+ end: 11,
61
+ start: 1,
62
+ },
63
+ ],
64
+ ]);
65
+ performance.measure.mockClear();
66
+
67
+ Scheduler.unstable_advanceTime(10);
68
+ await act(() => {
69
+ ReactNoop.render(<App items={items.concat('4')} />);
70
+ });
71
+
72
+ expect(performance.measure.mock.calls).toEqual([
73
+ [
74
+ 'App',
75
+ {
76
+ detail: {
77
+ devtools: {
78
+ color: 'primary-dark',
79
+ properties: [
80
+ ['Changed Props', ''],
81
+ [' items', 'Array'],
82
+ ['+ 2', '…'],
83
+ ],
84
+ tooltipText: 'App',
85
+ track: 'Components ⚛',
86
+ },
87
+ },
88
+ end: 31,
89
+ start: 21,
90
+ },
91
+ ],
92
+ ]);
93
+ });
94
+
95
+ // @gate __DEV__ && enableComponentPerformanceTrack
96
+ it('bails out of diffing wide arrays', async () => {
97
+ const App = function App({items}) {
98
+ Scheduler.unstable_advanceTime(10);
99
+ React.useEffect(() => {}, [items]);
100
+ };
101
+
102
+ Scheduler.unstable_advanceTime(1);
103
+ const items = Array.from({length: 1000}, (_, i) => i);
104
+ await act(() => {
105
+ ReactNoop.render(<App items={items} />);
106
+ });
107
+
108
+ expect(performance.measure.mock.calls).toEqual([
109
+ [
110
+ 'Mount',
111
+ {
112
+ detail: {
113
+ devtools: {
114
+ color: 'warning',
115
+ properties: null,
116
+ tooltipText: 'Mount',
117
+ track: 'Components ⚛',
118
+ },
119
+ },
120
+ end: 11,
121
+ start: 1,
122
+ },
123
+ ],
124
+ ]);
125
+ performance.measure.mockClear();
126
+
127
+ Scheduler.unstable_advanceTime(10);
128
+ await act(() => {
129
+ ReactNoop.render(<App items={items.concat('-1')} />);
130
+ });
131
+
132
+ expect(performance.measure.mock.calls).toEqual([
133
+ [
134
+ 'App',
135
+ {
136
+ detail: {
137
+ devtools: {
138
+ color: 'primary-dark',
139
+ properties: [
140
+ ['Changed Props', ''],
141
+ [' items', 'Array'],
142
+ [
143
+ 'Previous object has more than 100 properties. React will not attempt to diff objects with too many properties.',
144
+ '',
145
+ ],
146
+ [
147
+ 'Next object has more than 100 properties. React will not attempt to diff objects with too many properties.',
148
+ '',
149
+ ],
150
+ ],
151
+ tooltipText: 'App',
152
+ track: 'Components ⚛',
153
+ },
154
+ },
155
+ end: 31,
156
+ start: 21,
157
+ },
158
+ ],
159
+ ]);
160
+ });
161
+
162
+ // @gate __DEV__ && enableComponentPerformanceTrack
163
+ it('does not show all properties of wide objects', async () => {
164
+ const App = function App({items}) {
165
+ Scheduler.unstable_advanceTime(10);
166
+ React.useEffect(() => {}, [items]);
167
+ };
168
+
169
+ Scheduler.unstable_advanceTime(1);
170
+ await act(() => {
171
+ ReactNoop.render(<App data={{buffer: null}} />);
172
+ });
173
+
174
+ expect(performance.measure.mock.calls).toEqual([
175
+ [
176
+ 'Mount',
177
+ {
178
+ detail: {
179
+ devtools: {
180
+ color: 'warning',
181
+ properties: null,
182
+ tooltipText: 'Mount',
183
+ track: 'Components ⚛',
184
+ },
185
+ },
186
+ end: 11,
187
+ start: 1,
188
+ },
189
+ ],
190
+ ]);
191
+ performance.measure.mockClear();
192
+
193
+ Scheduler.unstable_advanceTime(10);
194
+
195
+ const bigData = new Uint8Array(1000);
196
+ await act(() => {
197
+ ReactNoop.render(<App data={{buffer: bigData}} />);
198
+ });
199
+
200
+ expect(performance.measure.mock.calls).toEqual([
201
+ [
202
+ 'App',
203
+ {
204
+ detail: {
205
+ devtools: {
206
+ color: 'primary-dark',
207
+ properties: [
208
+ ['Changed Props', ''],
209
+ [' data', ''],
210
+ ['– buffer', 'null'],
211
+ ['+ buffer', 'Uint8Array'],
212
+ ['+ 0', '0'],
213
+ ['+ 1', '0'],
214
+ ['+ 2', '0'],
215
+ ['+ 3', '0'],
216
+ ['+ 4', '0'],
217
+ ['+ 5', '0'],
218
+ ['+ 6', '0'],
219
+ ['+ 7', '0'],
220
+ ['+ 8', '0'],
221
+ ['+ 9', '0'],
222
+ ['+ 10', '0'],
223
+ ['+ 11', '0'],
224
+ ['+ 12', '0'],
225
+ ['+ 13', '0'],
226
+ ['+ 14', '0'],
227
+ ['+ 15', '0'],
228
+ ['+ 16', '0'],
229
+ ['+ 17', '0'],
230
+ ['+ 18', '0'],
231
+ ['+ 19', '0'],
232
+ ['+ 20', '0'],
233
+ ['+ 21', '0'],
234
+ ['+ 22', '0'],
235
+ ['+ 23', '0'],
236
+ ['+ 24', '0'],
237
+ ['+ 25', '0'],
238
+ ['+ 26', '0'],
239
+ ['+ 27', '0'],
240
+ ['+ 28', '0'],
241
+ ['+ 29', '0'],
242
+ ['+ 30', '0'],
243
+ ['+ 31', '0'],
244
+ ['+ 32', '0'],
245
+ ['+ 33', '0'],
246
+ ['+ 34', '0'],
247
+ ['+ 35', '0'],
248
+ ['+ 36', '0'],
249
+ ['+ 37', '0'],
250
+ ['+ 38', '0'],
251
+ ['+ 39', '0'],
252
+ ['+ 40', '0'],
253
+ ['+ 41', '0'],
254
+ ['+ 42', '0'],
255
+ ['+ 43', '0'],
256
+ ['+ 44', '0'],
257
+ ['+ 45', '0'],
258
+ ['+ 46', '0'],
259
+ ['+ 47', '0'],
260
+ ['+ 48', '0'],
261
+ ['+ 49', '0'],
262
+ ['+ 50', '0'],
263
+ ['+ 51', '0'],
264
+ ['+ 52', '0'],
265
+ ['+ 53', '0'],
266
+ ['+ 54', '0'],
267
+ ['+ 55', '0'],
268
+ ['+ 56', '0'],
269
+ ['+ 57', '0'],
270
+ ['+ 58', '0'],
271
+ ['+ 59', '0'],
272
+ ['+ 60', '0'],
273
+ ['+ 61', '0'],
274
+ ['+ 62', '0'],
275
+ ['+ 63', '0'],
276
+ ['+ 64', '0'],
277
+ ['+ 65', '0'],
278
+ ['+ 66', '0'],
279
+ ['+ 67', '0'],
280
+ ['+ 68', '0'],
281
+ ['+ 69', '0'],
282
+ ['+ 70', '0'],
283
+ ['+ 71', '0'],
284
+ ['+ 72', '0'],
285
+ ['+ 73', '0'],
286
+ ['+ 74', '0'],
287
+ ['+ 75', '0'],
288
+ ['+ 76', '0'],
289
+ ['+ 77', '0'],
290
+ ['+ 78', '0'],
291
+ ['+ 79', '0'],
292
+ ['+ 80', '0'],
293
+ ['+ 81', '0'],
294
+ ['+ 82', '0'],
295
+ ['+ 83', '0'],
296
+ ['+ 84', '0'],
297
+ ['+ 85', '0'],
298
+ ['+ 86', '0'],
299
+ ['+ 87', '0'],
300
+ ['+ 88', '0'],
301
+ ['+ 89', '0'],
302
+ ['+ 90', '0'],
303
+ ['+ 91', '0'],
304
+ ['+ 92', '0'],
305
+ ['+ 93', '0'],
306
+ ['+ 94', '0'],
307
+ ['+ 95', '0'],
308
+ ['+ 96', '0'],
309
+ ['+ 97', '0'],
310
+ ['+ 98', '0'],
311
+ ['+ 99', '0'],
312
+ [
313
+ '+ Only 100 properties are shown. React will not log more properties of this object.',
314
+ '',
315
+ ],
316
+ ],
317
+ tooltipText: 'App',
318
+ track: 'Components ⚛',
319
+ },
320
+ },
321
+ end: 31,
322
+ start: 21,
323
+ },
324
+ ],
325
+ ]);
326
+ });
327
+});
packages/shared/ReactPerformanceTrackProperties.js
+68
-3
@@ -18,9 +18,13 @@ const EMPTY_ARRAY = 0;
18
const COMPLEX_ARRAY = 1;
19
const PRIMITIVE_ARRAY = 2; // Primitive values only
20
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
21
+
22
+// Showing wider objects in the devtools is not useful.
23
+const OBJECT_WIDTH_LIMIT = 100;
24
+
25
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
26
let kind: 0 | 1 | 2 | 3 = EMPTY_ARRAY;
23
- for (let i = 0; i < array.length; i++) {
27
+ for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
28
const value = array[i];
29
if (typeof value === 'object' && value !== null) {
30
if (
@@ -55,10 +59,23 @@ export function addObjectToProperties(
59
indent: number,
60
prefix: string,
61
): void {
62
+ let addedProperties = 0;
63
for (const key in object) {
64
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
65
+ addedProperties++;
66
const value = object[key];
67
addValueToProperties(key, value, properties, indent, prefix);
68
+ if (addedProperties >= OBJECT_WIDTH_LIMIT) {
69
+ properties.push([
70
+ prefix +
71
+ '\xa0\xa0'.repeat(indent) +
72
+ 'Only ' +
73
+ OBJECT_WIDTH_LIMIT +
74
+ ' properties are shown. React will not log more properties of this object.',
75
+ '',
76
+ ]);
77
+ break;
78
+ }
79
}
80
}
81
}
@@ -103,7 +120,9 @@ export function addValueToProperties(
120
addValueToProperties('key', key, properties, indent + 1, prefix);
121
}
122
let hasChildren = false;
123
+ let addedProperties = 0;
124
for (const propKey in props) {
125
+ addedProperties++;
126
if (propKey === 'children') {
127
if (
128
props.children != null &&
@@ -123,6 +142,10 @@ export function addValueToProperties(
142
prefix,
143
);
144
}
145
+
146
+ if (addedProperties >= OBJECT_WIDTH_LIMIT) {
147
+ break;
148
+ }
149
}
150
properties.push([
151
'',
@@ -135,16 +158,21 @@ export function addValueToProperties(
158
let objectName = objectToString.slice(8, objectToString.length - 1);
159
if (objectName === 'Array') {
160
const array: Array<any> = (value: any);
161
+ const didTruncate = array.length > OBJECT_WIDTH_LIMIT;
162
const kind = getArrayKind(array);
163
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
140
- desc = JSON.stringify(array);
164
+ desc = JSON.stringify(
165
+ didTruncate
166
+ ? array.slice(0, OBJECT_WIDTH_LIMIT).concat('…')
167
+ : array,
168
+ );
169
break;
170
} else if (kind === ENTRIES_ARRAY) {
171
properties.push([
172
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
173
'',
174
]);
147
- for (let i = 0; i < array.length; i++) {
175
+ for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
176
const entry = array[i];
177
addValueToProperties(
178
entry[0],
@@ -154,6 +182,15 @@ export function addValueToProperties(
182
prefix,
183
);
184
}
185
+ if (didTruncate) {
186
+ addValueToProperties(
187
+ OBJECT_WIDTH_LIMIT.toString(),
188
+ '…',
189
+ properties,
190
+ indent + 1,
191
+ prefix,
192
+ );
193
+ }
194
return;
195
}
196
}
@@ -254,13 +291,39 @@ export function addObjectDiffToProperties(
291
// If a property is added or removed, we just emit the property name and omit the value it had.
292
// Mainly for performance. We need to minimize to only relevant information.
293
let isDeeplyEqual = true;
294
+ let prevPropertiesChecked = 0;
295
for (const key in prev) {
296
+ if (prevPropertiesChecked > OBJECT_WIDTH_LIMIT) {
297
+ properties.push([
298
+ 'Previous object has more than ' +
299
+ OBJECT_WIDTH_LIMIT +
300
+ ' properties. React will not attempt to diff objects with too many properties.',
301
+ '',
302
+ ]);
303
+ isDeeplyEqual = false;
304
+ break;
305
+ }
306
+
307
if (!(key in next)) {
308
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
309
isDeeplyEqual = false;
310
}
311
+ prevPropertiesChecked++;
312
}
313
+
314
+ let nextPropertiesChecked = 0;
315
for (const key in next) {
316
+ if (nextPropertiesChecked > OBJECT_WIDTH_LIMIT) {
317
+ properties.push([
318
+ 'Next object has more than ' +
319
+ OBJECT_WIDTH_LIMIT +
320
+ ' properties. React will not attempt to diff objects with too many properties.',
321
+ '',
322
+ ]);
323
+ isDeeplyEqual = false;
324
+ break;
325
+ }
326
+
327
if (key in prev) {
328
const prevValue = prev[key];
329
const nextValue = next[key];
@@ -368,6 +431,8 @@ export function addObjectDiffToProperties(
431
properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
432
isDeeplyEqual = false;
433
}
434
+
435
+ nextPropertiesChecked++;
436
}
437
return isDeeplyEqual;
438
}