Performance hooks
Monitor editor performance with real-time interaction metrics.
import { useCallback, useEffect, useState } from 'react'
import {
Editor,
PerformanceApiAdapter,
TLCameraEndPerfEvent,
TLInteractionEndPerfEvent,
TLPerfFrameTimeStats,
Tldraw,
useEditor,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './performance-hooks.css'
function FrameTimeStats({ event }: { event: TLPerfFrameTimeStats }) {
return (
<div className="perf-section">
<div className="perf-section-title">Frame times</div>
<div className="perf-row">
<span className="perf-label">Duration</span>
<span className="perf-value">{event.duration.toFixed(0)}ms</span>
</div>
<div className="perf-row">
<span className="perf-label">FPS</span>
<span className="perf-value">{event.fps.toFixed(1)}</span>
</div>
<div className="perf-row">
<span className="perf-label">Frames</span>
<span className="perf-value">{event.frameCount}</span>
</div>
<div className="perf-row">
<span className="perf-label">Avg</span>
<span className="perf-value">{event.avgFrameTime.toFixed(1)}ms</span>
</div>
<div className="perf-row">
<span className="perf-label">p95</span>
<span className="perf-value">{event.p95FrameTime.toFixed(1)}ms</span>
</div>
</div>
)
}
type LastEvent =
| { kind: 'interaction'; event: TLInteractionEndPerfEvent }
| { kind: 'camera'; event: TLCameraEndPerfEvent }
// [1]
function PerfPanel() {
const editor = useEditor()
const [last, setLast] = useState<LastEvent | null>(null)
useEffect(() => {
// [2]
const unsubs = [
editor.performance.on('interaction-end', (event) => {
setLast({ kind: 'interaction', event })
}),
editor.performance.on('camera-end', (event) => {
setLast({ kind: 'camera', event })
}),
]
// [3]
const adapter = new PerformanceApiAdapter(editor.performance)
return () => {
unsubs.forEach((unsub) => unsub())
adapter.dispose()
}
}, [editor])
return (
<div className="perf-panel">
{last ? (
<>
<div className="perf-section">
<div className="perf-section-title">
{last.kind === 'interaction'
? `Interaction: ${last.event.name}`
: `Camera: ${last.event.type}`}
</div>
</div>
<FrameTimeStats event={last.event} />
{last.kind === 'interaction' ? (
<div className="perf-section">
<div className="perf-section-title">Context</div>
{Object.entries(last.event.selectedShapeTypes).map(([type, count]) => (
<div className="perf-row" key={type}>
<span className="perf-label">{type}</span>
<span className="perf-value">{count}</span>
</div>
))}
<div className="perf-row">
<span className="perf-label">All shapes</span>
<span className="perf-value">{last.event.shapeCount}</span>
</div>
<div className="perf-row">
<span className="perf-label">Zoom</span>
<span className="perf-value">{(last.event.zoomLevel * 100).toFixed(0)}%</span>
</div>
</div>
) : (
<div className="perf-section">
<div className="perf-row">
<span className="perf-label">Visible shapes</span>
<span className="perf-value">{last.event.visibleShapeCount}</span>
</div>
<div className="perf-row">
<span className="perf-label">Culled shapes</span>
<span className="perf-value">{last.event.culledShapeCount}</span>
</div>
<div className="perf-row">
<span className="perf-label">Zoom</span>
<span className="perf-value">{(last.event.zoomLevel * 100).toFixed(0)}%</span>
</div>
</div>
)}
</>
) : (
<div className="perf-hint">Drag a shape or pan the canvas to see performance stats</div>
)}
</div>
)
}
// [4]
export default function PerformanceHooksExample() {
const handleMount = useCallback((editor: Editor) => {
editor.createShapes([
{ type: 'geo', x: 100, y: 100, props: { w: 200, h: 200, fill: 'solid' } },
{ type: 'geo', x: 400, y: 100, props: { w: 150, h: 150, geo: 'ellipse', fill: 'solid' } },
{ type: 'geo', x: 200, y: 350, props: { w: 250, h: 100, geo: 'diamond', fill: 'solid' } },
])
}, [])
return (
<div className="tldraw__editor">
<Tldraw onMount={handleMount} components={{ InFrontOfTheCanvas: PerfPanel }} />
</div>
)
}
/*
[1]
The PerfPanel subscribes to both `interaction-end` and `camera-end`
events. Only the most recent event is shown — each new event replaces
the previous one. Placed in the InFrontOfTheCanvas slot.
[2]
`editor.performance.on('interaction-end', fn)` fires when any interaction
completes (translate, resize, rotate, draw, etc.). `camera-end` fires
after panning or zooming stops (debounced). Both include frame time
stats (avg, p95, fps) plus contextual data.
[3]
The PerformanceApiAdapter pipes perf events into the browser's
Performance API (`performance.mark()` / `performance.measure()`).
Open DevTools → Performance tab → record → interact → stop, and you'll
see named measures like `tldraw:interaction:translating` in the Timings
lane. It's optional and tree-shakeable.
[4]
We create some shapes on mount so there's something to interact with.
Drag a shape or pan the canvas to see the performance panel update.
*/
Use editor.performance.on() to subscribe to performance events. In this example, we listen for interaction-end events to display frame time statistics (avg, p95, p99) whenever you finish resizing, translating, or drawing shapes. Try selecting a shape and resizing it to see the performance overlay update.
Is this page helpful?
Prev
Interaction end callbackNext
Selection bounds