Skip to content

Commit 2bdd602

Browse files
perf(web): cut click-handler latency from ~1s to ~0ms in PreviewPanel
"[Violation] click handler took 1151ms" — every state change to ComposePage re-ran PreviewPanel's offline-render effect, which spun up a fresh OfflineAudioContext (44.1 kHz × 10 s = 441k samples) and synchronously walked every step's factory. With the click landing inside the same task, the user-visible click→playback latency was hundreds of ms. PreviewPanel: - Skip the offline render entirely while playback is active. The on-screen waveform from the previous render stays visible; the effect re-runs once when `playing` flips back to false. - Cut sample rate from 44100 → 22050 (the preview is a visual waveform, not an audible source) and cap the buffer length to 8 s. ~4× cheaper buffer alloc + render. - Push the actual rendering through requestIdleCallback (250 ms debounce, 400 ms timeout fallback). The click handler returns immediately; the heavy work runs in the next idle slot. - New `playing` dep on the render effect so the waveform refreshes the moment playback ends. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cd8792a commit 2bdd602

1 file changed

Lines changed: 26 additions & 5 deletions

File tree

web/src/components/PreviewPanel.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,31 @@ export const PreviewPanel = forwardRef<PreviewPanelHandle, Props>(function Previ
8181
/* ------------------------------------------------------ waveform render */
8282

8383
const renderTokenRef = useRef(0)
84+
// Mirror `playing` into a ref so the schedule() callback can read the
85+
// latest value without re-running the effect (which would re-tear-down
86+
// the ResizeObserver).
87+
const playingRef = useRef(playing)
88+
playingRef.current = playing
8489

8590
useEffect(() => {
8691
let timer: ReturnType<typeof setTimeout> | null = null
8792
function schedule(): void {
8893
if (timer) clearTimeout(timer)
94+
// Skip the offline render entirely while playing — the playhead
95+
// animation runs at 60fps and a 200-800ms render call inside the
96+
// click/play handler caused "[Violation] click handler took ~1s".
97+
// The waveform is already on screen from the last render; we'll
98+
// refresh it when playback ends.
99+
if (playingRef.current) return
89100
timer = setTimeout(() => {
90-
void renderWaveform()
91-
}, 80)
101+
// Yield to the next idle slot so the click handler that triggered
102+
// this re-render can return immediately.
103+
if (typeof requestIdleCallback === "function") {
104+
requestIdleCallback(() => void renderWaveform(), { timeout: 400 })
105+
} else {
106+
void renderWaveform()
107+
}
108+
}, 250)
92109
}
93110

94111
async function renderWaveform(): Promise<void> {
@@ -109,8 +126,12 @@ export const PreviewPanel = forwardRef<PreviewPanelHandle, Props>(function Previ
109126
return
110127
}
111128

112-
const sampleRate = 44100
113-
const length = Math.max(1, Math.floor((sampleRate * Math.max(totalMs, 200)) / 1000))
129+
// Halve the sample rate (22.05 kHz vs 44.1 kHz) and cap the buffer
130+
// length: the preview is a visual waveform, not playback audio, so
131+
// we don't need full-fidelity samples.
132+
const sampleRate = 22050
133+
const cappedMs = Math.min(Math.max(totalMs, 200), 8000)
134+
const length = Math.max(1, Math.floor((sampleRate * cappedMs) / 1000))
114135
type OACtor = new (channels: number, length: number, sampleRate: number) => unknown
115136
const Offline =
116137
(window as unknown as { OfflineAudioContext?: OACtor }).OfflineAudioContext ??
@@ -256,7 +277,7 @@ export const PreviewPanel = forwardRef<PreviewPanelHandle, Props>(function Previ
256277
obs.disconnect()
257278
window.removeEventListener("resize", schedule)
258279
}
259-
}, [steps, totalMs, viewStart, viewEnd])
280+
}, [steps, totalMs, viewStart, viewEnd, playing])
260281

261282
return (
262283
<section className="relative flex-1 flex flex-col bg-foreground text-background p-4 min-h-0 overflow-hidden">

0 commit comments

Comments
 (0)