[DevTools] Add a little bar indicating time span of an async entry relative to others (#34016)
Stacked on #34012. This shows a time track for when some I/O started and when it finished relative to other I/O in the same component (or later in the same suspense boundary). This is not meant to be a precise visualization since the data might be misleading if you're running this in dev which has other perf characteristics anyway. It's just meant to be a general way to orient yourself in the data. We can also highlight rejected promises here. The color scheme is the same as Chrome's current Performance Track colors to add continuity but those could change. <img width="478" height="480" alt="Screenshot 2025-07-27 at 11 48 03 PM" src="https://github.com/user-attachments/assets/545dd591-a91f-4c47-be96-41d80f09a94a" />
Sebastian Markbåge committed
Jul 28, 2025 at 12:22 UTC
101b20b663cc866ad4c0102413bbf7cb111451b1
3 files changed
+86
-3
packages/react-devtools-shared/src/devtools/constants.js
+6
@@ -135,6 +135,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
135
'--color-timeline-text-color': '#000000',
136
'--color-timeline-text-dim-color': '#ccc',
137
'--color-timeline-react-work-border': '#eeeeee',
138
+ '--color-timebar-background': '#f6f6f6',
139
+ '--color-timespan-background': '#62bc6a',
140
+ '--color-timespan-background-errored': '#d57066',
141
'--color-search-match': 'yellow',
142
'--color-search-match-current': '#f7923b',
143
'--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)',
@@ -283,6 +286,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
286
'--color-timeline-text-color': '#282c34',
287
'--color-timeline-text-dim-color': '#555b66',
288
'--color-timeline-react-work-border': '#3d424a',
289
+ '--color-timebar-background': '#1d2129',
290
+ '--color-timespan-background': '#62bc6a',
291
+ '--color-timespan-background-errored': '#d57066',
292
'--color-search-match': 'yellow',
293
'--color-search-match-current': '#f7923b',
294
'--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)',
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+20
@@ -89,3 +89,23 @@
89
.PreviewContainer {
90
padding: 0 0.25rem 0.25rem 0.25rem;
91
}
92
+
93
+.TimeBarContainer {
94
+ position: relative;
95
+ flex: 0 0 20%;
96
+ height: 0.25rem;
97
+ border-radius: 0.125rem;
98
+ background-color: var(--color-timebar-background);
99
+}
100
+
101
+.TimeBarSpan, .TimeBarSpanErrored {
102
+ position: absolute;
103
+ border-radius: 0.125rem;
104
+ background-color: var(--color-timespan-background);
105
+ width: 100%;
106
+ height: 100%;
107
+}
108
+
109
+.TimeBarSpanErrored {
110
+ background-color: var(--color-timespan-background-errored);
111
+}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+60
-3
@@ -19,10 +19,13 @@ import styles from './InspectedElementSharedStyles.css';
19
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
20
import StackTraceView from './StackTraceView';
21
import OwnerView from './OwnerView';
22
+import {meta} from '../../../hydration';
23
23
-import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
24
+import type {
25
+ InspectedElement,
26
+ SerializedAsyncInfo,
27
+} from 'react-devtools-shared/src/frontend/types';
28
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
25
-import type {SerializedAsyncInfo} from 'react-devtools-shared/src/frontend/types';
29
30
type RowProps = {
31
bridge: FrontendBridge,
@@ -31,6 +34,8 @@ type RowProps = {
34
store: Store,
35
asyncInfo: SerializedAsyncInfo,
36
index: number,
37
+ minTime: number,
38
+ maxTime: number,
39
};
40
41
function SuspendedByRow({
@@ -40,6 +45,8 @@ function SuspendedByRow({
45
store,
46
asyncInfo,
47
index,
48
+ minTime,
49
+ maxTime,
50
}: RowProps) {
51
const [isOpen, setIsOpen] = useState(false);
52
const name = asyncInfo.awaited.name;
@@ -52,17 +59,47 @@ function SuspendedByRow({
59
stack = asyncInfo.stack;
60
owner = asyncInfo.owner;
61
}
62
+ const start = asyncInfo.awaited.start;
63
+ const end = asyncInfo.awaited.end;
64
+ const timeScale = 100 / (maxTime - minTime);
65
+ let left = (start - minTime) * timeScale;
66
+ let width = (end - start) * timeScale;
67
+ if (width < 5) {
68
+ // Use at least a 5% width to avoid showing too small indicators.
69
+ width = 5;
70
+ if (left > 95) {
71
+ left = 95;
72
+ }
73
+ }
74
+
75
+ const value: any = asyncInfo.awaited.value;
76
+ const isErrored =
77
+ value !== null &&
78
+ typeof value === 'object' &&
79
+ value[meta.name] === 'rejected Thenable';
80
+
81
return (
82
<div className={styles.CollapsableRow}>
83
<Button
84
className={styles.CollapsableHeader}
85
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
60
- title={`${isOpen ? 'Collapse' : 'Expand'}`}>
86
+ title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
87
<ButtonIcon
88
className={styles.CollapsableHeaderIcon}
89
type={isOpen ? 'expanded' : 'collapsed'}
90
/>
91
<span className={styles.CollapsableHeaderTitle}>{name}</span>
92
+ <div className={styles.TimeBarContainer}>
93
+ <div
94
+ className={
95
+ !isErrored ? styles.TimeBarSpan : styles.TimeBarSpanErrored
96
+ }
97
+ style={{
98
+ left: left.toFixed(2) + '%',
99
+ width: width.toFixed(2) + '%',
100
+ }}
101
+ />
102
+ </div>
103
</Button>
104
{isOpen && (
105
<div className={styles.CollapsableContent}>
@@ -129,6 +166,24 @@ export default function InspectedElementSuspendedBy({
166
() => copy(serializeDataForCopy(suspendedBy)),
167
);
168
169
+ let minTime = Infinity;
170
+ let maxTime = -Infinity;
171
+ for (let i = 0; i < suspendedBy.length; i++) {
172
+ const asyncInfo: SerializedAsyncInfo = suspendedBy[i];
173
+ if (asyncInfo.awaited.start < minTime) {
174
+ minTime = asyncInfo.awaited.start;
175
+ }
176
+ if (asyncInfo.awaited.end > maxTime) {
177
+ maxTime = asyncInfo.awaited.end;
178
+ }
179
+ }
180
+
181
+ if (maxTime - minTime < 25) {
182
+ // Stretch the time span a bit to ensure that we don't show
183
+ // large bars that represent very small timespans.
184
+ minTime = maxTime - 25;
185
+ }
186
+
187
return (
188
<div>
189
<div className={styles.HeaderRow}>
@@ -146,6 +201,8 @@ export default function InspectedElementSuspendedBy({
201
element={element}
202
inspectedElement={inspectedElement}
203
store={store}
204
+ minTime={minTime}
205
+ maxTime={maxTime}
206
/>
207
))}
208
</div>