Create Fabric-specific version of ReactNativeAttributesPayload (#28841)
## Summary This PR introduces Fabric-only version of `ReactNativeAttributesPayload`. It is a copy-paste of `ReactNativeAttributesPayload.js`, and is called `ReactNativeAttributesPayloadFabric.js`. The idea behind this change is that certain optimizations in prop diffing may actually be a regression on the old architecture. For example, removing custom diffing may result in larger updateProps payloads. Which is, I guess, fine with JSI, but might be a problem with the bridge. ## How did you test this change? There should be no runtime effect of this change.
Dmytro Rykun committed
May 7, 2024 at 11:53 UTC
703983426243422d9726ca3a0c7eef54e173a6bb
3 files changed
+568
-69
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+1
-1
@@ -12,7 +12,7 @@ import type {
12
TouchedViewDataAtPoint,
13
ViewConfig,
14
} from './ReactNativeTypes';
15
-import {create, diff} from './ReactNativeAttributePayload';
15
+import {create, diff} from './ReactNativeAttributePayloadFabric';
16
import {dispatchEvent} from './ReactFabricEventEmitter';
17
import {
18
NoEventPriority,
packages/react-native-renderer/src/ReactNativeAttributePayload.js
+2
-68
@@ -15,7 +15,6 @@ import {
15
import isArray from 'shared/isArray';
16
17
import {enableEarlyReturnForPropDiffing} from 'shared/ReactFeatureFlags';
18
-import {enableAddPropertiesFastPath} from 'shared/ReactFeatureFlags';
18
19
import type {AttributeConfiguration} from './ReactNativeTypes';
20
@@ -445,68 +444,6 @@ function diffProperties(
444
return updatePayload;
445
}
446
448
-function fastAddProperties(
449
- updatePayload: null | Object,
450
- nextProps: Object,
451
- validAttributes: AttributeConfiguration,
452
-): null | Object {
453
- let attributeConfig;
454
- let nextProp;
455
-
456
- for (const propKey in nextProps) {
457
- nextProp = nextProps[propKey];
458
-
459
- if (nextProp === undefined) {
460
- continue;
461
- }
462
-
463
- attributeConfig = validAttributes[propKey];
464
-
465
- if (attributeConfig === undefined) {
466
- continue;
467
- }
468
-
469
- if (typeof nextProp === 'function') {
470
- nextProp = (true: any);
471
- }
472
-
473
- if (typeof attributeConfig !== 'object') {
474
- if (!updatePayload) {
475
- updatePayload = ({}: {[string]: $FlowFixMe});
476
- }
477
- updatePayload[propKey] = nextProp;
478
- continue;
479
- }
480
-
481
- if (typeof attributeConfig.process === 'function') {
482
- if (!updatePayload) {
483
- updatePayload = ({}: {[string]: $FlowFixMe});
484
- }
485
- updatePayload[propKey] = attributeConfig.process(nextProp);
486
- continue;
487
- }
488
-
489
- if (isArray(nextProp)) {
490
- for (let i = 0; i < nextProp.length; i++) {
491
- updatePayload = fastAddProperties(
492
- updatePayload,
493
- nextProp[i],
494
- ((attributeConfig: any): AttributeConfiguration),
495
- );
496
- }
497
- continue;
498
- }
499
-
500
- updatePayload = fastAddProperties(
501
- updatePayload,
502
- nextProp,
503
- ((attributeConfig: any): AttributeConfiguration),
504
- );
505
- }
506
-
507
- return updatePayload;
508
-}
509
-
447
/**
448
* addProperties adds all the valid props to the payload after being processed.
449
*/
@@ -515,11 +452,8 @@ function addProperties(
452
props: Object,
453
validAttributes: AttributeConfiguration,
454
): null | Object {
518
- if (enableAddPropertiesFastPath) {
519
- return fastAddProperties(updatePayload, props, validAttributes);
520
- } else {
521
- return diffProperties(updatePayload, emptyObject, props, validAttributes);
522
- }
455
+ // TODO: Fast path
456
+ return diffProperties(updatePayload, emptyObject, props, validAttributes);
457
}
458
459
/**
packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js
new
+565
@@ -0,0 +1,565 @@
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
+ * @flow
8
+ */
9
+
10
+// Modules provided by RN:
11
+import {
12
+ deepDiffer,
13
+ flattenStyle,
14
+} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
15
+import isArray from 'shared/isArray';
16
+
17
+import {enableEarlyReturnForPropDiffing} from 'shared/ReactFeatureFlags';
18
+import {enableAddPropertiesFastPath} from 'shared/ReactFeatureFlags';
19
+
20
+import type {AttributeConfiguration} from './ReactNativeTypes';
21
+
22
+const emptyObject = {};
23
+
24
+/**
25
+ * Create a payload that contains all the updates between two sets of props.
26
+ *
27
+ * These helpers are all encapsulated into a single module, because they use
28
+ * mutation as a performance optimization which leads to subtle shared
29
+ * dependencies between the code paths. To avoid this mutable state leaking
30
+ * across modules, I've kept them isolated to this module.
31
+ */
32
+
33
+type NestedNode = Array<NestedNode> | Object;
34
+
35
+// Tracks removed keys
36
+let removedKeys: {[string]: boolean} | null = null;
37
+let removedKeyCount = 0;
38
+
39
+const deepDifferOptions = {
40
+ unsafelyIgnoreFunctions: true,
41
+};
42
+
43
+function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
44
+ if (typeof nextProp !== 'object' || nextProp === null) {
45
+ // Scalars have already been checked for equality
46
+ return true;
47
+ } else {
48
+ // For objects and arrays, the default diffing algorithm is a deep compare
49
+ return deepDiffer(prevProp, nextProp, deepDifferOptions);
50
+ }
51
+}
52
+
53
+function restoreDeletedValuesInNestedArray(
54
+ updatePayload: Object,
55
+ node: NestedNode,
56
+ validAttributes: AttributeConfiguration,
57
+) {
58
+ if (isArray(node)) {
59
+ let i = node.length;
60
+ while (i-- && removedKeyCount > 0) {
61
+ restoreDeletedValuesInNestedArray(
62
+ updatePayload,
63
+ node[i],
64
+ validAttributes,
65
+ );
66
+ }
67
+ } else if (node && removedKeyCount > 0) {
68
+ const obj = node;
69
+ for (const propKey in removedKeys) {
70
+ // $FlowFixMe[incompatible-use] found when upgrading Flow
71
+ if (!removedKeys[propKey]) {
72
+ continue;
73
+ }
74
+ let nextProp = obj[propKey];
75
+ if (nextProp === undefined) {
76
+ continue;
77
+ }
78
+
79
+ const attributeConfig = validAttributes[propKey];
80
+ if (!attributeConfig) {
81
+ continue; // not a valid native prop
82
+ }
83
+
84
+ if (typeof nextProp === 'function') {
85
+ // $FlowFixMe[incompatible-type] found when upgrading Flow
86
+ nextProp = true;
87
+ }
88
+ if (typeof nextProp === 'undefined') {
89
+ // $FlowFixMe[incompatible-type] found when upgrading Flow
90
+ nextProp = null;
91
+ }
92
+
93
+ if (typeof attributeConfig !== 'object') {
94
+ // case: !Object is the default case
95
+ updatePayload[propKey] = nextProp;
96
+ } else if (
97
+ typeof attributeConfig.diff === 'function' ||
98
+ typeof attributeConfig.process === 'function'
99
+ ) {
100
+ // case: CustomAttributeConfiguration
101
+ const nextValue =
102
+ typeof attributeConfig.process === 'function'
103
+ ? attributeConfig.process(nextProp)
104
+ : nextProp;
105
+ updatePayload[propKey] = nextValue;
106
+ }
107
+ // $FlowFixMe[incompatible-use] found when upgrading Flow
108
+ removedKeys[propKey] = false;
109
+ removedKeyCount--;
110
+ }
111
+ }
112
+}
113
+
114
+function diffNestedArrayProperty(
115
+ updatePayload: null | Object,
116
+ prevArray: Array<NestedNode>,
117
+ nextArray: Array<NestedNode>,
118
+ validAttributes: AttributeConfiguration,
119
+): null | Object {
120
+ const minLength =
121
+ prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
122
+ let i;
123
+ for (i = 0; i < minLength; i++) {
124
+ // Diff any items in the array in the forward direction. Repeated keys
125
+ // will be overwritten by later values.
126
+ updatePayload = diffNestedProperty(
127
+ updatePayload,
128
+ prevArray[i],
129
+ nextArray[i],
130
+ validAttributes,
131
+ );
132
+ }
133
+ for (; i < prevArray.length; i++) {
134
+ // Clear out all remaining properties.
135
+ updatePayload = clearNestedProperty(
136
+ updatePayload,
137
+ prevArray[i],
138
+ validAttributes,
139
+ );
140
+ }
141
+ for (; i < nextArray.length; i++) {
142
+ // Add all remaining properties.
143
+ updatePayload = addNestedProperty(
144
+ updatePayload,
145
+ nextArray[i],
146
+ validAttributes,
147
+ );
148
+ }
149
+ return updatePayload;
150
+}
151
+
152
+function diffNestedProperty(
153
+ updatePayload: null | Object,
154
+ prevProp: NestedNode,
155
+ nextProp: NestedNode,
156
+ validAttributes: AttributeConfiguration,
157
+): null | Object {
158
+ if (!updatePayload && prevProp === nextProp) {
159
+ // If no properties have been added, then we can bail out quickly on object
160
+ // equality.
161
+ return updatePayload;
162
+ }
163
+
164
+ if (!prevProp || !nextProp) {
165
+ if (nextProp) {
166
+ return addNestedProperty(updatePayload, nextProp, validAttributes);
167
+ }
168
+ if (prevProp) {
169
+ return clearNestedProperty(updatePayload, prevProp, validAttributes);
170
+ }
171
+ return updatePayload;
172
+ }
173
+
174
+ if (!isArray(prevProp) && !isArray(nextProp)) {
175
+ // Both are leaves, we can diff the leaves.
176
+ return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
177
+ }
178
+
179
+ if (isArray(prevProp) && isArray(nextProp)) {
180
+ // Both are arrays, we can diff the arrays.
181
+ return diffNestedArrayProperty(
182
+ updatePayload,
183
+ prevProp,
184
+ nextProp,
185
+ validAttributes,
186
+ );
187
+ }
188
+
189
+ if (isArray(prevProp)) {
190
+ return diffProperties(
191
+ updatePayload,
192
+ flattenStyle(prevProp),
193
+ nextProp,
194
+ validAttributes,
195
+ );
196
+ }
197
+
198
+ return diffProperties(
199
+ updatePayload,
200
+ prevProp,
201
+ flattenStyle(nextProp),
202
+ validAttributes,
203
+ );
204
+}
205
+
206
+/**
207
+ * addNestedProperty takes a single set of props and valid attribute
208
+ * attribute configurations. It processes each prop and adds it to the
209
+ * updatePayload.
210
+ */
211
+function addNestedProperty(
212
+ updatePayload: null | Object,
213
+ nextProp: NestedNode,
214
+ validAttributes: AttributeConfiguration,
215
+): $FlowFixMe {
216
+ if (!nextProp) {
217
+ return updatePayload;
218
+ }
219
+
220
+ if (!isArray(nextProp)) {
221
+ // Add each property of the leaf.
222
+ return addProperties(updatePayload, nextProp, validAttributes);
223
+ }
224
+
225
+ for (let i = 0; i < nextProp.length; i++) {
226
+ // Add all the properties of the array.
227
+ updatePayload = addNestedProperty(
228
+ updatePayload,
229
+ nextProp[i],
230
+ validAttributes,
231
+ );
232
+ }
233
+
234
+ return updatePayload;
235
+}
236
+
237
+/**
238
+ * clearNestedProperty takes a single set of props and valid attributes. It
239
+ * adds a null sentinel to the updatePayload, for each prop key.
240
+ */
241
+function clearNestedProperty(
242
+ updatePayload: null | Object,
243
+ prevProp: NestedNode,
244
+ validAttributes: AttributeConfiguration,
245
+): null | Object {
246
+ if (!prevProp) {
247
+ return updatePayload;
248
+ }
249
+
250
+ if (!isArray(prevProp)) {
251
+ // Add each property of the leaf.
252
+ return clearProperties(updatePayload, prevProp, validAttributes);
253
+ }
254
+
255
+ for (let i = 0; i < prevProp.length; i++) {
256
+ // Add all the properties of the array.
257
+ updatePayload = clearNestedProperty(
258
+ updatePayload,
259
+ prevProp[i],
260
+ validAttributes,
261
+ );
262
+ }
263
+ return updatePayload;
264
+}
265
+
266
+/**
267
+ * diffProperties takes two sets of props and a set of valid attributes
268
+ * and write to updatePayload the values that changed or were deleted.
269
+ * If no updatePayload is provided, a new one is created and returned if
270
+ * anything changed.
271
+ */
272
+function diffProperties(
273
+ updatePayload: null | Object,
274
+ prevProps: Object,
275
+ nextProps: Object,
276
+ validAttributes: AttributeConfiguration,
277
+): null | Object {
278
+ let attributeConfig;
279
+ let nextProp;
280
+ let prevProp;
281
+
282
+ for (const propKey in nextProps) {
283
+ attributeConfig = validAttributes[propKey];
284
+ if (!attributeConfig) {
285
+ continue; // not a valid native prop
286
+ }
287
+
288
+ prevProp = prevProps[propKey];
289
+ nextProp = nextProps[propKey];
290
+
291
+ // functions are converted to booleans as markers that the associated
292
+ // events should be sent from native.
293
+ if (typeof nextProp === 'function') {
294
+ nextProp = (true: any);
295
+ // If nextProp is not a function, then don't bother changing prevProp
296
+ // since nextProp will win and go into the updatePayload regardless.
297
+ if (typeof prevProp === 'function') {
298
+ prevProp = (true: any);
299
+ }
300
+ }
301
+
302
+ // An explicit value of undefined is treated as a null because it overrides
303
+ // any other preceding value.
304
+ if (typeof nextProp === 'undefined') {
305
+ nextProp = (null: any);
306
+ if (typeof prevProp === 'undefined') {
307
+ prevProp = (null: any);
308
+ }
309
+ }
310
+
311
+ if (removedKeys) {
312
+ removedKeys[propKey] = false;
313
+ }
314
+
315
+ if (updatePayload && updatePayload[propKey] !== undefined) {
316
+ // Something else already triggered an update to this key because another
317
+ // value diffed. Since we're now later in the nested arrays our value is
318
+ // more important so we need to calculate it and override the existing
319
+ // value. It doesn't matter if nothing changed, we'll set it anyway.
320
+
321
+ // Pattern match on: attributeConfig
322
+ if (typeof attributeConfig !== 'object') {
323
+ // case: !Object is the default case
324
+ updatePayload[propKey] = nextProp;
325
+ } else if (
326
+ typeof attributeConfig.diff === 'function' ||
327
+ typeof attributeConfig.process === 'function'
328
+ ) {
329
+ // case: CustomAttributeConfiguration
330
+ const nextValue =
331
+ typeof attributeConfig.process === 'function'
332
+ ? attributeConfig.process(nextProp)
333
+ : nextProp;
334
+ updatePayload[propKey] = nextValue;
335
+ }
336
+ continue;
337
+ }
338
+
339
+ if (prevProp === nextProp) {
340
+ continue; // nothing changed
341
+ }
342
+
343
+ // Pattern match on: attributeConfig
344
+ if (typeof attributeConfig !== 'object') {
345
+ // case: !Object is the default case
346
+ if (defaultDiffer(prevProp, nextProp)) {
347
+ // a normal leaf has changed
348
+ (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
349
+ propKey
350
+ ] = nextProp;
351
+ }
352
+ } else if (
353
+ typeof attributeConfig.diff === 'function' ||
354
+ typeof attributeConfig.process === 'function'
355
+ ) {
356
+ // case: CustomAttributeConfiguration
357
+ const shouldUpdate =
358
+ prevProp === undefined ||
359
+ (typeof attributeConfig.diff === 'function'
360
+ ? attributeConfig.diff(prevProp, nextProp)
361
+ : defaultDiffer(prevProp, nextProp));
362
+ if (shouldUpdate) {
363
+ const nextValue =
364
+ typeof attributeConfig.process === 'function'
365
+ ? // $FlowFixMe[incompatible-use] found when upgrading Flow
366
+ attributeConfig.process(nextProp)
367
+ : nextProp;
368
+ (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
369
+ propKey
370
+ ] = nextValue;
371
+ }
372
+ } else {
373
+ // default: fallthrough case when nested properties are defined
374
+ removedKeys = null;
375
+ removedKeyCount = 0;
376
+ // We think that attributeConfig is not CustomAttributeConfiguration at
377
+ // this point so we assume it must be AttributeConfiguration.
378
+ updatePayload = diffNestedProperty(
379
+ updatePayload,
380
+ prevProp,
381
+ nextProp,
382
+ ((attributeConfig: any): AttributeConfiguration),
383
+ );
384
+ if (removedKeyCount > 0 && updatePayload) {
385
+ restoreDeletedValuesInNestedArray(
386
+ updatePayload,
387
+ nextProp,
388
+ ((attributeConfig: any): AttributeConfiguration),
389
+ );
390
+ removedKeys = null;
391
+ }
392
+ }
393
+ }
394
+
395
+ // Also iterate through all the previous props to catch any that have been
396
+ // removed and make sure native gets the signal so it can reset them to the
397
+ // default.
398
+ for (const propKey in prevProps) {
399
+ if (nextProps[propKey] !== undefined) {
400
+ continue; // we've already covered this key in the previous pass
401
+ }
402
+ attributeConfig = validAttributes[propKey];
403
+ if (!attributeConfig) {
404
+ continue; // not a valid native prop
405
+ }
406
+
407
+ if (updatePayload && updatePayload[propKey] !== undefined) {
408
+ // This was already updated to a diff result earlier.
409
+ continue;
410
+ }
411
+
412
+ prevProp = prevProps[propKey];
413
+ if (prevProp === undefined) {
414
+ continue; // was already empty anyway
415
+ }
416
+ // Pattern match on: attributeConfig
417
+ if (
418
+ typeof attributeConfig !== 'object' ||
419
+ typeof attributeConfig.diff === 'function' ||
420
+ typeof attributeConfig.process === 'function'
421
+ ) {
422
+ // case: CustomAttributeConfiguration | !Object
423
+ // Flag the leaf property for removal by sending a sentinel.
424
+ (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
425
+ propKey
426
+ ] = null;
427
+ if (!removedKeys) {
428
+ removedKeys = ({}: {[string]: boolean});
429
+ }
430
+ if (!removedKeys[propKey]) {
431
+ removedKeys[propKey] = true;
432
+ removedKeyCount++;
433
+ }
434
+ } else {
435
+ // default:
436
+ // This is a nested attribute configuration where all the properties
437
+ // were removed so we need to go through and clear out all of them.
438
+ updatePayload = clearNestedProperty(
439
+ updatePayload,
440
+ prevProp,
441
+ ((attributeConfig: any): AttributeConfiguration),
442
+ );
443
+ }
444
+ }
445
+ return updatePayload;
446
+}
447
+
448
+function fastAddProperties(
449
+ updatePayload: null | Object,
450
+ nextProps: Object,
451
+ validAttributes: AttributeConfiguration,
452
+): null | Object {
453
+ let attributeConfig;
454
+ let nextProp;
455
+
456
+ for (const propKey in nextProps) {
457
+ nextProp = nextProps[propKey];
458
+
459
+ if (nextProp === undefined) {
460
+ continue;
461
+ }
462
+
463
+ attributeConfig = validAttributes[propKey];
464
+
465
+ if (attributeConfig === undefined) {
466
+ continue;
467
+ }
468
+
469
+ if (typeof nextProp === 'function') {
470
+ nextProp = (true: any);
471
+ }
472
+
473
+ if (typeof attributeConfig !== 'object') {
474
+ if (!updatePayload) {
475
+ updatePayload = ({}: {[string]: $FlowFixMe});
476
+ }
477
+ updatePayload[propKey] = nextProp;
478
+ continue;
479
+ }
480
+
481
+ if (typeof attributeConfig.process === 'function') {
482
+ if (!updatePayload) {
483
+ updatePayload = ({}: {[string]: $FlowFixMe});
484
+ }
485
+ updatePayload[propKey] = attributeConfig.process(nextProp);
486
+ continue;
487
+ }
488
+
489
+ if (isArray(nextProp)) {
490
+ for (let i = 0; i < nextProp.length; i++) {
491
+ updatePayload = fastAddProperties(
492
+ updatePayload,
493
+ nextProp[i],
494
+ ((attributeConfig: any): AttributeConfiguration),
495
+ );
496
+ }
497
+ continue;
498
+ }
499
+
500
+ updatePayload = fastAddProperties(
501
+ updatePayload,
502
+ nextProp,
503
+ ((attributeConfig: any): AttributeConfiguration),
504
+ );
505
+ }
506
+
507
+ return updatePayload;
508
+}
509
+
510
+/**
511
+ * addProperties adds all the valid props to the payload after being processed.
512
+ */
513
+function addProperties(
514
+ updatePayload: null | Object,
515
+ props: Object,
516
+ validAttributes: AttributeConfiguration,
517
+): null | Object {
518
+ if (enableAddPropertiesFastPath) {
519
+ return fastAddProperties(updatePayload, props, validAttributes);
520
+ } else {
521
+ return diffProperties(updatePayload, emptyObject, props, validAttributes);
522
+ }
523
+}
524
+
525
+/**
526
+ * clearProperties clears all the previous props by adding a null sentinel
527
+ * to the payload for each valid key.
528
+ */
529
+function clearProperties(
530
+ updatePayload: null | Object,
531
+ prevProps: Object,
532
+ validAttributes: AttributeConfiguration,
533
+): null | Object {
534
+ // TODO: Fast path
535
+ return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
536
+}
537
+
538
+export function create(
539
+ props: Object,
540
+ validAttributes: AttributeConfiguration,
541
+): null | Object {
542
+ return addProperties(
543
+ null, // updatePayload
544
+ props,
545
+ validAttributes,
546
+ );
547
+}
548
+
549
+export function diff(
550
+ prevProps: Object,
551
+ nextProps: Object,
552
+ validAttributes: AttributeConfiguration,
553
+): null | Object {
554
+ if (enableEarlyReturnForPropDiffing) {
555
+ if (prevProps === nextProps) {
556
+ return null; // no change
557
+ }
558
+ }
559
+ return diffProperties(
560
+ null, // updatePayload
561
+ prevProps,
562
+ nextProps,
563
+ validAttributes,
564
+ );
565
+}