@samitouri / QOS-React-2 / commits / ca41adb8c1

Diff properties in the commit phase instead of generating an update payload (#26583)

This removes the concept of `prepareUpdate()`, behind a flag. React Native already does everything in the commit phase, but generates a temporary update payload before applying it. React Fabric does it both in the render phase. Now it just moves it to a single host config. For DOM I forked updateProperties into one that does diffing and updating in one pass vs just applying a pre-diffed updatePayload. There are a few downsides of this approach: - If only "children" has changed, we end up scheduling an update to be done in the commit phase. Since we traverse through it anyway, it's probably not much extra. - It does more work in the commit phase so for a large tree that is mostly unchanged, it'll stall longer. - It does some extra work for special cases since that work happens if anything has changed. We no longer have a deep bailout. - The special cases now have to each replicate the "clean up old props" loop, leading to extra code. The benefit is that this doesn't allocate temporary extra objects (possibly multiple per element if the array has to resize). It's less work overall. It also gives us an option to reuse this function for a sync render optimization. Another benefit is that if we do the loop in the commit phase I can do further optimizations by reading all props that I need for special cases in that loop instead of polymorphic reads from props. This is what I'd like to do in future refactors that would be stacked on top of this change.

Sebastian Markbåge committed Apr 10, 2023 at 19:09 UTC ca41adb8c1b256705f73d1fb657421a03dfad82c
19 files changed +675 -130
packages/react-dom-bindings/src/client/CSSPropertyOperations.js
+93 -36
@@ -11,6 +11,7 @@ import hyphenateStyleName from '../shared/hyphenateStyleName';
11 import warnValidStyle from '../shared/warnValidStyle';
12 import isUnitlessNumber from '../shared/isUnitlessNumber';
13 import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
14 +import {diffInCommitPhase} from 'shared/ReactFeatureFlags';
15
16 /**
17 * Operations for dealing with CSS properties.
@@ -64,6 +65,42 @@ export function createDangerousStringForStyles(styles) {
65 }
66 }
67
68 +function setValueForStyle(style, styleName, value) {
69 + const isCustomProperty = styleName.indexOf('--') === 0;
70 + if (__DEV__) {
71 + if (!isCustomProperty) {
72 + warnValidStyle(styleName, value);
73 + }
74 + }
75 +
76 + if (value == null || typeof value === 'boolean' || value === '') {
77 + if (isCustomProperty) {
78 + style.setProperty(styleName, '');
79 + } else if (styleName === 'float') {
80 + style.cssFloat = '';
81 + } else {
82 + style[styleName] = '';
83 + }
84 + } else if (isCustomProperty) {
85 + style.setProperty(styleName, value);
86 + } else if (
87 + typeof value === 'number' &&
88 + value !== 0 &&
89 + !isUnitlessNumber(styleName)
90 + ) {
91 + style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
92 + } else {
93 + if (styleName === 'float') {
94 + style.cssFloat = value;
95 + } else {
96 + if (__DEV__) {
97 + checkCSSPropertyStringCoercion(value, styleName);
98 + }
99 + style[styleName] = ('' + value).trim();
100 + }
101 + }
102 +}
103 +
104 /**
105 * Sets the value for multiple styles on a node. If a value is specified as
106 * '' (empty string), the corresponding style property will be unset.
@@ -71,7 +108,7 @@ export function createDangerousStringForStyles(styles) {
108 * @param {DOMElement} node
109 * @param {object} styles
110 */
74 -export function setValueForStyles(node, styles) {
111 +export function setValueForStyles(node, styles, prevStyles) {
112 if (styles != null && typeof styles !== 'object') {
113 throw new Error(
114 'The `style` prop expects a mapping from style properties to values, ' +
@@ -88,42 +125,39 @@ export function setValueForStyles(node, styles) {
125 }
126
127 const style = node.style;
91 - for (const styleName in styles) {
92 - if (!styles.hasOwnProperty(styleName)) {
93 - continue;
94 - }
95 - const value = styles[styleName];
96 - const isCustomProperty = styleName.indexOf('--') === 0;
128 +
129 + if (diffInCommitPhase && prevStyles != null) {
130 if (__DEV__) {
98 - if (!isCustomProperty) {
99 - warnValidStyle(styleName, value);
100 - }
131 + validateShorthandPropertyCollisionInDev(prevStyles, styles);
132 }
133
103 - if (value == null || typeof value === 'boolean' || value === '') {
104 - if (isCustomProperty) {
105 - style.setProperty(styleName, '');
106 - } else if (styleName === 'float') {
107 - style.cssFloat = '';
108 - } else {
109 - style[styleName] = '';
110 - }
111 - } else if (isCustomProperty) {
112 - style.setProperty(styleName, value);
113 - } else if (
114 - typeof value === 'number' &&
115 - value !== 0 &&
116 - !isUnitlessNumber(styleName)
117 - ) {
118 - style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
119 - } else {
120 - if (styleName === 'float') {
121 - style.cssFloat = value;
122 - } else {
123 - if (__DEV__) {
124 - checkCSSPropertyStringCoercion(value, styleName);
134 + for (const styleName in prevStyles) {
135 + if (
136 + prevStyles.hasOwnProperty(styleName) &&
137 + (styles == null || !styles.hasOwnProperty(styleName))
138 + ) {
139 + // Clear style
140 + const isCustomProperty = styleName.indexOf('--') === 0;
141 + if (isCustomProperty) {
142 + style.setProperty(styleName, '');
143 + } else if (styleName === 'float') {
144 + style.cssFloat = '';
145 + } else {
146 + style[styleName] = '';
147 }
126 - style[styleName] = ('' + value).trim();
148 + }
149 + }
150 + for (const styleName in styles) {
151 + const value = styles[styleName];
152 + if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
153 + setValueForStyle(style, styleName, value);
154 + }
155 + }
156 + } else {
157 + for (const styleName in styles) {
158 + if (styles.hasOwnProperty(styleName)) {
159 + const value = styles[styleName];
160 + setValueForStyle(style, styleName, value);
161 }
162 }
163 }
@@ -167,7 +201,7 @@ function expandShorthandMap(styles) {
201 * becomes .style.fontVariant = ''
202 */
203 export function validateShorthandPropertyCollisionInDev(
170 - styleUpdates,
204 + prevStyles,
205 nextStyles,
206 ) {
207 if (__DEV__) {
@@ -175,7 +209,30 @@ export function validateShorthandPropertyCollisionInDev(
209 return;
210 }
211
178 - const expandedUpdates = expandShorthandMap(styleUpdates);
212 + // Compute the diff as it would happen elsewhere.
213 + const expandedUpdates = {};
214 + if (prevStyles) {
215 + for (const key in prevStyles) {
216 + if (prevStyles.hasOwnProperty(key) && !nextStyles.hasOwnProperty(key)) {
217 + const longhands = shorthandToLonghand[key] || [key];
218 + for (let i = 0; i < longhands.length; i++) {
219 + expandedUpdates[longhands[i]] = key;
220 + }
221 + }
222 + }
223 + }
224 + for (const key in nextStyles) {
225 + if (
226 + nextStyles.hasOwnProperty(key) &&
227 + (!prevStyles || prevStyles[key] !== nextStyles[key])
228 + ) {
229 + const longhands = shorthandToLonghand[key] || [key];
230 + for (let i = 0; i < longhands.length; i++) {
231 + expandedUpdates[longhands[i]] = key;
232 + }
233 + }
234 + }
235 +
236 const expandedStyles = expandShorthandMap(nextStyles);
237 const warnedAbout = {};
238 for (const key in expandedUpdates) {
@@ -193,7 +250,7 @@ export function validateShorthandPropertyCollisionInDev(
250 "avoid this, don't mix shorthand and non-shorthand properties " +
251 'for the same value; instead, replace the shorthand with ' +
252 'separate values.',
196 - isValueEmpty(styleUpdates[originalKey]) ? 'Removing' : 'Updating',
253 + isValueEmpty(nextStyles[originalKey]) ? 'Removing' : 'Updating',
254 originalKey,
255 correctOriginalKey,
256 );
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+466 -16
@@ -69,6 +69,7 @@ import {
69 disableIEWorkarounds,
70 enableTrustedTypesIntegration,
71 enableFilterEmptyStringAttributesDOM,
72 + diffInCommitPhase,
73 } from 'shared/ReactFeatureFlags';
74 import {
75 mediaEventTypes,
@@ -273,6 +274,7 @@ function setProp(
274 key: string,
275 value: mixed,
276 props: any,
277 + prevValue: mixed,
278 ): void {
279 switch (key) {
280 case 'children': {
@@ -314,7 +316,7 @@ function setProp(
316 break;
317 }
318 case 'style': {
317 - setValueForStyles(domElement, value);
319 + setValueForStyles(domElement, value, prevValue);
320 break;
321 }
322 // These attributes accept URLs. These must not allow javascript: URLS.
@@ -655,6 +657,21 @@ function setProp(
657 );
658 break;
659 // Properties that should not be allowed on custom elements.
660 + case 'is': {
661 + if (__DEV__) {
662 + if (prevValue != null) {
663 + console.error(
664 + 'Cannot update the "is" prop after it has been initialized.',
665 + );
666 + }
667 + }
668 + // TODO: We shouldn't actually set this attribute, because we've already
669 + // passed it to createElement. We don't also need the attribute.
670 + // However, our tests currently query for it so it's plausible someone
671 + // else does too so it's break.
672 + setValueForAttribute(domElement, 'is', value);
673 + break;
674 + }
675 case 'innerText':
676 case 'textContent':
677 if (enableCustomElementPropertySupport) {
@@ -689,10 +706,11 @@ function setPropOnCustomElement(
706 key: string,
707 value: mixed,
708 props: any,
709 + prevValue: mixed,
710 ): void {
711 switch (key) {
712 case 'style': {
695 - setValueForStyles(domElement, value);
713 + setValueForStyles(domElement, value, prevValue);
714 break;
715 }
716 case 'dangerouslySetInnerHTML': {
@@ -859,7 +877,7 @@ export function setInitialProperties(
877 }
878 // defaultChecked and defaultValue are ignored by setProp
879 default: {
862 - setProp(domElement, tag, propKey, propValue, props);
880 + setProp(domElement, tag, propKey, propValue, props, null);
881 }
882 }
883 }
@@ -892,7 +910,7 @@ export function setInitialProperties(
910 }
911 // defaultValue are ignored by setProp
912 default: {
895 - setProp(domElement, tag, propKey, propValue, props);
913 + setProp(domElement, tag, propKey, propValue, props, null);
914 }
915 }
916 }
@@ -1047,7 +1065,7 @@ export function setInitialProperties(
1065 }
1066 // defaultChecked and defaultValue are ignored by setProp
1067 default: {
1050 - setProp(domElement, tag, propKey, propValue, props);
1068 + setProp(domElement, tag, propKey, propValue, props, null);
1069 }
1070 }
1071 }
@@ -1063,7 +1081,14 @@ export function setInitialProperties(
1081 if (propValue == null) {
1082 continue;
1083 }
1066 - setPropOnCustomElement(domElement, tag, propKey, propValue, props);
1084 + setPropOnCustomElement(
1085 + domElement,
1086 + tag,
1087 + propKey,
1088 + propValue,
1089 + props,
1090 + null,
1091 + );
1092 }
1093 return;
1094 }
@@ -1078,7 +1103,7 @@ export function setInitialProperties(
1103 if (propValue == null) {
1104 continue;
1105 }
1081 - setProp(domElement, tag, propKey, propValue, props);
1106 + setProp(domElement, tag, propKey, propValue, props, null);
1107 }
1108 }
1109
@@ -1188,15 +1213,428 @@ export function diffProperties(
1213 }
1214 if (styleUpdates) {
1215 if (__DEV__) {
1191 - validateShorthandPropertyCollisionInDev(styleUpdates, nextProps.style);
1216 + validateShorthandPropertyCollisionInDev(lastProps.style, nextProps.style);
1217 }
1218 (updatePayload = updatePayload || []).push('style', styleUpdates);
1219 }
1220 return updatePayload;
1221 }
1222
1198 -// Apply the diff.
1223 export function updateProperties(
1224 + domElement: Element,
1225 + tag: string,
1226 + lastProps: Object,
1227 + nextProps: Object,
1228 +): void {
1229 + if (__DEV__) {
1230 + validatePropertiesInDevelopment(tag, nextProps);
1231 + }
1232 +
1233 + switch (tag) {
1234 + case 'div':
1235 + case 'span':
1236 + case 'svg':
1237 + case 'path':
1238 + case 'a':
1239 + case 'g':
1240 + case 'p':
1241 + case 'li': {
1242 + // Fast track the most common tag types
1243 + break;
1244 + }
1245 + case 'input': {
1246 + // Update checked *before* name.
1247 + // In the middle of an update, it is possible to have multiple checked.
1248 + // When a checked radio tries to change name, browser makes another radio's checked false.
1249 + if (nextProps.type === 'radio' && nextProps.name != null) {
1250 + updateInputChecked(domElement, nextProps);
1251 + }
1252 + for (const propKey in lastProps) {
1253 + const lastProp = lastProps[propKey];
1254 + if (
1255 + lastProps.hasOwnProperty(propKey) &&
1256 + lastProp != null &&
1257 + !nextProps.hasOwnProperty(propKey)
1258 + ) {
1259 + switch (propKey) {
1260 + case 'checked': {
1261 + const checked = nextProps.defaultChecked;
1262 + const inputElement: HTMLInputElement = (domElement: any);
1263 + inputElement.checked =
1264 + !!checked &&
1265 + typeof checked !== 'function' &&
1266 + checked !== 'symbol';
1267 + break;
1268 + }
1269 + case 'value': {
1270 + // This is handled by updateWrapper below.
1271 + break;
1272 + }
1273 + // defaultChecked and defaultValue are ignored by setProp
1274 + default: {
1275 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1276 + }
1277 + }
1278 + }
1279 + }
1280 + for (const propKey in nextProps) {
1281 + const nextProp = nextProps[propKey];
1282 + const lastProp = lastProps[propKey];
1283 + if (
1284 + nextProps.hasOwnProperty(propKey) &&
1285 + nextProp !== lastProp &&
1286 + (nextProp != null || lastProp != null)
1287 + ) {
1288 + switch (propKey) {
1289 + case 'checked': {
1290 + const checked =
1291 + nextProp != null ? nextProp : nextProps.defaultChecked;
1292 + const inputElement: HTMLInputElement = (domElement: any);
1293 + inputElement.checked =
1294 + !!checked &&
1295 + typeof checked !== 'function' &&
1296 + checked !== 'symbol';
1297 + break;
1298 + }
1299 + case 'value': {
1300 + // This is handled by updateWrapper below.
1301 + break;
1302 + }
1303 + case 'children':
1304 + case 'dangerouslySetInnerHTML': {
1305 + if (nextProp != null) {
1306 + throw new Error(
1307 + `${tag} is a void element tag and must neither have \`children\` nor ` +
1308 + 'use `dangerouslySetInnerHTML`.',
1309 + );
1310 + }
1311 + break;
1312 + }
1313 + // defaultChecked and defaultValue are ignored by setProp
1314 + default: {
1315 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1316 + }
1317 + }
1318 + }
1319 + }
1320 +
1321 + if (__DEV__) {
1322 + const wasControlled =
1323 + lastProps.type === 'checkbox' || lastProps.type === 'radio'
1324 + ? lastProps.checked != null
1325 + : lastProps.value != null;
1326 + const isControlled =
1327 + nextProps.type === 'checkbox' || nextProps.type === 'radio'
1328 + ? nextProps.checked != null
1329 + : nextProps.value != null;
1330 +
1331 + if (
1332 + !wasControlled &&
1333 + isControlled &&
1334 + !didWarnUncontrolledToControlled
1335 + ) {
1336 + console.error(
1337 + 'A component is changing an uncontrolled input to be controlled. ' +
1338 + 'This is likely caused by the value changing from undefined to ' +
1339 + 'a defined value, which should not happen. ' +
1340 + 'Decide between using a controlled or uncontrolled input ' +
1341 + 'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
1342 + );
1343 + didWarnUncontrolledToControlled = true;
1344 + }
1345 + if (
1346 + wasControlled &&
1347 + !isControlled &&
1348 + !didWarnControlledToUncontrolled
1349 + ) {
1350 + console.error(
1351 + 'A component is changing a controlled input to be uncontrolled. ' +
1352 + 'This is likely caused by the value changing from a defined to ' +
1353 + 'undefined, which should not happen. ' +
1354 + 'Decide between using a controlled or uncontrolled input ' +
1355 + 'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
1356 + );
1357 + didWarnControlledToUncontrolled = true;
1358 + }
1359 + }
1360 + // Update the wrapper around inputs *after* updating props. This has to
1361 + // happen after updating the rest of props. Otherwise HTML5 input validations
1362 + // raise warnings and prevent the new value from being assigned.
1363 + updateInput(domElement, nextProps);
1364 + return;
1365 + }
1366 + case 'select': {
1367 + for (const propKey in lastProps) {
1368 + const lastProp = lastProps[propKey];
1369 + if (
1370 + lastProps.hasOwnProperty(propKey) &&
1371 + lastProp != null &&
1372 + !nextProps.hasOwnProperty(propKey)
1373 + ) {
1374 + switch (propKey) {
1375 + case 'value': {
1376 + // This is handled by updateWrapper below.
1377 + break;
1378 + }
1379 + // defaultValue are ignored by setProp
1380 + default: {
1381 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1382 + }
1383 + }
1384 + }
1385 + }
1386 + for (const propKey in nextProps) {
1387 + const nextProp = nextProps[propKey];
1388 + const lastProp = lastProps[propKey];
1389 + if (
1390 + nextProps.hasOwnProperty(propKey) &&
1391 + nextProp !== lastProp &&
1392 + (nextProp != null || lastProp != null)
1393 + ) {
1394 + switch (propKey) {
1395 + case 'value': {
1396 + // This is handled by updateWrapper below.
1397 + break;
1398 + }
1399 + // defaultValue are ignored by setProp
1400 + default: {
1401 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1402 + }
1403 + }
1404 + }
1405 + }
1406 + // <select> value update needs to occur after <option> children
1407 + // reconciliation
1408 + updateSelect(domElement, lastProps, nextProps);
1409 + return;
1410 + }
1411 + case 'textarea': {
1412 + for (const propKey in lastProps) {
1413 + const lastProp = lastProps[propKey];
1414 + if (
1415 + lastProps.hasOwnProperty(propKey) &&
1416 + lastProp != null &&
1417 + !nextProps.hasOwnProperty(propKey)
1418 + ) {
1419 + switch (propKey) {
1420 + case 'value': {
1421 + // This is handled by updateWrapper below.
1422 + break;
1423 + }
1424 + case 'children': {
1425 + // TODO: This doesn't actually do anything if it updates.
1426 + break;
1427 + }
1428 + // defaultValue is ignored by setProp
1429 + default: {
1430 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1431 + }
1432 + }
1433 + }
1434 + }
1435 + for (const propKey in nextProps) {
1436 + const nextProp = nextProps[propKey];
1437 + const lastProp = lastProps[propKey];
1438 + if (
1439 + nextProps.hasOwnProperty(propKey) &&
1440 + nextProp !== lastProp &&
1441 + (nextProp != null || lastProp != null)
1442 + ) {
1443 + switch (propKey) {
1444 + case 'value': {
1445 + // This is handled by updateWrapper below.
1446 + break;
1447 + }
1448 + case 'children': {
1449 + // TODO: This doesn't actually do anything if it updates.
1450 + break;
1451 + }
1452 + case 'dangerouslySetInnerHTML': {
1453 + if (nextProp != null) {
1454 + // TODO: Do we really need a special error message for this. It's also pretty blunt.
1455 + throw new Error(
1456 + '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
1457 + );
1458 + }
1459 + break;
1460 + }
1461 + // defaultValue is ignored by setProp
1462 + default: {
1463 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1464 + }
1465 + }
1466 + }
1467 + }
1468 + updateTextarea(domElement, nextProps);
1469 + return;
1470 + }
1471 + case 'option': {
1472 + for (const propKey in lastProps) {
1473 + const lastProp = lastProps[propKey];
1474 + if (
1475 + lastProps.hasOwnProperty(propKey) &&
1476 + lastProp != null &&
1477 + !nextProps.hasOwnProperty(propKey)
1478 + ) {
1479 + switch (propKey) {
1480 + case 'selected': {
1481 + // TODO: Remove support for selected on option.
1482 + (domElement: any).selected = false;
1483 + break;
1484 + }
1485 + default: {
1486 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1487 + }
1488 + }
1489 + }
1490 + }
1491 + for (const propKey in nextProps) {
1492 + const nextProp = nextProps[propKey];
1493 + const lastProp = lastProps[propKey];
1494 + if (
1495 + nextProps.hasOwnProperty(propKey) &&
1496 + nextProp !== lastProp &&
1497 + (nextProp != null || lastProp != null)
1498 + ) {
1499 + switch (propKey) {
1500 + case 'selected': {
1501 + // TODO: Remove support for selected on option.
1502 + (domElement: any).selected =
1503 + nextProp &&
1504 + typeof nextProp !== 'function' &&
1505 + typeof nextProp !== 'symbol';
1506 + break;
1507 + }
1508 + default: {
1509 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1510 + }
1511 + }
1512 + }
1513 + }
1514 + return;
1515 + }
1516 + case 'img':
1517 + case 'link':
1518 + case 'area':
1519 + case 'base':
1520 + case 'br':
1521 + case 'col':
1522 + case 'embed':
1523 + case 'hr':
1524 + case 'keygen':
1525 + case 'meta':
1526 + case 'param':
1527 + case 'source':
1528 + case 'track':
1529 + case 'wbr':
1530 + case 'menuitem': {
1531 + // Void elements
1532 + for (const propKey in lastProps) {
1533 + const lastProp = lastProps[propKey];
1534 + if (
1535 + lastProps.hasOwnProperty(propKey) &&
1536 + lastProp != null &&
1537 + !nextProps.hasOwnProperty(propKey)
1538 + ) {
1539 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1540 + }
1541 + }
1542 + for (const propKey in nextProps) {
1543 + const nextProp = nextProps[propKey];
1544 + const lastProp = lastProps[propKey];
1545 + if (
1546 + nextProps.hasOwnProperty(propKey) &&
1547 + nextProp !== lastProp &&
1548 + (nextProp != null || lastProp != null)
1549 + ) {
1550 + switch (propKey) {
1551 + case 'children':
1552 + case 'dangerouslySetInnerHTML': {
1553 + if (nextProp != null) {
1554 + // TODO: Can we make this a DEV warning to avoid this deny list?
1555 + throw new Error(
1556 + `${tag} is a void element tag and must neither have \`children\` nor ` +
1557 + 'use `dangerouslySetInnerHTML`.',
1558 + );
1559 + }
1560 + break;
1561 + }
1562 + // defaultChecked and defaultValue are ignored by setProp
1563 + default: {
1564 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1565 + }
1566 + }
1567 + }
1568 + }
1569 + return;
1570 + }
1571 + default: {
1572 + if (isCustomElement(tag, nextProps)) {
1573 + for (const propKey in lastProps) {
1574 + const lastProp = lastProps[propKey];
1575 + if (
1576 + lastProps.hasOwnProperty(propKey) &&
1577 + lastProp != null &&
1578 + !nextProps.hasOwnProperty(propKey)
1579 + ) {
1580 + setPropOnCustomElement(
1581 + domElement,
1582 + tag,
1583 + propKey,
1584 + null,
1585 + nextProps,
1586 + lastProp,
1587 + );
1588 + }
1589 + }
1590 + for (const propKey in nextProps) {
1591 + const nextProp = nextProps[propKey];
1592 + const lastProp = lastProps[propKey];
1593 + if (
1594 + nextProps.hasOwnProperty(propKey) &&
1595 + nextProp !== lastProp &&
1596 + (nextProp != null || lastProp != null)
1597 + ) {
1598 + setPropOnCustomElement(
1599 + domElement,
1600 + tag,
1601 + propKey,
1602 + nextProp,
1603 + nextProps,
1604 + lastProp,
1605 + );
1606 + }
1607 + }
1608 + return;
1609 + }
1610 + }
1611 + }
1612 +
1613 + for (const propKey in lastProps) {
1614 + const lastProp = lastProps[propKey];
1615 + if (
1616 + lastProps.hasOwnProperty(propKey) &&
1617 + lastProp != null &&
1618 + !nextProps.hasOwnProperty(propKey)
1619 + ) {
1620 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1621 + }
1622 + }
1623 + for (const propKey in nextProps) {
1624 + const nextProp = nextProps[propKey];
1625 + const lastProp = lastProps[propKey];
1626 + if (
1627 + nextProps.hasOwnProperty(propKey) &&
1628 + nextProp !== lastProp &&
1629 + (nextProp != null || lastProp != null)
1630 + ) {
1631 + setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1632 + }
1633 + }
1634 +}
1635 +
1636 +// Apply the diff.
1637 +export function updatePropertiesWithDiff(
1638 domElement: Element,
1639 updatePayload: Array<any>,
1640 tag: string,
@@ -1252,7 +1690,7 @@ export function updateProperties(
1690 }
1691 // defaultChecked and defaultValue are ignored by setProp
1692 default: {
1255 - setProp(domElement, tag, propKey, propValue, nextProps);
1693 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1694 }
1695 }
1696 }
@@ -1313,7 +1751,7 @@ export function updateProperties(
1751 }
1752 // defaultValue are ignored by setProp
1753 default: {
1316 - setProp(domElement, tag, propKey, propValue, nextProps);
1754 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1755 }
1756 }
1757 }
@@ -1346,7 +1784,7 @@ export function updateProperties(
1784 }
1785 // defaultValue is ignored by setProp
1786 default: {
1349 - setProp(domElement, tag, propKey, propValue, nextProps);
1787 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1788 }
1789 }
1790 }
@@ -1367,7 +1805,7 @@ export function updateProperties(
1805 break;
1806 }
1807 default: {
1370 - setProp(domElement, tag, propKey, propValue, nextProps);
1808 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1809 }
1810 }
1811 }
@@ -1406,7 +1844,7 @@ export function updateProperties(
1844 }
1845 // defaultChecked and defaultValue are ignored by setProp
1846 default: {
1409 - setProp(domElement, tag, propKey, propValue, nextProps);
1847 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1848 }
1849 }
1850 }
@@ -1423,6 +1861,7 @@ export function updateProperties(
1861 propKey,
1862 propValue,
1863 nextProps,
1864 + null,
1865 );
1866 }
1867 return;
@@ -1434,7 +1873,7 @@ export function updateProperties(
1873 for (let i = 0; i < updatePayload.length; i += 2) {
1874 const propKey = updatePayload[i];
1875 const propValue = updatePayload[i + 1];
1437 - setProp(domElement, tag, propKey, propValue, nextProps);
1876 + setProp(domElement, tag, propKey, propValue, nextProps, null);
1877 }
1878 }
1879
@@ -2377,7 +2816,18 @@ export function diffHydratedProperties(
2816 );
2817 }
2818 if (!isConcurrentMode || !enableClientRenderFallbackOnTextMismatch) {
2380 - updatePayload = ['children', children];
2819 + if (diffInCommitPhase) {
2820 + // We really should be patching this in the commit phase but since
2821 + // this only affects legacy mode hydration which is deprecated anyway
2822 + // we can get away with it.
2823 + // Host singletons get their children appended and don't use the text
2824 + // content mechanism.
2825 + if (!enableHostSingletons || tag !== 'body') {
2826 + domElement.textContent = (children: any);
2827 + }
2828 + } else {
2829 + updatePayload = ['children', children];
2830 + }
2831 }
2832 }
2833 }
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+21 -3
@@ -47,6 +47,7 @@ import {
47 setInitialProperties,
48 diffProperties,
49 updateProperties,
50 + updatePropertiesWithDiff,
51 diffHydratedProperties,
52 diffHydratedText,
53 trapClickOnNonInteractiveElement,
@@ -86,6 +87,7 @@ import {
87 enableFloat,
88 enableHostSingletons,
89 enableTrustedTypesIntegration,
90 + diffInCommitPhase,
91 } from 'shared/ReactFeatureFlags';
92 import {
93 HostComponent,
@@ -485,6 +487,10 @@ export function prepareUpdate(
487 newProps: Props,
488 hostContext: HostContext,
489 ): null | Array<mixed> {
490 + if (diffInCommitPhase) {
491 + // TODO: Figure out how to validateDOMNesting when children turn into a string.
492 + return null;
493 + }
494 if (__DEV__) {
495 const hostContextDev = ((hostContext: any): HostContextDev);
496 if (
@@ -642,14 +648,26 @@ export function commitMount(
648
649 export function commitUpdate(
650 domElement: Instance,
645 - updatePayload: Array<mixed>,
651 + updatePayload: any,
652 type: string,
653 oldProps: Props,
654 newProps: Props,
655 internalInstanceHandle: Object,
656 ): void {
651 - // Apply the diff to the DOM node.
652 - updateProperties(domElement, updatePayload, type, oldProps, newProps);
657 + if (diffInCommitPhase) {
658 + // Diff and update the properties.
659 + updateProperties(domElement, type, oldProps, newProps);
660 + } else {
661 + // Apply the diff to the DOM node.
662 + updatePropertiesWithDiff(
663 + domElement,
664 + updatePayload,
665 + type,
666 + oldProps,
667 + newProps,
668 + );
669 + }
670 +
671 // Update the props handle so that we know which props are the ones with
672 // with current event handlers.
673 updateFiberProps(domElement, newProps);
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+20 -1
@@ -42,6 +42,8 @@ const {
42 unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
43 } = nativeFabricUIManager;
44
45 +import {diffInCommitPhase} from 'shared/ReactFeatureFlags';
46 +
47 const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
48
49 // Counter for uniquely identifying views.
@@ -288,6 +290,9 @@ export function prepareUpdate(
290 newProps: Props,
291 hostContext: HostContext,
292 ): null | Object {
293 + if (diffInCommitPhase) {
294 + return null;
295 + }
296 const viewConfig = instance.canonical.viewConfig;
297 const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
298 // TODO: If the event handlers have changed, we need to update the current props
@@ -355,13 +360,27 @@ export function cloneInstance(
360 keepChildren: boolean,
361 recyclableInstance: null | Instance,
362 ): Instance {
363 + if (diffInCommitPhase) {
364 + const viewConfig = instance.canonical.viewConfig;
365 + updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
366 + // TODO: If the event handlers have changed, we need to update the current props
367 + // in the commit phase but there is no host config hook to do it yet.
368 + // So instead we hack it by updating it in the render phase.
369 + instance.canonical.currentProps = newProps;
370 + }
371 +
372 const node = instance.node;
373 let clone;
374 if (keepChildren) {
375 if (updatePayload !== null) {
376 clone = cloneNodeWithNewProps(node, updatePayload);
377 } else {
364 - clone = cloneNode(node);
378 + if (diffInCommitPhase) {
379 + // No changes
380 + return instance;
381 + } else {
382 + clone = cloneNode(node);
383 + }
384 }
385 } else {
386 if (updatePayload !== null) {
packages/react-noop-renderer/src/createReactNoop.js
-11
@@ -88,7 +88,6 @@ if (__DEV__) {
88
89 function createReactNoop(reconciler: Function, useMutation: boolean) {
90 let instanceCounter = 0;
91 - let hostDiffCounter = 0;
91 let hostUpdateCounter = 0;
92 let hostCloneCounter = 0;
93
@@ -458,16 +457,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
457 oldProps: Props,
458 newProps: Props,
459 ): null | {...} {
461 - if (type === 'errorInCompletePhase') {
462 - throw new Error('Error in host config.');
463 - }
460 if (oldProps === null) {
461 throw new Error('Should have old props');
462 }
463 if (newProps === null) {
464 throw new Error('Should have new props');
465 }
470 - hostDiffCounter++;
466 return UPDATE_SIGNAL;
467 },
468
@@ -1186,30 +1181,24 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1181 },
1182
1183 startTrackingHostCounters(): void {
1189 - hostDiffCounter = 0;
1184 hostUpdateCounter = 0;
1185 hostCloneCounter = 0;
1186 },
1187
1188 stopTrackingHostCounters():
1189 | {
1196 - hostDiffCounter: number,
1190 hostUpdateCounter: number,
1191 }
1192 | {
1200 - hostDiffCounter: number,
1193 hostCloneCounter: number,
1194 } {
1195 const result = useMutation
1196 ? {
1205 - hostDiffCounter,
1197 hostUpdateCounter,
1198 }
1199 : {
1209 - hostDiffCounter,
1200 hostCloneCounter,
1201 };
1212 - hostDiffCounter = 0;
1202 hostUpdateCounter = 0;
1203 hostCloneCounter = 0;
1204
packages/react-reconciler/src/ReactFiberCommitWork.js
+2 -1
@@ -54,6 +54,7 @@ import {
54 enableFloat,
55 enableLegacyHidden,
56 enableHostSingletons,
57 + diffInCommitPhase,
58 } from 'shared/ReactFeatureFlags';
59 import {
60 FunctionComponent,
@@ -2774,7 +2775,7 @@ function commitMutationEffectsOnFiber(
2775 const updatePayload: null | UpdatePayload =
2776 (finishedWork.updateQueue: any);
2777 finishedWork.updateQueue = null;
2777 - if (updatePayload !== null) {
2778 + if (updatePayload !== null || diffInCommitPhase) {
2779 try {
2780 commitUpdate(
2781 instance,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+48 -36
@@ -38,6 +38,7 @@ import {
38 enableCache,
39 enableTransitionTracing,
40 enableFloat,
41 + diffInCommitPhase,
42 } from 'shared/ReactFeatureFlags';
43
44 import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource';
@@ -432,29 +433,32 @@ function updateHostComponent(
433 return;
434 }
435
435 - // If we get updated because one of our children updated, we don't
436 - // have newProps so we'll have to reuse them.
437 - // TODO: Split the update API as separate for the props vs. children.
438 - // Even better would be if children weren't special cased at all tho.
439 - const instance: Instance = workInProgress.stateNode;
440 -
441 - const currentHostContext = getHostContext();
442 - // TODO: Experiencing an error where oldProps is null. Suggests a host
443 - // component is hitting the resume path. Figure out why. Possibly
444 - // related to `hidden`.
445 - const updatePayload = prepareUpdate(
446 - instance,
447 - type,
448 - oldProps,
449 - newProps,
450 - currentHostContext,
451 - );
452 - // TODO: Type this specific to this type of component.
453 - workInProgress.updateQueue = (updatePayload: any);
454 - // If the update payload indicates that there is a change or if there
455 - // is a new ref we mark this as an update. All the work is done in commitWork.
456 - if (updatePayload) {
436 + if (diffInCommitPhase) {
437 markUpdate(workInProgress);
438 + } else {
439 + // If we get updated because one of our children updated, we don't
440 + // have newProps so we'll have to reuse them.
441 + // TODO: Split the update API as separate for the props vs. children.
442 + // Even better would be if children weren't special cased at all tho.
443 + const instance: Instance = workInProgress.stateNode;
444 + // TODO: Experiencing an error where oldProps is null. Suggests a host
445 + // component is hitting the resume path. Figure out why. Possibly
446 + // related to `hidden`.
447 + const currentHostContext = getHostContext();
448 + const updatePayload = prepareUpdate(
449 + instance,
450 + type,
451 + oldProps,
452 + newProps,
453 + currentHostContext,
454 + );
455 + // TODO: Type this specific to this type of component.
456 + workInProgress.updateQueue = (updatePayload: any);
457 + // If the update payload indicates that there is a change or if there
458 + // is a new ref we mark this as an update. All the work is done in commitWork.
459 + if (updatePayload) {
460 + markUpdate(workInProgress);
461 + }
462 }
463 } else if (supportsPersistence) {
464 const currentInstance = current.stateNode;
@@ -471,20 +475,22 @@ function updateHostComponent(
475 const recyclableInstance: Instance = workInProgress.stateNode;
476 const currentHostContext = getHostContext();
477 let updatePayload = null;
474 - if (oldProps !== newProps) {
475 - updatePayload = prepareUpdate(
476 - recyclableInstance,
477 - type,
478 - oldProps,
479 - newProps,
480 - currentHostContext,
481 - );
482 - }
483 - if (childrenUnchanged && updatePayload === null) {
484 - // No changes, just reuse the existing instance.
485 - // Note that this might release a previous clone.
486 - workInProgress.stateNode = currentInstance;
487 - return;
478 + if (!diffInCommitPhase) {
479 + if (oldProps !== newProps) {
480 + updatePayload = prepareUpdate(
481 + recyclableInstance,
482 + type,
483 + oldProps,
484 + newProps,
485 + currentHostContext,
486 + );
487 + }
488 + if (childrenUnchanged && updatePayload === null) {
489 + // No changes, just reuse the existing instance.
490 + // Note that this might release a previous clone.
491 + workInProgress.stateNode = currentInstance;
492 + return;
493 + }
494 }
495 const newInstance = cloneInstance(
496 currentInstance,
@@ -496,6 +502,12 @@ function updateHostComponent(
502 childrenUnchanged,
503 recyclableInstance,
504 );
505 + if (diffInCommitPhase && newInstance === currentInstance) {
506 + // No changes, just reuse the existing instance.
507 + // Note that this might release a previous clone.
508 + workInProgress.stateNode = currentInstance;
509 + return;
510 + }
511
512 if (
513 finalizeInitialChildren(newInstance, type, newProps, currentHostContext)
packages/react-reconciler/src/ReactFiberHydrationContext.js
+9 -5
@@ -39,6 +39,7 @@ import {
39 enableHostSingletons,
40 enableFloat,
41 enableClientRenderFallbackOnTextMismatch,
42 + diffInCommitPhase,
43 } from 'shared/ReactFeatureFlags';
44
45 import {
@@ -687,12 +688,15 @@ function prepareToHydrateHostInstance(
688 fiber,
689 shouldWarnIfMismatchDev,
690 );
691 +
692 // TODO: Type this specific to this type of component.
691 - fiber.updateQueue = (updatePayload: any);
692 - // If the update payload indicates that there is a change or if there
693 - // is a new ref we mark this as an update.
694 - if (updatePayload !== null) {
695 - return true;
693 + if (!diffInCommitPhase) {
694 + fiber.updateQueue = (updatePayload: any);
695 + // If the update payload indicates that there is a change or if there
696 + // is a new ref we mark this as an update.
697 + if (updatePayload !== null) {
698 + return true;
699 + }
700 }
701 return false;
702 }
packages/react-reconciler/src/__tests__/ReactIncrementalUpdatesMinimalism-test.js
-10
@@ -35,14 +35,12 @@ describe('ReactIncrementalUpdatesMinimalism', () => {
35 ReactNoop.startTrackingHostCounters();
36 await act(() => ReactNoop.render(<Parent />));
37 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
38 - hostDiffCounter: 0,
38 hostUpdateCounter: 0,
39 });
40
41 ReactNoop.startTrackingHostCounters();
42 await act(() => ReactNoop.render(<Parent />));
43 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
45 - hostDiffCounter: 1,
44 hostUpdateCounter: 1,
45 });
46 });
@@ -75,14 +73,12 @@ describe('ReactIncrementalUpdatesMinimalism', () => {
73 ReactNoop.startTrackingHostCounters();
74 await act(() => ReactNoop.render(<Parent />));
75 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
78 - hostDiffCounter: 0,
76 hostUpdateCounter: 0,
77 });
78
79 ReactNoop.startTrackingHostCounters();
80 await act(() => ReactNoop.render(<Parent />));
81 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
85 - hostDiffCounter: 0,
82 hostUpdateCounter: 0,
83 });
84 });
@@ -128,17 +124,12 @@ describe('ReactIncrementalUpdatesMinimalism', () => {
124 ReactNoop.startTrackingHostCounters();
125 await act(() => ReactNoop.render(<Parent />));
126 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
131 - hostDiffCounter: 0,
127 hostUpdateCounter: 0,
128 });
129
130 ReactNoop.startTrackingHostCounters();
131 await act(() => childInst.setState({name: 'Robin'}));
132 expect(ReactNoop.stopTrackingHostCounters()).toEqual({
138 - // Child > div
139 - // Child > Leaf > span
140 - // Child > Leaf > span > b
141 - hostDiffCounter: 3,
133 // Child > div
134 // Child > Leaf > span
135 // Child > Leaf > span > b
@@ -159,7 +150,6 @@ describe('ReactIncrementalUpdatesMinimalism', () => {
150 // Parent > section > div > hr
151 // Parent > section > div > Leaf > span
152 // Parent > section > div > Leaf > span > b
162 - hostDiffCounter: 10,
153 hostUpdateCounter: 10,
154 });
155 });
packages/react-reconciler/src/__tests__/ReactPersistentUpdatesMinimalism-test.js
-10
@@ -34,14 +34,12 @@ describe('ReactPersistentUpdatesMinimalism', () => {
34 ReactNoopPersistent.startTrackingHostCounters();
35 await act(() => ReactNoopPersistent.render(<Parent />));
36 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
37 - hostDiffCounter: 0,
37 hostCloneCounter: 0,
38 });
39
40 ReactNoopPersistent.startTrackingHostCounters();
41 await act(() => ReactNoopPersistent.render(<Parent />));
42 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
44 - hostDiffCounter: 1,
43 hostCloneCounter: 1,
44 });
45 });
@@ -74,14 +72,12 @@ describe('ReactPersistentUpdatesMinimalism', () => {
72 ReactNoopPersistent.startTrackingHostCounters();
73 await act(() => ReactNoopPersistent.render(<Parent />));
74 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
77 - hostDiffCounter: 0,
75 hostCloneCounter: 0,
76 });
77
78 ReactNoopPersistent.startTrackingHostCounters();
79 await act(() => ReactNoopPersistent.render(<Parent />));
80 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
84 - hostDiffCounter: 0,
81 hostCloneCounter: 0,
82 });
83 });
@@ -127,17 +123,12 @@ describe('ReactPersistentUpdatesMinimalism', () => {
123 ReactNoopPersistent.startTrackingHostCounters();
124 await act(() => ReactNoopPersistent.render(<Parent />));
125 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
130 - hostDiffCounter: 0,
126 hostCloneCounter: 0,
127 });
128
129 ReactNoopPersistent.startTrackingHostCounters();
130 await act(() => childInst.setState({name: 'Robin'}));
131 expect(ReactNoopPersistent.stopTrackingHostCounters()).toEqual({
137 - // section > div > Child > div
138 - // section > div > Child > Leaf > span
139 - // section > div > Child > Leaf > span > b
140 - hostDiffCounter: 3,
132 // section
133 // section > div
134 // section > div > Child > div
@@ -159,7 +150,6 @@ describe('ReactPersistentUpdatesMinimalism', () => {
150 // Parent > section > div > hr
151 // Parent > section > div > Leaf > span
152 // Parent > section > div > Leaf > span > b
162 - hostDiffCounter: 10,
153 hostCloneCounter: 10,
154 });
155 });
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+1 -1
@@ -230,7 +230,7 @@ export const supportsMutation = true;
230
231 export function commitUpdate(
232 instance: Instance,
233 - updatePayload: {...},
233 + updatePayload: null | {...},
234 type: string,
235 oldProps: Props,
236 newProps: Props,
packages/shared/ReactFeatureFlags.js
+3
@@ -122,6 +122,9 @@ export const enableUseEffectEventHook = __EXPERIMENTAL__;
122 // (handled with an MutationObserver) instead of inline-scripts
123 export const enableFizzExternalRuntime = true;
124
125 +// Performance related test
126 +export const diffInCommitPhase = __EXPERIMENTAL__;
127 +
128 // -----------------------------------------------------------------------------
129 // Chopping Block
130 //
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -82,5 +82,7 @@ export const enableHostSingletons = true;
82 export const useModernStrictMode = false;
83 export const enableFizzExternalRuntime = false;
84
85 +export const diffInCommitPhase = true;
86 +
87 // Flow magic to verify the exports of this file match the original version.
88 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -72,5 +72,7 @@ export const useModernStrictMode = false;
72 export const enableFizzExternalRuntime = false;
73 export const enableDeferRootSchedulingToMicrotask = true;
74
75 +export const diffInCommitPhase = true;
76 +
77 // Flow magic to verify the exports of this file match the original version.
78 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -72,5 +72,7 @@ export const useModernStrictMode = false;
72 export const enableFizzExternalRuntime = false;
73 export const enableDeferRootSchedulingToMicrotask = true;
74
75 +export const diffInCommitPhase = true;
76 +
77 // Flow magic to verify the exports of this file match the original version.
78 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+2
@@ -69,5 +69,7 @@ export const enableHostSingletons = true;
69 export const useModernStrictMode = false;
70 export const enableDeferRootSchedulingToMicrotask = true;
71
72 +export const diffInCommitPhase = true;
73 +
74 // Flow magic to verify the exports of this file match the original version.
75 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -74,5 +74,7 @@ export const useModernStrictMode = false;
74 export const enableFizzExternalRuntime = false;
75 export const enableDeferRootSchedulingToMicrotask = true;
76
77 +export const diffInCommitPhase = true;
78 +
79 // Flow magic to verify the exports of this file match the original version.
80 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -25,6 +25,7 @@ export const enableUnifiedSyncLane = __VARIANT__;
25 export const enableTransitionTracing = __VARIANT__;
26 export const enableCustomElementPropertySupport = __VARIANT__;
27 export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
28 +export const diffInCommitPhase = __VARIANT__;
29
30 // Enable this flag to help with concurrent mode debugging.
31 // It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -29,6 +29,7 @@ export const {
29 enableTransitionTracing,
30 enableCustomElementPropertySupport,
31 enableDeferRootSchedulingToMicrotask,
32 + diffInCommitPhase,
33 } = dynamicFeatureFlags;
34
35 // On WWW, __EXPERIMENTAL__ is used for a new modern build.