[DevTools] Remove Timeline profiler tab, suggest React Performance tracks (#37186)
Cleaning up the Timeline profiler in the next commit on top of this one. If user is debugging React 19.2+, we will show a suggestion to record a trace on Performance panel. Otherwise, we will suggest to upgrade to React 19.2 to unlock Performance tracks. <img width="751" height="832" alt="Screenshot 2026-08-03 at 14 57 43" src="https://github.com/user-attachments/assets/153e712b-8f7c-4ec5-87f8-b01cf1180aae" />
Ruslan Lesiutin committed
Aug 3, 2026 at 16:32 UTC
c7cab6664db1b18c78044dde5317722acbeac1cd
7 files changed
+101
-116
packages/react-devtools-shared/src/devtools/views/Icon.js
-9
@@ -21,7 +21,6 @@ export type IconType =
21
| 'flame-chart'
22
| 'profiler'
23
| 'ranked-chart'
24
- | 'timeline'
24
| 'search'
25
| 'settings'
26
| 'store-as-global-variable'
@@ -75,9 +74,6 @@ export default function Icon({
74
case 'ranked-chart':
75
pathData = PATH_RANKED_CHART;
76
break;
78
- case 'timeline':
79
- pathData = PATH_SCHEDULING_PROFILER;
80
- break;
77
case 'search':
78
pathData = PATH_SEARCH;
79
break;
@@ -159,11 +155,6 @@ const PATH_FLAME_CHART = `
155
156
const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z';
157
162
-const PATH_SCHEDULING_PROFILER = `
163
- M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0
164
- 16H5V9h14v10zm0-12H5V5h14v2zM7 11h5v5H7z
165
-`;
166
-
158
const PATH_SEARCH = `
159
M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91
160
16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99
packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js
+1
-9
@@ -13,31 +13,23 @@ import {ProfilerContext} from './ProfilerContext';
13
import Button from '../Button';
14
import ButtonIcon from '../ButtonIcon';
15
import {StoreContext} from '../context';
16
-import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
16
17
export default function ClearProfilingDataButton(): React.Node {
18
const store = useContext(StoreContext);
19
const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
21
- const {file, setFile} = useContext(TimelineContext);
20
const {profilerStore} = store;
21
22
const doesHaveInMemoryData = didRecordCommits;
25
- const doesHaveUserTimingData = file !== null;
23
24
const clear = () => {
25
if (doesHaveInMemoryData) {
26
profilerStore.clear();
27
}
31
- if (doesHaveUserTimingData) {
32
- setFile(null);
33
- }
28
};
29
30
return (
31
<Button
38
- disabled={
39
- isProfiling || !(doesHaveInMemoryData || doesHaveUserTimingData)
40
- }
32
+ disabled={isProfiling || !doesHaveInMemoryData}
33
onClick={clear}
34
title="Clear profiling data">
35
<ButtonIcon type="clear" />
packages/react-devtools-shared/src/devtools/views/Profiler/NoProfilingData.js
+40
-1
@@ -8,13 +8,38 @@
8
*/
9
10
import * as React from 'react';
11
-import {useContext} from 'react';
11
+import {useContext, useSyncExternalStore} from 'react';
12
import {ProfilerContext} from './ProfilerContext';
13
+import {StoreContext} from '../context';
14
15
import styles from './Profiler.css';
16
17
export default function NoProfilingData(): React.Node {
18
const {startProfiling} = useContext(ProfilerContext);
19
+ const store = useContext(StoreContext);
20
+
21
+ // React only started emitting Performance tracks in 19.2.
22
+ const supportsPerformanceTracks = useSyncExternalStore<boolean>(
23
+ function subscribe(callback) {
24
+ store.addListener('rootSupportsPerformanceTracks', callback);
25
+ return function unsubscribe() {
26
+ store.removeListener('rootSupportsPerformanceTracks', callback);
27
+ };
28
+ },
29
+ function getState() {
30
+ return store.rootSupportsPerformanceTracks;
31
+ },
32
+ );
33
+
34
+ const performanceTracksLink = (
35
+ <a
36
+ className={styles.DescriptionLink}
37
+ href="https://react.dev/reference/dev-tools/react-performance-tracks"
38
+ rel="noopener noreferrer"
39
+ target="_blank">
40
+ Performance tracks
41
+ </a>
42
+ );
43
44
return (
45
<div className={styles.Column}>
@@ -36,6 +61,20 @@ export default function NoProfilingData(): React.Node {
61
type="button">
62
Start recording
63
</button>
64
+ <div className={styles.PerformanceTracksCard}>
65
+ To profile scheduling and rendering on a timeline,{' '}
66
+ {supportsPerformanceTracks ? (
67
+ <>
68
+ record a profile in the Performance panel — React adds its own{' '}
69
+ {performanceTracksLink} there.
70
+ </>
71
+ ) : (
72
+ <>
73
+ upgrade to React 19.2 or newer, which adds {performanceTracksLink}{' '}
74
+ to the Performance panel.
75
+ </>
76
+ )}
77
+ </div>
78
</div>
79
);
80
}
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css
+11
-7
@@ -68,7 +68,7 @@
68
}
69
70
.Description {
71
- max-width: 26rem;
71
+ max-width: 100%;
72
text-align: center;
73
color: var(--color-dim);
74
}
@@ -78,6 +78,16 @@
78
text-decoration: underline;
79
}
80
81
+.PerformanceTracksCard {
82
+ margin-top: 1.5rem;
83
+ max-width: 80%;
84
+ padding: 0.625rem 0.75rem;
85
+ background: var(--color-background-hover);
86
+ /* Squarer than the CTA's pill, so the two don't read as the same control. */
87
+ border-radius: 0.25rem;
88
+ text-align: center;
89
+}
90
+
91
.CTAButton {
92
margin-top: 0.5rem;
93
padding: 0.375rem 1rem;
@@ -150,9 +160,3 @@
160
.Link {
161
color: var(--color-button);
162
}
153
-
154
-.TimelineSearchInputContainer {
155
- flex: 1 1;
156
- display: flex;
157
- align-items: center;
158
-}
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+4
-53
@@ -16,8 +16,6 @@ import ClearProfilingDataButton from './ClearProfilingDataButton';
16
import CommitFlamegraph from './CommitFlamegraph';
17
import CommitRanked from './CommitRanked';
18
import RootSelector from './RootSelector';
19
-import {Timeline} from 'react-devtools-timeline/src/Timeline';
20
-import SidebarEventInfo from './SidebarEventInfo';
19
import RecordToggle from './RecordToggle';
20
import ReloadAndProfileButton from './ReloadAndProfileButton';
21
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
@@ -32,8 +30,6 @@ import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/Set
30
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
31
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
32
import portaledContent from '../portaledContent';
35
-import {StoreContext} from '../context';
36
-import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
33
34
import styles from './Profiler.css';
35
@@ -58,13 +54,6 @@ function Profiler(_: {}) {
54
selectNextCommitIndex,
55
} = useContext(ProfilerContext);
56
61
- const {file: timelineTraceEventData, searchInputContainerRef} =
62
- useContext(TimelineContext);
63
-
64
- const {supportsTimeline} = useContext(StoreContext);
65
-
66
- const isLegacyProfilerSelected = selectedTabID !== 'timeline';
67
-
57
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
58
const correctModifier = isMac ? event.metaKey : event.ctrlKey;
59
// Cmd+E to start/stop profiler recording
@@ -76,11 +65,7 @@ function Profiler(_: {}) {
65
}
66
event.preventDefault();
67
event.stopPropagation();
79
- } else if (
80
- isLegacyProfilerSelected &&
81
- didRecordCommits &&
82
- selectedCommitIndex !== null
83
- ) {
68
+ } else if (didRecordCommits && selectedCommitIndex !== null) {
69
// Cmd+Left/Right (Mac) or Ctrl+Left/Right (Windows/Linux) to navigate commits
70
if (
71
correctModifier &&
@@ -110,7 +95,7 @@ function Profiler(_: {}) {
95
}, []);
96
97
let view = null;
113
- if (didRecordCommits || selectedTabID === 'timeline') {
98
+ if (didRecordCommits) {
99
switch (selectedTabID) {
100
case 'flame-chart':
101
view = <CommitFlamegraph />;
@@ -118,9 +103,6 @@ function Profiler(_: {}) {
103
case 'ranked-chart':
104
view = <CommitRanked />;
105
break;
121
- case 'timeline':
122
- view = <Timeline />;
123
- break;
106
default:
107
break;
108
}
@@ -128,8 +110,6 @@ function Profiler(_: {}) {
110
view = <RecordingInProgress />;
111
} else if (isProcessingData) {
112
view = <ProcessingData />;
131
- } else if (timelineTraceEventData) {
132
- view = <OnlyTimelineData />;
113
} else if (supportsProfiling) {
114
view = <NoProfilingData />;
115
} else {
@@ -155,9 +135,6 @@ function Profiler(_: {}) {
135
}
136
}
137
break;
158
- case 'timeline':
159
- sidebar = <SidebarEventInfo />;
160
- break;
138
default:
139
break;
140
}
@@ -177,19 +154,13 @@ function Profiler(_: {}) {
154
currentTab={selectedTabID}
155
id="Profiler"
156
selectTab={selectTab}
180
- tabs={supportsTimeline ? tabsWithTimeline : tabs}
157
+ tabs={tabs}
158
type="profiler"
159
/>
160
<RootSelector />
161
<div className={styles.Spacer} />
185
- {!isLegacyProfilerSelected && (
186
- <div
187
- ref={searchInputContainerRef}
188
- className={styles.TimelineSearchInputContainer}
189
- />
190
- )}
162
<SettingsModalContextToggle />
192
- {isLegacyProfilerSelected && didRecordCommits && (
163
+ {didRecordCommits && (
164
<Fragment>
165
<div className={styles.VRule} />
166
<SnapshotSelector />
@@ -208,15 +179,6 @@ function Profiler(_: {}) {
179
);
180
}
181
211
-const OnlyTimelineData = () => (
212
- <div className={styles.Column}>
213
- <div className={styles.Header}>Timeline only</div>
214
- <div className={styles.Row}>
215
- The current profile contains only Timeline data.
216
- </div>
217
- </div>
218
-);
219
-
182
const tabs = [
183
{
184
id: 'flame-chart',
@@ -232,15 +194,4 @@ const tabs = [
194
},
195
];
196
235
-const tabsWithTimeline = [
236
- ...tabs,
237
- null, // Divider/separator
238
- {
239
- id: 'timeline',
240
- icon: 'timeline',
241
- label: 'Timeline',
242
- title: 'Timeline',
243
- },
244
-];
245
-
197
export default portaledContent(Profiler) as component();
packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
+7
-2
@@ -29,7 +29,7 @@ import {useCommitFilteringAndNavigation} from './useCommitFilteringAndNavigation
29
30
import type {CommitDataFrontend, ProfilingDataFrontend} from './types';
31
32
-export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';
32
+export type TabID = 'flame-chart' | 'ranked-chart';
33
34
export type Context = {
35
// Which tab is selected in the Profiler UI?
@@ -204,7 +204,7 @@ function ProfilerContextController({children}: Props): React.Node {
204
}
205
}
206
207
- const [selectedTabID, selectTab] = useLocalStorage<TabID>(
207
+ const [persistedTabID, selectTab] = useLocalStorage<TabID>(
208
'React::DevTools::Profiler::defaultTab',
209
'flame-chart',
210
value => {
@@ -217,6 +217,11 @@ function ProfilerContextController({children}: Props): React.Node {
217
},
218
);
219
220
+ // The persisted value may name a tab that no longer exists,
221
+ // e.g. the removed "timeline" tab. Fall back rather than render nothing.
222
+ const selectedTabID: TabID =
223
+ persistedTabID === 'ranked-chart' ? persistedTabID : 'flame-chart';
224
+
225
const stopProfiling = useCallback(
226
() => store.profilerStore.stopProfiling(),
227
[store],
packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js
+38
-35
@@ -19,7 +19,6 @@ import {
19
prepareProfilingDataFrontendFromExport,
20
} from './utils';
21
import {downloadFile} from '../utils';
22
-import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
22
import isArray from 'shared/isArray';
23
import hasOwnProperty from 'shared/hasOwnProperty';
24
@@ -29,7 +28,6 @@ import type {ProfilingDataExport} from './types';
28
29
export default function ProfilingImportExportButtons(): React.Node {
30
const {isProfiling, profilingData, rootID} = useContext(ProfilerContext);
32
- const {setFile} = useContext(TimelineContext);
31
const store = useContext(StoreContext);
32
const {profilerStore} = store;
33
@@ -76,6 +74,22 @@ export default function ProfilingImportExportButtons(): React.Node {
74
}
75
}, []);
76
77
+ const showImportError = (message: string | null) => {
78
+ modalDialogDispatch({
79
+ id: 'ProfilingImportExportButtons',
80
+ type: 'SHOW',
81
+ title: 'Import failed',
82
+ content: (
83
+ <Fragment>
84
+ <div>The profiling data you selected cannot be imported.</div>
85
+ {message !== null && (
86
+ <div className={styles.ErrorMessage}>{message}</div>
87
+ )}
88
+ </Fragment>
89
+ ),
90
+ });
91
+ };
92
+
93
// TODO (profiling) We should probably use a transition for this and suspend while loading the file.
94
// Local files load so fast it's probably not very noticeable though.
95
const handleChange = () => {
@@ -87,39 +101,28 @@ export default function ProfilingImportExportButtons(): React.Node {
101
const fileReader = new FileReader();
102
fileReader.addEventListener('load', () => {
103
const raw = fileReader.result as any as string;
90
- const json = JSON.parse(raw);
91
-
92
- if (!isArray(json) && hasOwnProperty.call(json, 'version')) {
93
- // This looks like React profiling data.
94
- // But first, clear any User Timing marks; we should only have one type open at a time.
95
- setFile(null);
96
-
97
- try {
98
- const profilingDataExport = json as any as ProfilingDataExport;
99
- profilerStore.profilingData =
100
- prepareProfilingDataFrontendFromExport(profilingDataExport);
101
- } catch (error) {
102
- modalDialogDispatch({
103
- id: 'ProfilingImportExportButtons',
104
- type: 'SHOW',
105
- title: 'Import failed',
106
- content: (
107
- <Fragment>
108
- <div>The profiling data you selected cannot be imported.</div>
109
- {error !== null && (
110
- <div className={styles.ErrorMessage}>{error.message}</div>
111
- )}
112
- </Fragment>
113
- ),
114
- });
115
- }
116
- } else {
117
- // Otherwise let's assume this is Trace Event data and pass it to the Timeline preprocessor.
118
- // But first, clear React profiling data; we should only have one type open at a time.
119
- profilerStore.clear();
120
-
121
- // TODO (timeline) We shouldn't need to re-open the File but we'll need to refactor to avoid this.
122
- setFile(file);
104
+
105
+ let json;
106
+ try {
107
+ json = JSON.parse(raw);
108
+ } catch (error) {
109
+ showImportError(error !== null ? error.message : null);
110
+ return;
111
+ }
112
+
113
+ if (isArray(json) || !hasOwnProperty.call(json, 'version')) {
114
+ showImportError(
115
+ 'This file does not look like a profile exported from the React DevTools profiler.',
116
+ );
117
+ return;
118
+ }
119
+
120
+ try {
121
+ const profilingDataExport = json as any as ProfilingDataExport;
122
+ profilerStore.profilingData =
123
+ prepareProfilingDataFrontendFromExport(profilingDataExport);
124
+ } catch (error) {
125
+ showImportError(error !== null ? error.message : null);
126
}
127
});
128
fileReader.readAsText(file);