[DevTools] Recommend React Performance tracks if supported when Timeline profiler is not supported (#34684)
Sebastian "Sebbie" Silbermann committed
Oct 2, 2025 at 18:33 UTC
bc828bf6e3b762747134099f338a2b57df586f43
6 files changed
+136
-17
packages/react-devtools-shared/src/backend/fiber/renderer.js
+5
@@ -78,6 +78,7 @@ import {
78
__DEBUG__,
79
PROFILING_FLAG_BASIC_SUPPORT,
80
PROFILING_FLAG_TIMELINE_SUPPORT,
81
+ PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
82
TREE_OPERATION_ADD,
83
TREE_OPERATION_REMOVE,
84
TREE_OPERATION_REORDER_CHILDREN,
@@ -1074,6 +1075,7 @@ export function attach(
1075
const supportsTogglingSuspense =
1076
typeof setSuspenseHandler === 'function' &&
1077
typeof scheduleUpdate === 'function';
1078
+ const supportsPerformanceTracks = gte(version, '19.2.0');
1079
1080
if (typeof scheduleRefresh === 'function') {
1081
// When Fast Refresh updates a component, the frontend may need to purge cached information.
@@ -2401,6 +2403,9 @@ export function attach(
2403
if (typeof injectProfilingHooks === 'function') {
2404
profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
2405
}
2406
+ if (supportsPerformanceTracks) {
2407
+ profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;
2408
+ }
2409
}
2410
2411
// Set supportsStrictMode to false for production renderer builds
packages/react-devtools-shared/src/constants.js
+3
-2
@@ -30,8 +30,9 @@ export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
30
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
31
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
32
33
-export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
34
-export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
33
+export const PROFILING_FLAG_BASIC_SUPPORT /*. */ = 0b001;
34
+export const PROFILING_FLAG_TIMELINE_SUPPORT /* */ = 0b010;
35
+export const PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT /* */ = 0b100;
36
37
export const UNKNOWN_SUSPENDERS_NONE: UnknownSuspendersReason = 0; // If we had at least one debugInfo, then that might have been the reason.
38
export const UNKNOWN_SUSPENDERS_REASON_PRODUCTION: UnknownSuspendersReason = 1; // We're running in prod. That might be why we had unknown suspenders.
packages/react-devtools-shared/src/devtools/store.js
+47
-7
@@ -13,6 +13,7 @@ import {inspect} from 'util';
13
import {
14
PROFILING_FLAG_BASIC_SUPPORT,
15
PROFILING_FLAG_TIMELINE_SUPPORT,
16
+ PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
17
TREE_OPERATION_ADD,
18
TREE_OPERATION_REMOVE,
19
TREE_OPERATION_REMOVE_ROOT,
@@ -86,12 +87,17 @@ export type Config = {
87
supportsTraceUpdates?: boolean,
88
};
89
90
+const ADVANCED_PROFILING_NONE = 0;
91
+const ADVANCED_PROFILING_TIMELINE = 1;
92
+const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2;
93
+type AdvancedProfiling = 0 | 1 | 2;
94
+
95
export type Capabilities = {
96
supportsBasicProfiling: boolean,
97
hasOwnerMetadata: boolean,
98
supportsStrictMode: boolean,
99
supportsTogglingSuspense: boolean,
94
- supportsTimeline: boolean,
100
+ supportsAdvancedProfiling: AdvancedProfiling,
101
};
102
103
/**
@@ -112,6 +118,7 @@ export default class Store extends EventEmitter<{
118
roots: [],
119
rootSupportsBasicProfiling: [],
120
rootSupportsTimelineProfiling: [],
121
+ rootSupportsPerformanceTracks: [],
122
suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
123
supportsNativeStyleEditor: [],
124
supportsReloadAndProfile: [],
@@ -195,6 +202,7 @@ export default class Store extends EventEmitter<{
202
// These options default to false but may be updated as roots are added and removed.
203
_rootSupportsBasicProfiling: boolean = false;
204
_rootSupportsTimelineProfiling: boolean = false;
205
+ _rootSupportsPerformanceTracks: boolean = false;
206
207
_bridgeProtocol: BridgeProtocol | null = null;
208
_unsupportedBridgeProtocolDetected: boolean = false;
@@ -474,6 +482,11 @@ export default class Store extends EventEmitter<{
482
return this._rootSupportsTimelineProfiling;
483
}
484
485
+ // At least one of the currently mounted roots support performance tracks.
486
+ get rootSupportsPerformanceTracks(): boolean {
487
+ return this._rootSupportsPerformanceTracks;
488
+ }
489
+
490
get supportsInspectMatchingDOMElement(): boolean {
491
return this._supportsInspectMatchingDOMElement;
492
}
@@ -1161,11 +1174,20 @@ export default class Store extends EventEmitter<{
1174
const isStrictModeCompliant = operations[i] > 0;
1175
i++;
1176
1177
+ const profilerFlags = operations[i++];
1178
const supportsBasicProfiling =
1165
- (operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
1179
+ (profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
1180
const supportsTimeline =
1167
- (operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
1168
- i++;
1181
+ (profilerFlags & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
1182
+ const supportsPerformanceTracks =
1183
+ (profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;
1184
+ let supportsAdvancedProfiling: AdvancedProfiling =
1185
+ ADVANCED_PROFILING_NONE;
1186
+ if (supportsPerformanceTracks) {
1187
+ supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS;
1188
+ } else if (supportsTimeline) {
1189
+ supportsAdvancedProfiling = ADVANCED_PROFILING_TIMELINE;
1190
+ }
1191
1192
let supportsStrictMode = false;
1193
let hasOwnerMetadata = false;
@@ -1194,7 +1216,7 @@ export default class Store extends EventEmitter<{
1216
hasOwnerMetadata,
1217
supportsStrictMode,
1218
supportsTogglingSuspense,
1197
- supportsTimeline,
1219
+ supportsAdvancedProfiling,
1220
});
1221
1222
// Not all roots support StrictMode;
@@ -1842,21 +1864,33 @@ export default class Store extends EventEmitter<{
1864
const prevRootSupportsProfiling = this._rootSupportsBasicProfiling;
1865
const prevRootSupportsTimelineProfiling =
1866
this._rootSupportsTimelineProfiling;
1867
+ const prevRootSupportsPerformanceTracks =
1868
+ this._rootSupportsPerformanceTracks;
1869
1870
this._hasOwnerMetadata = false;
1871
this._rootSupportsBasicProfiling = false;
1872
this._rootSupportsTimelineProfiling = false;
1873
+ this._rootSupportsPerformanceTracks = false;
1874
this._rootIDToCapabilities.forEach(
1850
- ({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => {
1875
+ ({
1876
+ supportsBasicProfiling,
1877
+ hasOwnerMetadata,
1878
+ supportsAdvancedProfiling,
1879
+ }) => {
1880
if (supportsBasicProfiling) {
1881
this._rootSupportsBasicProfiling = true;
1882
}
1883
if (hasOwnerMetadata) {
1884
this._hasOwnerMetadata = true;
1885
}
1857
- if (supportsTimeline) {
1886
+ if (supportsAdvancedProfiling === ADVANCED_PROFILING_TIMELINE) {
1887
this._rootSupportsTimelineProfiling = true;
1888
}
1889
+ if (
1890
+ supportsAdvancedProfiling === ADVANCED_PROFILING_PERFORMANCE_TRACKS
1891
+ ) {
1892
+ this._rootSupportsPerformanceTracks = true;
1893
+ }
1894
},
1895
);
1896
@@ -1872,6 +1906,12 @@ export default class Store extends EventEmitter<{
1906
) {
1907
this.emit('rootSupportsTimelineProfiling');
1908
}
1909
+ if (
1910
+ this._rootSupportsPerformanceTracks !==
1911
+ prevRootSupportsPerformanceTracks
1912
+ ) {
1913
+ this.emit('rootSupportsPerformanceTracks');
1914
+ }
1915
}
1916
1917
if (hasSuspenseTreeChanged) {
packages/react-devtools-timeline/src/Timeline.js
+13
-3
@@ -33,8 +33,14 @@ import {TimelineSearchContextController} from './TimelineSearchContext';
33
import styles from './Timeline.css';
34
35
export function Timeline(_: {}): React.Node {
36
- const {file, inMemoryTimelineData, isTimelineSupported, setFile, viewState} =
37
- useContext(TimelineContext);
36
+ const {
37
+ file,
38
+ inMemoryTimelineData,
39
+ isPerformanceTracksSupported,
40
+ isTimelineSupported,
41
+ setFile,
42
+ viewState,
43
+ } = useContext(TimelineContext);
44
const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
45
46
const ref = useRef(null);
@@ -95,7 +101,11 @@ export function Timeline(_: {}): React.Node {
101
} else if (isTimelineSupported) {
102
content = <NoProfilingData />;
103
} else {
98
- content = <TimelineNotSupported />;
104
+ content = (
105
+ <TimelineNotSupported
106
+ isPerformanceTracksSupported={isPerformanceTracksSupported}
107
+ />
108
+ );
109
}
110
111
return (
packages/react-devtools-timeline/src/TimelineContext.js
+15
@@ -31,6 +31,7 @@ import type {
31
export type Context = {
32
file: File | null,
33
inMemoryTimelineData: Array<TimelineData> | null,
34
+ isPerformanceTracksSupported: boolean,
35
isTimelineSupported: boolean,
36
searchInputContainerRef: RefObject,
37
setFile: (file: File | null) => void,
@@ -66,6 +67,18 @@ function TimelineContextController({children}: Props): React.Node {
67
},
68
);
69
70
+ const isPerformanceTracksSupported = useSyncExternalStore<boolean>(
71
+ function subscribe(callback) {
72
+ store.addListener('rootSupportsPerformanceTracks', callback);
73
+ return function unsubscribe() {
74
+ store.removeListener('rootSupportsPerformanceTracks', callback);
75
+ };
76
+ },
77
+ function getState() {
78
+ return store.rootSupportsPerformanceTracks;
79
+ },
80
+ );
81
+
82
const inMemoryTimelineData = useSyncExternalStore<Array<TimelineData> | null>(
83
function subscribe(callback) {
84
store.profilerStore.addListener('isProcessingData', callback);
@@ -135,6 +148,7 @@ function TimelineContextController({children}: Props): React.Node {
148
() => ({
149
file,
150
inMemoryTimelineData,
151
+ isPerformanceTracksSupported,
152
isTimelineSupported,
153
searchInputContainerRef,
154
setFile,
@@ -145,6 +159,7 @@ function TimelineContextController({children}: Props): React.Node {
159
[
160
file,
161
inMemoryTimelineData,
162
+ isPerformanceTracksSupported,
163
isTimelineSupported,
164
setFile,
165
viewState,
packages/react-devtools-timeline/src/TimelineNotSupported.js
+53
-5
@@ -12,16 +12,48 @@ import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
12
13
import styles from './TimelineNotSupported.css';
14
15
-export default function TimelineNotSupported(): React.Node {
15
+type Props = {
16
+ isPerformanceTracksSupported: boolean,
17
+};
18
+
19
+function PerformanceTracksSupported() {
20
return (
17
- <div className={styles.Column}>
18
- <div className={styles.Header}>Timeline profiling not supported.</div>
21
+ <>
22
<p className={styles.Paragraph}>
23
<span>
21
- Timeline profiler requires a development or profiling build of{' '}
22
- <code className={styles.Code}>react-dom@^18</code>.
24
+ Please use{' '}
25
+ <a
26
+ className={styles.Link}
27
+ href="https://react.dev/reference/dev-tools/react-performance-tracks"
28
+ rel="noopener noreferrer"
29
+ target="_blank">
30
+ React Performance tracks
31
+ </a>{' '}
32
+ instead of the Timeline profiler.
33
</span>
34
</p>
35
+ </>
36
+ );
37
+}
38
+
39
+function UnknownUnsupportedReason() {
40
+ return (
41
+ <>
42
+ <p className={styles.Paragraph}>
43
+ Timeline profiler requires a development or profiling build of{' '}
44
+ <code className={styles.Code}>react-dom@{'>='}18</code>.
45
+ </p>
46
+ <p className={styles.Paragraph}>
47
+ In React 19.2 and above{' '}
48
+ <a
49
+ className={styles.Link}
50
+ href="https://react.dev/reference/dev-tools/react-performance-tracks"
51
+ rel="noopener noreferrer"
52
+ target="_blank">
53
+ React Performance tracks
54
+ </a>{' '}
55
+ can be used instead.
56
+ </p>
57
<div className={styles.LearnMoreRow}>
58
Click{' '}
59
<a
@@ -33,6 +65,22 @@ export default function TimelineNotSupported(): React.Node {
65
</a>{' '}
66
to learn more about profiling.
67
</div>
68
+ </>
69
+ );
70
+}
71
+
72
+export default function TimelineNotSupported({
73
+ isPerformanceTracksSupported,
74
+}: Props): React.Node {
75
+ return (
76
+ <div className={styles.Column}>
77
+ <div className={styles.Header}>Timeline profiling not supported.</div>
78
+
79
+ {isPerformanceTracksSupported ? (
80
+ <PerformanceTracksSupported />
81
+ ) : (
82
+ <UnknownUnsupportedReason />
83
+ )}
84
85
{isInternalFacebookBuild && (
86
<div className={styles.MetaGKRow}>