diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js
index 08987750eb210..49bfc9e05135c 100644
--- a/fixtures/flight/src/App.js
+++ b/fixtures/flight/src/App.js
@@ -20,12 +20,26 @@ import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';
const promisedText = new Promise(resolve =>
- setTimeout(() => resolve('deferred text'), 100)
+ setTimeout(() => resolve('deferred text'), 50)
);
+function Foo({children}) {
+ return
{children}
;
+}
+
+function Bar({children}) {
+ return {children}
;
+}
+
+async function ServerComponent() {
+ await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50));
+}
+
export default async function App({prerender}) {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();
+
+ const dedupedChild = ;
return (
@@ -66,6 +80,8 @@ export default async function App({prerender}) {
+ {dedupedChild}
+ {dedupedChild}
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 19b1285ad9732..4d5204ab00220 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -71,6 +71,8 @@ import {createBoundServerReference} from './ReactFlightReplyClient';
import {readTemporaryReference} from './ReactFlightTemporaryReferences';
+import {logComponentRender} from './ReactFlightPerformanceTrack';
+
import {
REACT_LAZY_TYPE,
REACT_ELEMENT_TYPE,
@@ -124,6 +126,10 @@ export type JSONValue =
| {+[key: string]: JSONValue}
| $ReadOnlyArray;
+type ProfilingResult = {
+ endTime: number,
+};
+
const ROW_ID = 0;
const ROW_TAG = 1;
const ROW_LENGTH = 2;
@@ -144,7 +150,8 @@ type PendingChunk = {
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type BlockedChunk = {
@@ -152,7 +159,8 @@ type BlockedChunk = {
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type ResolvedModelChunk = {
@@ -160,7 +168,8 @@ type ResolvedModelChunk = {
value: UninitializedModel,
reason: null,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type ResolvedModuleChunk = {
@@ -168,7 +177,8 @@ type ResolvedModuleChunk = {
value: ClientReference,
reason: null,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedChunk = {
@@ -176,7 +186,8 @@ type InitializedChunk = {
value: T,
reason: null | FlightStreamController,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedStreamChunk<
@@ -186,7 +197,8 @@ type InitializedStreamChunk<
value: T,
reason: FlightStreamController,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
};
type ErroredChunk = {
@@ -194,7 +206,8 @@ type ErroredChunk = {
value: null,
reason: mixed,
_response: Response,
- _debugInfo?: null | ReactDebugInfo,
+ _children: Array> | ProfilingResult, // Profiling-only
+ _debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type SomeChunk =
@@ -216,6 +229,9 @@ function ReactPromise(
this.value = value;
this.reason = reason;
this._response = response;
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ this._children = [];
+ }
if (__DEV__) {
this._debugInfo = null;
}
@@ -548,9 +564,11 @@ type InitializationHandler = {
errored: boolean,
};
let initializingHandler: null | InitializationHandler = null;
+let initializingChunk: null | BlockedChunk = null;
function initializeModelChunk(chunk: ResolvedModelChunk): void {
const prevHandler = initializingHandler;
+ const prevChunk = initializingChunk;
initializingHandler = null;
const resolvedModel = chunk.value;
@@ -563,6 +581,10 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void {
cyclicChunk.value = null;
cyclicChunk.reason = null;
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ initializingChunk = cyclicChunk;
+ }
+
try {
const value: T = parseModel(chunk._response, resolvedModel);
// Invoke any listeners added while resolving this model. I.e. cyclic
@@ -595,6 +617,9 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void {
erroredChunk.reason = error;
} finally {
initializingHandler = prevHandler;
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ initializingChunk = prevChunk;
+ }
}
}
@@ -622,6 +647,9 @@ export function reportGlobalError(response: Response, error: Error): void {
triggerErrorOnChunk(chunk, error);
}
});
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ flushComponentPerformance(getChunk(response, 0));
+ }
}
function nullRefGetter() {
@@ -1210,6 +1238,11 @@ function getOutlinedModel(
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ if (initializingChunk !== null && isArray(initializingChunk._children)) {
+ initializingChunk._children.push(chunk);
+ }
+ }
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
@@ -1359,6 +1392,14 @@ function parseModelString(
// Lazy node
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ if (
+ initializingChunk !== null &&
+ isArray(initializingChunk._children)
+ ) {
+ initializingChunk._children.push(chunk);
+ }
+ }
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
@@ -1371,6 +1412,14 @@ function parseModelString(
}
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
+ if (enableProfilerTimer && enableComponentPerformanceTrack) {
+ if (
+ initializingChunk !== null &&
+ isArray(initializingChunk._children)
+ ) {
+ initializingChunk._children.push(chunk);
+ }
+ }
return chunk;
}
case 'S': {
@@ -2704,6 +2753,67 @@ function resolveTypedArray(
resolveBuffer(response, id, view);
}
+function flushComponentPerformance(root: SomeChunk): number {
+ if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
+ return 0;
+ }
+ // Write performance.measure() entries for Server Components in tree order.
+ // This must be done at the end to collect the end time from the whole tree.
+ if (!isArray(root._children)) {
+ // We have already written this chunk. If this was a cycle, then this will
+ // be -Infinity and it won't contribute to the parent end time.
+ // If this was already emitted by another sibling then we reused the same
+ // chunk in two places. We should extend the current end time as if it was
+ // rendered as part of this tree.
+ const previousResult: ProfilingResult = root._children;
+ return previousResult.endTime;
+ }
+ const children = root._children;
+ if (root.status === RESOLVED_MODEL) {
+ // If the model is not initialized by now, do that now so we can find its
+ // children. This part is a little sketchy since it significantly changes
+ // the performance characteristics of the app by profiling.
+ initializeModelChunk(root);
+ }
+ const result: ProfilingResult = {endTime: -Infinity};
+ root._children = result;
+ let childrenEndTime = -Infinity;
+ for (let i = 0; i < children.length; i++) {
+ const childEndTime = flushComponentPerformance(children[i]);
+ if (childEndTime > childrenEndTime) {
+ childrenEndTime = childEndTime;
+ }
+ }
+ const debugInfo = root._debugInfo;
+ if (debugInfo) {
+ let endTime = 0;
+ for (let i = debugInfo.length - 1; i >= 0; i--) {
+ const info = debugInfo[i];
+ if (typeof info.time === 'number') {
+ endTime = info.time;
+ if (endTime > childrenEndTime) {
+ childrenEndTime = endTime;
+ }
+ }
+ if (typeof info.name === 'string' && i > 0) {
+ // $FlowFixMe: Refined.
+ const componentInfo: ReactComponentInfo = info;
+ const startTimeInfo = debugInfo[i - 1];
+ if (typeof startTimeInfo.time === 'number') {
+ const startTime = startTimeInfo.time;
+ logComponentRender(
+ componentInfo,
+ startTime,
+ endTime,
+ childrenEndTime,
+ );
+ }
+ }
+ }
+ }
+ return (result.endTime = childrenEndTime);
+}
+
function processFullBinaryRow(
response: Response,
id: number,
diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js
new file mode 100644
index 0000000000000..f1e7b30280722
--- /dev/null
+++ b/packages/react-client/src/ReactFlightPerformanceTrack.js
@@ -0,0 +1,56 @@
+/**
+ * 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 type {ReactComponentInfo} from 'shared/ReactTypes';
+
+import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
+
+const supportsUserTiming =
+ enableProfilerTimer &&
+ typeof performance !== 'undefined' &&
+ // $FlowFixMe[method-unbinding]
+ typeof performance.measure === 'function';
+
+const COMPONENTS_TRACK = 'Server Components ⚛';
+
+// Reused to avoid thrashing the GC.
+const reusableComponentDevToolDetails = {
+ color: 'primary',
+ track: COMPONENTS_TRACK,
+};
+const reusableComponentOptions = {
+ start: -0,
+ end: -0,
+ detail: {
+ devtools: reusableComponentDevToolDetails,
+ },
+};
+
+export function logComponentRender(
+ componentInfo: ReactComponentInfo,
+ startTime: number,
+ endTime: number,
+ childrenEndTime: number,
+): void {
+ if (supportsUserTiming && childrenEndTime >= 0) {
+ const name = componentInfo.name;
+ const selfTime = endTime - startTime;
+ reusableComponentDevToolDetails.color =
+ selfTime < 0.5
+ ? 'primary-light'
+ : selfTime < 50
+ ? 'primary'
+ : selfTime < 500
+ ? 'primary-dark'
+ : 'error';
+ reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
+ reusableComponentOptions.end = childrenEndTime;
+ performance.measure(name, reusableComponentOptions);
+ }
+}