Skip to content

Commit 2cfe8d3

Browse files
committed
Fast JSX: Don't clone props object (#28768)
(Unless "key" is spread onto the element.) Historically, the JSX runtime clones the props object that is passed in. We've done this for two reasons. One reason is that there are certain prop names that are reserved by React, like `key` and (before React 19) `ref`. These are not actual props and are not observable by the target component; React uses them internally but removes them from the props object before passing them to userspace. The second reason is that the classic JSX runtime, `createElement`, is both a compiler target _and_ a public API that can be called manually. Therefore, we can't assume that the props object that is passed into `createElement` won't be mutated by userspace code after it is passed in. However, the new JSX runtime, `jsx`, is not a public API — it's solely a compiler target, and the compiler _will_ always pass a fresh, inline object. So the only reason to clone the props is if a reserved prop name is used. In React 19, `ref` is no longer a reserved prop name, and `key` will only appear in the props object if it is spread onto the element. (Because if `key` is statically defined, the compiler will pass it as a separate argument to the `jsx` function.) So the only remaining reason to clone the props object is if `key` is spread onto the element, which is a rare case, and also triggers a warning in development. In a future release, we will not remove a spread key from the props object. (But we'll still warn.) We'll always pass the object straight through. The expected impact is much faster JSX element creation, which in many apps is a significant slice of the overall runtime cost of rendering. DiffTrain build for [d1547de](d1547de)
1 parent c7d55de commit 2cfe8d3

11 files changed

+259
-164
lines changed

compiled/facebook-www/JSXDEVRuntime-dev.classic.js

+38-21
Original file line numberDiff line numberDiff line change
@@ -1352,9 +1352,6 @@ if (__DEV__) {
13521352
}
13531353
}
13541354

1355-
var propName; // Reserved names are extracted
1356-
1357-
var props = {};
13581355
var key = null;
13591356
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
13601357
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
@@ -1391,22 +1388,42 @@ if (__DEV__) {
13911388
{
13921389
warnIfStringRefCannotBeAutoConverted(config, self);
13931390
}
1394-
} // Remaining properties are added to a new props object
1391+
}
13951392

