Files
react/packages/shared/ReactPerformanceTrackProperties.js
Sebastian Markbåge e104795f63 [Fiber] Show Diff Render Props in Performance Track in DEV (#33658)
<img width="634" alt="Screenshot 2025-06-27 at 1 13 20 PM"
src="https://github.com/user-attachments/assets/dc8c488b-4a23-453f-918f-36b245364934"
/>

We have to be careful with performance in DEV. It can slow down DX since
these are ran whether you're currently running a performance trace or
not. It can also show up as misleading since these add time to the
"Remaining Effects" entry.

I'm not adding all props to the entries. Instead, I'm only adding the
changed props after diffing and none for initial mount. I'm trying to as
much as possible pick a fast path when possible. I'm also only logging
this for the "render" entries and not the effects. If we did something
for effects, it would be more like checking with dep changed.

This could still have a negative effect on dev performance since we're
now also using the slower `performance.measure` API when there's a diff.
2025-07-02 16:10:07 -04:00

358 lines
12 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';
import {REACT_ELEMENT_TYPE} from './ReactSymbols';
import getComponentNameFromType from './getComponentNameFromType';
const EMPTY_ARRAY = 0;
const COMPLEX_ARRAY = 1;
const PRIMITIVE_ARRAY = 2; // Primitive values only
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
let kind = EMPTY_ARRAY;
for (let i = 0; i < array.length; i++) {
const value = array[i];
if (typeof value === 'object' && value !== null) {
if (
isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string'
) {
// Key value tuple
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
return COMPLEX_ARRAY;
}
kind = ENTRIES_ARRAY;
} else {
return COMPLEX_ARRAY;
}
} else if (typeof value === 'function') {
return COMPLEX_ARRAY;
} else if (typeof value === 'string' && value.length > 50) {
return COMPLEX_ARRAY;
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
return COMPLEX_ARRAY;
} else {
kind = PRIMITIVE_ARRAY;
}
}
return kind;
}
export function addObjectToProperties(
object: Object,
properties: Array<[string, string]>,
indent: number,
prefix: string,
): void {
for (const key in object) {
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
const value = object[key];
addValueToProperties(key, value, properties, indent, prefix);
}
}
}
export function addValueToProperties(
propertyName: string,
value: mixed,
properties: Array<[string, string]>,
indent: number,
prefix: string,
): void {
let desc;
switch (typeof value) {
case 'object':
if (value === null) {
desc = 'null';
break;
} else {
if (value.$$typeof === REACT_ELEMENT_TYPE) {
// JSX
const typeName = getComponentNameFromType(value.type) || '\u2026';
const key = value.key;
const props: any = value.props;
const propsKeys = Object.keys(props);
const propsLength = propsKeys.length;
if (key == null && propsLength === 0) {
desc = '<' + typeName + ' />';
break;
}
if (
indent < 3 ||
(propsLength === 1 && propsKeys[0] === 'children' && key == null)
) {
desc = '<' + typeName + ' \u2026 />';
break;
}
properties.push([
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
'<' + typeName,
]);
if (key !== null) {
addValueToProperties('key', key, properties, indent + 1, prefix);
}
let hasChildren = false;
for (const propKey in props) {
if (propKey === 'children') {
if (
props.children != null &&
(!isArray(props.children) || props.children.length > 0)
) {
hasChildren = true;
}
} else if (
hasOwnProperty.call(props, propKey) &&
propKey[0] !== '_'
) {
addValueToProperties(
propKey,
props[propKey],
properties,
indent + 1,
prefix,
);
}
}
properties.push([
'',
hasChildren ? '>\u2026</' + typeName + '>' : '/>',
]);
return;
}
// $FlowFixMe[method-unbinding]
const objectToString = Object.prototype.toString.call(value);
let objectName = objectToString.slice(8, objectToString.length - 1);
if (objectName === 'Array') {
const array: Array<any> = (value: any);
const kind = getArrayKind(array);
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
desc = JSON.stringify(array);
break;
} else if (kind === ENTRIES_ARRAY) {
properties.push([
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
'',
]);
for (let i = 0; i < array.length; i++) {
const entry = array[i];
addValueToProperties(
entry[0],
entry[1],
properties,
indent + 1,
prefix,
);
}
return;
}
}
if (objectName === 'Promise') {
if (value.status === 'fulfilled') {
// Print the inner value
const idx = properties.length;
addValueToProperties(propertyName, value.value, properties, indent);
if (properties.length > idx) {
// Wrap the value or type in Promise descriptor.
const insertedEntry = properties[idx];
insertedEntry[1] =
'Promise<' + (insertedEntry[1] || 'Object') + '>';
return;
}
} else if (value.status === 'rejected') {
// Print the inner error
const idx = properties.length;
addValueToProperties(
propertyName,
value.reason,
properties,
indent,
);
if (properties.length > idx) {
// Wrap the value or type in Promise descriptor.
const insertedEntry = properties[idx];
insertedEntry[1] = 'Rejected Promise<' + insertedEntry[1] + '>';
return;
}
}
properties.push([
'\xa0\xa0'.repeat(indent) + propertyName,
'Promise',
]);
return;
}
if (objectName === 'Object') {
const proto: any = Object.getPrototypeOf(value);
if (proto && typeof proto.constructor === 'function') {
objectName = proto.constructor.name;
}
}
properties.push([
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
]);
if (indent < 3) {
addObjectToProperties(value, properties, indent + 1, prefix);
}
return;
}
case 'function':
if (value.name === '') {
desc = '() => {}';
} else {
desc = value.name + '() {}';
}
break;
case 'string':
if (value === OMITTED_PROP_ERROR) {
desc = '\u2026'; // ellipsis
} else {
desc = JSON.stringify(value);
}
break;
case 'undefined':
desc = 'undefined';
break;
case 'boolean':
desc = value ? 'true' : 'false';
break;
default:
// eslint-disable-next-line react-internal/safe-string-coercion
desc = String(value);
}
properties.push([prefix + '\xa0\xa0'.repeat(indent) + propertyName, desc]);
}
const REMOVED = '\u2013\xa0';
const ADDED = '+\xa0';
const UNCHANGED = '\u2007\xa0';
export function addObjectDiffToProperties(
prev: Object,
next: Object,
properties: Array<[string, string]>,
indent: number,
): void {
// Note: We diff even non-owned properties here but things that are shared end up just the same.
// If a property is added or removed, we just emit the property name and omit the value it had.
// Mainly for performance. We need to minimize to only relevant information.
for (const key in prev) {
if (!(key in next)) {
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
}
}
for (const key in next) {
if (key in prev) {
const prevValue = prev[key];
const nextValue = next[key];
if (prevValue !== nextValue) {
if (indent === 0 && key === 'children') {
// Omit any change inside the top level children prop since it's expected to change
// with any change to children of the component and their props will be logged
// elsewhere but still mark it as a cause of render.
const line = '\xa0\xa0'.repeat(indent) + key;
properties.push([REMOVED + line, '\u2026'], [ADDED + line, '\u2026']);
continue;
}
if (indent >= 3) {
// Just fallthrough to print the two values if we're deep.
// This will skip nested properties of the objects.
} else if (
typeof prevValue === 'object' &&
typeof nextValue === 'object' &&
prevValue !== null &&
nextValue !== null &&
prevValue.$$typeof === nextValue.$$typeof
) {
if (nextValue.$$typeof === REACT_ELEMENT_TYPE) {
if (
prevValue.type === nextValue.type &&
prevValue.key === nextValue.key
) {
// If the only thing that has changed is the props of a nested element, then
// we omit the props because it is likely to be represented as a diff elsewhere.
const typeName =
getComponentNameFromType(nextValue.type) || '\u2026';
const line = '\xa0\xa0'.repeat(indent) + key;
const desc = '<' + typeName + ' \u2026 />';
properties.push([REMOVED + line, desc], [ADDED + line, desc]);
continue;
}
} else {
// $FlowFixMe[method-unbinding]
const prevKind = Object.prototype.toString.call(prevValue);
// $FlowFixMe[method-unbinding]
const nextKind = Object.prototype.toString.call(nextValue);
if (
prevKind === nextKind &&
(nextKind === '[object Object]' || nextKind === '[object Array]')
) {
// Diff nested object
const entry = [
UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
nextKind === '[object Array]' ? 'Array' : '',
];
properties.push(entry);
const prevLength = properties.length;
addObjectDiffToProperties(
prevValue,
nextValue,
properties,
indent + 1,
);
if (prevLength === properties.length) {
// Nothing notably changed inside the nested object. So this is only a change in reference
// equality. Let's note it.
entry[1] =
'Referentially unequal but deeply equal objects. Consider memoization.';
}
continue;
}
}
} else if (
typeof prevValue === 'function' &&
typeof nextValue === 'function' &&
prevValue.name === nextValue.name &&
prevValue.length === nextValue.length
) {
// $FlowFixMe[method-unbinding]
const prevSrc = Function.prototype.toString.call(prevValue);
// $FlowFixMe[method-unbinding]
const nextSrc = Function.prototype.toString.call(nextValue);
if (prevSrc === nextSrc) {
// This looks like it might be the same function but different closures.
let desc;
if (nextValue.name === '') {
desc = '() => {}';
} else {
desc = nextValue.name + '() {}';
}
properties.push([
UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
desc +
' Referentially unequal function closure. Consider memoization.',
]);
continue;
}
}
// Otherwise, emit the change in property and the values.
addValueToProperties(key, prevValue, properties, indent, REMOVED);
addValueToProperties(key, nextValue, properties, indent, ADDED);
}
} else {
properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
}
}
}