Skip to content

Commit

Permalink
Fast JSX: Don't clone props object (#28768)
Browse files Browse the repository at this point in the history
(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 commit d1547de.
  • Loading branch information
acdlite committed Apr 5, 2024
1 parent 4e6493a commit 86065f6
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<3be60e1b6619e00a767ed0394fe7c1eb>>
* @generated SignedSource<<ec56b30d76b5a69544373d710da9f9aa>>
*/

"use strict";
Expand Down Expand Up @@ -1280,9 +1280,6 @@ if (__DEV__) {
}
}

var propName; // Reserved names are extracted

var props = {};
var key = null;
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
Expand Down Expand Up @@ -1319,16 +1316,25 @@ if (__DEV__) {
{
warnIfStringRefCannotBeAutoConverted(config, self);
}
} // Remaining properties are added to a new props object
}

for (propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
var props;

{
// We need to remove reserved props (key, prop, ref). Create a fresh props
// object and copy over all the non-reserved props. We don't use `delete`
// because in V8 it will deopt the object to dictionary mode.
props = {};

for (var propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
}
}
}
}
Expand All @@ -1338,9 +1344,9 @@ if (__DEV__) {
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;

for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
for (var _propName2 in defaultProps) {
if (props[_propName2] === undefined) {
props[_propName2] = defaultProps[_propName2];
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<14b1fac5da603ed3a82ddba00e57ffbe>>
* @generated SignedSource<<f9829995214271eca2ce345a7f2da74a>>
*/

"use strict";
Expand Down Expand Up @@ -1316,9 +1316,6 @@ if (__DEV__) {
}
}

var propName; // Reserved names are extracted

var props = {};
var key = null;
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
Expand Down Expand Up @@ -1355,16 +1352,25 @@ if (__DEV__) {
{
warnIfStringRefCannotBeAutoConverted(config, self);
}
} // Remaining properties are added to a new props object
}

for (propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
var props;

{
// We need to remove reserved props (key, prop, ref). Create a fresh props
// object and copy over all the non-reserved props. We don't use `delete`
// because in V8 it will deopt the object to dictionary mode.
props = {};

for (var propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
}
}
}
}
Expand All @@ -1374,9 +1380,9 @@ if (__DEV__) {
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;

for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
for (var _propName2 in defaultProps) {
if (props[_propName2] === undefined) {
props[_propName2] = defaultProps[_propName2];
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<95871e7bee6e8808b03f8a095c660457>>
* @generated SignedSource<<d24a690e1d6994ec3438bae6ba3ca710>>
*/

"use strict";
Expand Down Expand Up @@ -36,9 +36,7 @@ var disableDefaultPropsExceptForClasses =
ReactCurrentOwner =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
function jsxProd(type, config, maybeKey) {
var propName,
props = {},
key = null,
var key = null,
ref = null;
void 0 !== maybeKey && (key = "" + maybeKey);
void 0 !== config.key && (key = "" + config.key);
Expand All @@ -54,20 +52,24 @@ function jsxProd(type, config, maybeKey) {
ReactCurrentOwner.current
);
}
for (propName in config)
maybeKey = {};
for (var propName in config)
hasOwnProperty.call(config, propName) &&
"key" !== propName &&
"ref" !== propName &&
(props[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps)
for (propName in ((config = type.defaultProps), config))
void 0 === props[propName] && (props[propName] = config[propName]);
(maybeKey[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps) {
config = type.defaultProps;
for (var propName$0 in config)
void 0 === maybeKey[propName$0] &&
(maybeKey[propName$0] = config[propName$0]);
}
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
props: maybeKey,
_owner: ReactCurrentOwner.current
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<95871e7bee6e8808b03f8a095c660457>>
* @generated SignedSource<<d24a690e1d6994ec3438bae6ba3ca710>>
*/

"use strict";
Expand Down Expand Up @@ -36,9 +36,7 @@ var disableDefaultPropsExceptForClasses =
ReactCurrentOwner =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
function jsxProd(type, config, maybeKey) {
var propName,
props = {},
key = null,
var key = null,
ref = null;
void 0 !== maybeKey && (key = "" + maybeKey);
void 0 !== config.key && (key = "" + config.key);
Expand All @@ -54,20 +52,24 @@ function jsxProd(type, config, maybeKey) {
ReactCurrentOwner.current
);
}
for (propName in config)
maybeKey = {};
for (var propName in config)
hasOwnProperty.call(config, propName) &&
"key" !== propName &&
"ref" !== propName &&
(props[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps)
for (propName in ((config = type.defaultProps), config))
void 0 === props[propName] && (props[propName] = config[propName]);
(maybeKey[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps) {
config = type.defaultProps;
for (var propName$0 in config)
void 0 === maybeKey[propName$0] &&
(maybeKey[propName$0] = config[propName$0]);
}
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
props: maybeKey,
_owner: ReactCurrentOwner.current
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<3b8aa03ba713718447aaac97d2ea80ce>>
* @generated SignedSource<<c641be89261f73fa6d57361dd219a15d>>
*/

"use strict";
Expand All @@ -26,7 +26,7 @@ if (__DEV__) {
}
var dynamicFlagsUntyped = require("ReactNativeInternalFeatureFlags");

var ReactVersion = "19.0.0-canary-7c6402ae";
var ReactVersion = "19.0.0-canary-33300bbd";

// ATTENTION
// When adding new symbols to this file,
Expand Down Expand Up @@ -1702,9 +1702,6 @@ if (__DEV__) {
}
}

var propName; // Reserved names are extracted

var props = {};
var key = null;
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
Expand Down Expand Up @@ -1741,16 +1738,25 @@ if (__DEV__) {
{
warnIfStringRefCannotBeAutoConverted(config, self);
}
} // Remaining properties are added to a new props object
}

for (propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
var props;

{
// We need to remove reserved props (key, prop, ref). Create a fresh props
// object and copy over all the non-reserved props. We don't use `delete`
// because in V8 it will deopt the object to dictionary mode.
props = {};

for (var propName in config) {
if (
hasOwnProperty.call(config, propName) && // Skip over reserved prop names
propName !== "key" &&
propName !== "ref"
) {
{
props[propName] = config[propName];
}
}
}
}
Expand All @@ -1760,9 +1766,9 @@ if (__DEV__) {
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;

for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
for (var _propName2 in defaultProps) {
if (props[_propName2] === undefined) {
props[_propName2] = defaultProps[_propName2];
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<1d4dfe262135ef97303d2935beb2bc9f>>
* @generated SignedSource<<c127deb722db5c1322641d4b8f8d0b01>>
*/

"use strict";
Expand Down Expand Up @@ -108,31 +108,33 @@ function ReactElement(type, key, _ref, self, source, owner, props) {
};
}
function jsxProd(type, config, maybeKey) {
var propName,
props = {},
key = null,
var key = null,
ref = null;
void 0 !== maybeKey && (key = "" + maybeKey);
void 0 !== config.key && (key = "" + config.key);
void 0 !== config.ref &&
((ref = config.ref),
(ref = coerceStringRef(ref, ReactCurrentOwner.current, type)));
for (propName in config)
maybeKey = {};
for (var propName in config)
hasOwnProperty.call(config, propName) &&
"key" !== propName &&
"ref" !== propName &&
(props[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps)
for (propName in ((config = type.defaultProps), config))
void 0 === props[propName] && (props[propName] = config[propName]);
(maybeKey[propName] = config[propName]);
if (!disableDefaultPropsExceptForClasses && type && type.defaultProps) {
config = type.defaultProps;
for (var propName$0 in config)
void 0 === maybeKey[propName$0] &&
(maybeKey[propName$0] = config[propName$0]);
}
return ReactElement(
type,
key,
ref,
void 0,
void 0,
ReactCurrentOwner.current,
props
maybeKey
);
}
function cloneAndReplaceKey(oldElement, newKey) {
Expand Down Expand Up @@ -683,4 +685,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactCurrentDispatcher.current.useTransition();
};
exports.version = "19.0.0-canary-54d35464";
exports.version = "19.0.0-canary-78482d58";
Loading

0 comments on commit 86065f6

Please sign in to comment.