@samitouri / QOS-React / commits / c7cab6664d

[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);