@samitouri / QOS-React-1 / commits / 1be3ce9996

[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 }