-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
2 changed files
with
101 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -314,11 +314,6 @@ function ReactElement(type, key, _ref, self, source, owner, props) { | |
* @param {string} key | ||
*/ | ||
export function jsxProd(type, config, maybeKey) { | ||
let propName; | ||
|
||
// Reserved names are extracted | ||
const props = {}; | ||
|
||
let key = null; | ||
let ref = null; | ||
|
||
|
@@ -351,22 +346,39 @@ export function jsxProd(type, config, maybeKey) { | |
} | ||
} | ||
|
||
// 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' && | ||
(enableRefAsProp || propName !== 'ref') | ||
) { | ||
if (enableRefAsProp && !disableStringRefs && propName === 'ref') { | ||
props.ref = coerceStringRef( | ||
config[propName], | ||
ReactCurrentOwner.current, | ||
type, | ||
); | ||
} else { | ||
props[propName] = config[propName]; | ||
let props; | ||
if (enableRefAsProp && disableStringRefs && !('key' in config)) { | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
// If key was not spread in, we can reuse the original props object. This | ||
// only works for `jsx`, not `createElement`, because `jsx` is a compiler | ||
// target and the compiler always passes a new object. For `createElement`, | ||
// we can't assume a new object is passed every time because it can be | ||
// called manually. | ||
// | ||
// Spreading key is a warning in dev. 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. | ||
props = config; | ||
This comment has been minimized.
Sorry, something went wrong. |
||
} else { | ||
// 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 (const propName in config) { | ||
if ( | ||
hasOwnProperty.call(config, propName) && | ||
// Skip over reserved prop names | ||
propName !== 'key' && | ||
(enableRefAsProp || propName !== 'ref') | ||
) { | ||
if (enableRefAsProp && !disableStringRefs && propName === 'ref') { | ||
props.ref = coerceStringRef( | ||
config[propName], | ||
ReactCurrentOwner.current, | ||
type, | ||
); | ||
} else { | ||
props[propName] = config[propName]; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -375,7 +387,7 @@ export function jsxProd(type, config, maybeKey) { | |
// Resolve default props | ||
if (type && type.defaultProps) { | ||
const defaultProps = type.defaultProps; | ||
for (propName in defaultProps) { | ||
for (const propName in defaultProps) { | ||
if (props[propName] === undefined) { | ||
props[propName] = defaultProps[propName]; | ||
This comment has been minimized.
Sorry, something went wrong.
yungsters
Contributor
|
||
} | ||
|
@@ -538,11 +550,6 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { | |
} | ||
} | ||
|
||
let propName; | ||
|
||
// Reserved names are extracted | ||
const props = {}; | ||
|
||
let key = null; | ||
let ref = null; | ||
|
||
|
@@ -578,22 +585,39 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, 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' && | ||
(enableRefAsProp || propName !== 'ref') | ||
) { | ||
if (enableRefAsProp && !disableStringRefs && propName === 'ref') { | ||
props.ref = coerceStringRef( | ||
config[propName], | ||
ReactCurrentOwner.current, | ||
type, | ||
); | ||
} else { | ||
props[propName] = config[propName]; | ||
let props; | ||
if (enableRefAsProp && disableStringRefs && !('key' in config)) { | ||
// If key was not spread in, we can reuse the original props object. This | ||
// only works for `jsx`, not `createElement`, because `jsx` is a compiler | ||
// target and the compiler always passes a new object. For `createElement`, | ||
// we can't assume a new object is passed every time because it can be | ||
// called manually. | ||
// | ||
// Spreading key is a warning in dev. 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. | ||
props = config; | ||
} else { | ||
// 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 (const propName in config) { | ||
if ( | ||
hasOwnProperty.call(config, propName) && | ||
// Skip over reserved prop names | ||
propName !== 'key' && | ||
(enableRefAsProp || propName !== 'ref') | ||
) { | ||
if (enableRefAsProp && !disableStringRefs && propName === 'ref') { | ||
props.ref = coerceStringRef( | ||
config[propName], | ||
ReactCurrentOwner.current, | ||
type, | ||
); | ||
} else { | ||
props[propName] = config[propName]; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -602,7 +626,7 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { | |
// Resolve default props | ||
if (type && type.defaultProps) { | ||
const defaultProps = type.defaultProps; | ||
for (propName in defaultProps) { | ||
for (const propName in defaultProps) { | ||
if (props[propName] === undefined) { | ||
props[propName] = defaultProps[propName]; | ||
} | ||
|
@acdlite – Question about the
enableRefAsProp
anddisableStringRefs
checks here. Ifref
isn't present, could we not also allow "good call sites" to benefit from the fast path?For example, something like: