@samitouri / QOS-React / commits / bc828bf6e3

[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}>