Waterfall
Spans of a trace on its time axis, as a tree.
import { Waterfall } from "@openperf/deft";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
export default function WaterfallBasic() {
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
<Waterfall.Ruler />
{TRACE.spans.map((span) => (
<Waterfall.Row
depth={span.depth}
duration={span.duration}
key={span.name}
label={`${span.duration} ms`}
name={span.name}
start={span.start}
tone={span.tone}
/>
))}
</Waterfall.Root>
</Preview>
);
}
- Use it for one trace when its hierarchy, timing and selected span matter together.
- Use one time scale: row
startand markeratare milliseconds from the trace start, and everydurationuses milliseconds on that same scale. - Keep rows in parent/child order and set
depthto match the hierarchy. - The root span is
tone="root", ordinary spans arechild, and the failing span isfailing, which also colours its name. - Give
Waterfall.Rootanaria-label. It is atree, and the rows are one tab stop: ↑ ↓ move, → expands, ← collapses, Enter or Space selects. - A row scrolls into view when it becomes selected, so selection driven from outside the tree, such as a page shortcut, stays visible.
- The duration label sits after the bar, or before it when the bar ends near the right edge, and is left out when neither side has room.
Summary
import { Waterfall } from "@openperf/deft";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
export default function WaterfallSummary() {
return (
<Preview block={true}>
<Waterfall.Summary
segments={TRACE.spans
.filter((span) => span.depth === 1)
.map((span) => ({ id: span.name, duration: span.duration }))}
/>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
{TRACE.spans.map((span) => (
<Waterfall.Row
depth={span.depth}
duration={span.duration}
key={span.name}
name={span.name}
start={span.start}
tone={span.tone}
/>
))}
</Waterfall.Root>
</Preview>
);
}
Put a Waterfall.Summary above the waterfall to show where the request spent time. Each segment is as wide as its share of the total.
Expandable rows
import { Waterfall } from "@openperf/deft";
import { useState } from "react";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
const PARENTS = new Set(["Span 1", "Span 3"]);
export default function WaterfallExpandableRows() {
const [collapsed, setCollapsed] = useState<string[]>([]);
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
{TRACE.spans
.filter((span, index) =>
TRACE.spans
.slice(0, index)
.every(
(ancestor, ancestorIndex) =>
!collapsed.includes(ancestor.name) ||
TRACE.spans
.slice(ancestorIndex + 1, index + 1)
.some((between) => between.depth <= ancestor.depth),
),
)
.map((span) => (
<Waterfall.Row
depth={span.depth}
duration={span.duration}
expanded={PARENTS.has(span.name) ? !collapsed.includes(span.name) : undefined}
key={span.name}
name={span.name}
onToggle={() => {
setCollapsed(
collapsed.includes(span.name)
? collapsed.filter((name) => name !== span.name)
: [...collapsed, span.name],
);
}}
start={span.start}
tone={span.tone}
/>
))}
</Waterfall.Root>
</Preview>
);
}
Set expanded and onToggle on parents to show or hide their children. Leave expanded undefined on leaves.
Critical path
import { Waterfall } from "@openperf/deft";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
const CRITICAL_PATH = new Set(["Span 1", "Span 3", "Span 4"]);
export default function WaterfallCriticalPath() {
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
{TRACE.spans.map((span) => (
<Waterfall.Row
depth={span.depth}
dimmed={!CRITICAL_PATH.has(span.name)}
duration={span.duration}
key={span.name}
name={span.name}
start={span.start}
tone={CRITICAL_PATH.has(span.name) ? "path" : "child"}
/>
))}
</Waterfall.Root>
</Preview>
);
}
Set tone="path" on the spans that determined the trace’s latency and dimmed on the rest. This is a timing view, separate from failure status: a healthy span can be critical, and a failing span is not promoted to the path automatically.
Flat density
import { Waterfall } from "@openperf/deft";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
export default function WaterfallFlatDensity() {
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" density="flat" duration={TRACE.duration}>
{TRACE.spans.map((span) => (
<Waterfall.Row
depth={span.depth}
duration={span.duration}
key={span.name}
name={span.name}
start={span.start}
tone={span.tone}
/>
))}
</Waterfall.Root>
</Preview>
);
}
density="flat" is for compact waterfalls embedded in list pages. Rows are shorter and leaves get no chevron space; leave expanded undefined on every row to make a static summary rather than an expandable tree.
Details
import { Waterfall } from "@openperf/deft";
import { Fragment, useState } from "react";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
export default function WaterfallDetails() {
const [selected, setSelected] = useState<null | string>("Span 3");
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
{TRACE.spans.map((span) => (
<Fragment key={span.name}>
<Waterfall.Row
depth={span.depth}
duration={span.duration}
name={span.name}
onSelect={() => {
setSelected(selected === span.name ? null : span.name);
}}
selected={selected === span.name}
start={span.start}
tone={span.tone}
/>
{selected === span.name && (
<Waterfall.Details depth={span.depth}>Details</Waterfall.Details>
)}
</Fragment>
))}
</Waterfall.Root>
</Preview>
);
}
Waterfall.Details shows what the selected span carries right under its row: stats, an error and its attributes. Render it after the selected row and pass the row’s depth so it lines up with the name. Selecting the row again should close it. Details are not rows: arrow keys skip them, and clicks inside never select a span.
Markers
import { Waterfall } from "@openperf/deft";
import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";
export default function WaterfallMarkers() {
return (
<Preview block={true}>
<Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
{TRACE.spans.map((span) => (
<Waterfall.Row
depth={span.depth}
duration={span.duration}
key={span.name}
markers={
span.name === "Span 3"
? [
{ at: 60, id: "log", kind: "log" },
{ at: 90, id: "error", kind: "error" },
]
: undefined
}
name={span.name}
start={span.start}
tone={span.tone}
/>
))}
</Waterfall.Root>
</Preview>
);
}
Markers show point-in-time events along a span: log circles for log entries and error diamonds for failures. Position them with at so readers see when an event happened without changing the span’s duration.
API
durationnumber
On Root and Row, in ms.
numbernameWidth?number
On Root. Width of the name column in pixels, capped at a share of the root.
number300density?"tree" | "flat"
On Root.
"tree" | "flat""tree"nameReactNode
On Row. The span name on the left.
ReactNodeleading?ReactNode
On Row. Before the name, such as a service icon.
ReactNodelabel?ReactNode
On Row. Next to the bar, such as the duration.
ReactNodestartnumber
On Row, ms from the trace start.
numberdepth?number
On Row and Details.
number0tone?"root" | "child" | "path" | "failing"
On Row.
"root" | "child" | "path" | "failing""child"dimmed?boolean
On Row. De-emphasizes spans off the critical path.
booleanfalseexpanded?boolean
On Row. Undefined for leaves.
booleanonToggle?() => void
On Row. Expands or collapses its children.
() => voidselected?boolean
On Row.
booleanfalseonSelect?() => void
On Row.
() => voidmarkers?{ id: string; at: number; kind: "log" | "error" }[]
On Row. `at` is in ms from the trace start.
{ id: string; at: number; kind: "log" | "error" }[]formatDuration?(milliseconds: number) => ReactNode
On Ruler. Defaults to milliseconds in the active locale.
(milliseconds: number) => ReactNodesegments{ id: string; duration: number; className?: string }[]
On Summary. Each segment is as wide as its share of the total.
{ id: string; duration: number; className?: string }[]errorAt?number
On Summary. Error marker, ms from the trace start.
numberOther props of Root, Details and Summary go to the native div. Parts: Root, Details, Row, Ruler, Summary.