1396-
for (propName in config) {
1397-
if (
1398-
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1399-
propName !== "key" &&
1400-
(enableRefAsProp || propName !== "ref")
1401-
) {
1402-
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1403-
props.ref = coerceStringRef(
1404-
config[propName],
1405-
ReactCurrentOwner.current,
1406-
type
1407-
);
1408-
} else {
1409-
props[propName] = config[propName];
1393+
var props;
1394+
1395+
if (enableRefAsProp && disableStringRefs && !("key" in config)) {
1396+
// If key was not spread in, we can reuse the original props object. This
1397+
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
1398+
// target and the compiler always passes a new object. For `createElement`,
1399+
// we can't assume a new object is passed every time because it can be
1400+
// called manually.
1401+
//
1402+
// Spreading key is a warning in dev. In a future release, we will not
1403+
// remove a spread key from the props object. (But we'll still warn.) We'll
1404+
// always pass the object straight through.
1405+
props = config;
1406+
} else {
1407+
// We need to remove reserved props (key, prop, ref). Create a fresh props
1408+
// object and copy over all the non-reserved props. We don't use `delete`
1409+
// because in V8 it will deopt the object to dictionary mode.
1410+
props = {};
1411+
1412+
for (var propName in config) {
1413+
if (
1414+
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1415+
propName !== "key" &&
1416+
(enableRefAsProp || propName !== "ref")
1417+
) {
1418+
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1419+
props.ref = coerceStringRef(
1420+
config[propName],
1421+
ReactCurrentOwner.current,
1422+
type
1423+
);
1424+
} else {
1425+
props[propName] = config[propName];
1426+
}
14101427
}
14111428
}
14121429
}
@@ -1416,9 +1433,9 @@ if (__DEV__) {
14161433
if (type && type.defaultProps) {
14171434
var defaultProps = type.defaultProps;
14181435

1419-
for (propName in defaultProps) {
1420-
if (props[propName] === undefined) {
1421-
props[propName] = defaultProps[propName];
1436+
for (var _propName2 in defaultProps) {
1437+
if (props[_propName2] === undefined) {
1438+
props[_propName2] = defaultProps[_propName2];
14221439
}
14231440
}
14241441
}

compiled/facebook-www/JSXDEVRuntime-dev.modern.js

+38-21
Original file line numberDiff line numberDiff line change
@@ -1354,9 +1354,6 @@ if (__DEV__) {
13541354
}
13551355
}
13561356

1357-
var propName; // Reserved names are extracted
1358-
1359-
var props = {};
13601357
var key = null;
13611358
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
13621359
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
@@ -1393,22 +1390,42 @@ if (__DEV__) {
13931390
{
13941391
warnIfStringRefCannotBeAutoConverted(config, self);
13951392
}
1396-
} // Remaining properties are added to a new props object
1393+
}
13971394

1398-
for (propName in config) {
1399-
if (
1400-
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1401-
propName !== "key" &&
1402-
(enableRefAsProp || propName !== "ref")
1403-
) {
1404-
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1405-
props.ref = coerceStringRef(
1406-
config[propName],
1407-
ReactCurrentOwner.current,
1408-
type
1409-
);
1410-
} else {
1411-
props[propName] = config[propName];
1395+
var props;
1396+
1397+
if (enableRefAsProp && disableStringRefs && !("key" in config)) {
1398+
// If key was not spread in, we can reuse the original props object. This
1399+
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
1400+
// target and the compiler always passes a new object. For `createElement`,
1401+
// we can't assume a new object is passed every time because it can be
1402+
// called manually.
1403+
//
1404+
// Spreading key is a warning in dev. In a future release, we will not
1405+
// remove a spread key from the props object. (But we'll still warn.) We'll
1406+
// always pass the object straight through.
1407+
props = config;
1408+
} else {
1409+
// We need to remove reserved props (key, prop, ref). Create a fresh props
1410+
// object and copy over all the non-reserved props. We don't use `delete`
1411+
// because in V8 it will deopt the object to dictionary mode.
1412+
props = {};
1413+
1414+
for (var propName in config) {
1415+
if (
1416+
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1417+
propName !== "key" &&
1418+
(enableRefAsProp || propName !== "ref")
1419+
) {
1420+
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1421+
props.ref = coerceStringRef(
1422+
config[propName],
1423+
ReactCurrentOwner.current,
1424+
type
1425+
);
1426+
} else {
1427+
props[propName] = config[propName];
1428+
}
14121429
}
14131430
}
14141431
}
@@ -1418,9 +1435,9 @@ if (__DEV__) {
14181435
if (type && type.defaultProps) {
14191436
var defaultProps = type.defaultProps;
14201437

1421-
for (propName in defaultProps) {
1422-
if (props[propName] === undefined) {
1423-
props[propName] = defaultProps[propName];
1438+
for (var _propName2 in defaultProps) {
1439+
if (props[_propName2] === undefined) {
1440+
props[_propName2] = defaultProps[_propName2];
14241441
}
14251442
}
14261443
}

compiled/facebook-www/REVISION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
bfd8da807c75a2d123627415f9eaf2d36ac3ed6a
1+
d1547defe34cee6326a61059148afc83228d8ecf

compiled/facebook-www/React-dev.classic.js

+39-22
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "19.0.0-www-classic-ff21e352";
27+
var ReactVersion = "19.0.0-www-classic-4511ca3e";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,
@@ -1760,9 +1760,6 @@ if (__DEV__) {
17601760
}
17611761
}
17621762

1763-
var propName; // Reserved names are extracted
1764-
1765-
var props = {};
17661763
var key = null;
17671764
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
17681765
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
@@ -1799,22 +1796,42 @@ if (__DEV__) {
17991796
{
18001797
warnIfStringRefCannotBeAutoConverted(config, self);
18011798
}
1802-
} // Remaining properties are added to a new props object
1799+
}
18031800

1804-
for (propName in config) {
1805-
if (
1806-
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1807-
propName !== "key" &&
1808-
(enableRefAsProp || propName !== "ref")
1809-
) {
1810-
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1811-
props.ref = coerceStringRef(
1812-
config[propName],
1813-
ReactCurrentOwner.current,
1814-
type
1815-
);
1816-
} else {
1817-
props[propName] = config[propName];
1801+
var props;
1802+
1803+
if (enableRefAsProp && disableStringRefs && !("key" in config)) {
1804+
// If key was not spread in, we can reuse the original props object. This
1805+
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
1806+
// target and the compiler always passes a new object. For `createElement`,
1807+
// we can't assume a new object is passed every time because it can be
1808+
// called manually.
1809+
//
1810+
// Spreading key is a warning in dev. In a future release, we will not
1811+
// remove a spread key from the props object. (But we'll still warn.) We'll
1812+
// always pass the object straight through.
1813+
props = config;
1814+
} else {
1815+
// We need to remove reserved props (key, prop, ref). Create a fresh props
1816+
// object and copy over all the non-reserved props. We don't use `delete`
1817+
// because in V8 it will deopt the object to dictionary mode.
1818+
props = {};
1819+
1820+
for (var propName in config) {
1821+
if (
1822+
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1823+
propName !== "key" &&
1824+
(enableRefAsProp || propName !== "ref")
1825+
) {
1826+
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1827+
props.ref = coerceStringRef(
1828+
config[propName],
1829+
ReactCurrentOwner.current,
1830+
type
1831+
);
1832+
} else {
1833+
props[propName] = config[propName];
1834+
}
18181835
}
18191836
}
18201837
}
@@ -1824,9 +1841,9 @@ if (__DEV__) {
18241841
if (type && type.defaultProps) {
18251842
var defaultProps = type.defaultProps;
18261843

1827-
for (propName in defaultProps) {
1828-
if (props[propName] === undefined) {
1829-
props[propName] = defaultProps[propName];
1844+
for (var _propName2 in defaultProps) {
1845+
if (props[_propName2] === undefined) {
1846+
props[_propName2] = defaultProps[_propName2];
18301847
}
18311848
}
18321849
}

compiled/facebook-www/React-dev.modern.js

+39-22
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "19.0.0-www-modern-99738e97";
27+
var ReactVersion = "19.0.0-www-modern-8e3891bc";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,
@@ -1762,9 +1762,6 @@ if (__DEV__) {
17621762
}
17631763
}
17641764

1765-
var propName; // Reserved names are extracted
1766-
1767-
var props = {};
17681765
var key = null;
17691766
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
17701767
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
@@ -1801,22 +1798,42 @@ if (__DEV__) {
18011798
{
18021799
warnIfStringRefCannotBeAutoConverted(config, self);
18031800
}
1804-
} // Remaining properties are added to a new props object
1801+
}
18051802

1806-
for (propName in config) {
1807-
if (
1808-
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1809-
propName !== "key" &&
1810-
(enableRefAsProp || propName !== "ref")
1811-
) {
1812-
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1813-
props.ref = coerceStringRef(
1814-
config[propName],
1815-
ReactCurrentOwner.current,
1816-
type
1817-
);
1818-
} else {
1819-
props[propName] = config[propName];
1803+
var props;
1804+
1805+
if (enableRefAsProp && disableStringRefs && !("key" in config)) {
1806+
// If key was not spread in, we can reuse the original props object. This
1807+
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
1808+
// target and the compiler always passes a new object. For `createElement`,
1809+
// we can't assume a new object is passed every time because it can be
1810+
// called manually.
1811+
//
1812+
// Spreading key is a warning in dev. In a future release, we will not
1813+
// remove a spread key from the props object. (But we'll still warn.) We'll
1814+
// always pass the object straight through.
1815+
props = config;
1816+
} else {
1817+
// We need to remove reserved props (key, prop, ref). Create a fresh props
1818+
// object and copy over all the non-reserved props. We don't use `delete`
1819+
// because in V8 it will deopt the object to dictionary mode.
1820+
props = {};
1821+
1822+
for (var propName in config) {
1823+
if (
1824+
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
1825+
propName !== "key" &&
1826+
(enableRefAsProp || propName !== "ref")
1827+
) {
1828+
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
1829+
props.ref = coerceStringRef(
1830+
config[propName],
1831+
ReactCurrentOwner.current,
1832+
type
1833+
);
1834+
} else {
1835+
props[propName] = config[propName];
1836+
}
18201837
}
18211838
}
18221839
}
@@ -1826,9 +1843,9 @@ if (__DEV__) {
18261843
if (type && type.defaultProps) {
18271844
var defaultProps = type.defaultProps;
18281845

1829-
for (propName in defaultProps) {
1830-
if (props[propName] === undefined) {
1831-
props[propName] = defaultProps[propName];
1846+
for (var _propName2 in defaultProps) {
1847+
if (props[_propName2] === undefined) {
1848+
props[_propName2] = defaultProps[_propName2];
18321849
}
18331850
}
18341851
}

0 commit comments

Comments
 (0)