react-sonic-perf
v0.1.0
Published
Audio-based real-time performance feedback for React apps — hear long tasks, layout shifts, slow renders and hydration delays as sound cues via the Web Audio API.
Downloads
146
Maintainers
Readme
react-sonic-perf
Hear your React app's performance problems in real time.
react-sonic-perf turns performance degradations — long tasks, layout shifts (CLS),
slow component renders, and hydration delays — into live sound cues using the Web
Audio API. Wrap your tree in <SonicProfiler> and jank becomes audible: the worse
the incident, the more intense the sound.
Why audio? Your eyes are busy looking at the UI you're building. A Geiger-counter click when the main thread blocks, or a pitch that rises with render cost, gives you an ambient performance channel you can't tune out — no DevTools tab required.
- Zero runtime dependencies
- React 18 and 19 (
peerDependencies) - SSR-safe and Next.js App Router compatible (
"use client"built in) - Fully typed (TypeScript, strict)
Intended as a development tool. Gate it behind
process.env.NODE_ENV !== "production"(Next.js) orimport.meta.env.DEV(Vite).
Install
npm install react-sonic-perf
# or
pnpm add react-sonic-perf
# or
yarn add react-sonic-perfQuickstart — Next.js (App Router)
<SonicProfiler> is a client component. Create a small client wrapper and render it
from your root layout:
// app/sonic-perf-provider.tsx
"use client";
import { SonicProfiler } from "react-sonic-perf";
export function SonicPerfProvider({ children }: { children: React.ReactNode }) {
return (
<SonicProfiler
mode="geiger"
threshold={16}
enabled={process.env.NODE_ENV !== "production"}
>
{children}
</SonicProfiler>
);
}// app/layout.tsx
import { SonicPerfProvider } from "./sonic-perf-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SonicPerfProvider>{children}</SonicPerfProvider>
</body>
</html>
);
}See example/NextLayoutExample.tsx for a fuller
version with a demo "block the main thread" button.
Quickstart — Vite
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { SonicProfiler } from "react-sonic-perf";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<SonicProfiler mode="pulse" threshold={16} enabled={import.meta.env.DEV}>
<App />
</SonicProfiler>
</StrictMode>
);See example/ViteAppExample.tsx for a demo app with
buttons that trigger artificial long tasks and slow renders.
Modes
| Mode | Sound | How severity is expressed | Best for |
| --------- | --------------------------------------- | -------------------------------------------------------- | ------------------------------------- |
| pulse | Short sine blip (default) | Pitch rises: 220 Hz base + severity × 220 Hz (max 880 Hz) | General use; distinct, non-intrusive |
| geiger | Geiger-counter square-wave click bursts | Click count and rate scale with severity (1 → 7 clicks) | Hunting jank; visceral feedback |
| ambient | Low 55 Hz drone | Loudness (gain) rises with severity | Background monitoring while you work |
Threshold guidance
threshold is the duration (ms) an incident must exceed before it makes sound.
Severity is (duration − threshold) / threshold, clamped to 0..3.
- 16 ms (default) — one 60 fps frame. Strict; good for animation-heavy UIs.
- 50 ms — the "long task" definition from RAIL. Good general default for busier apps.
- 100–200 ms — only hear the genuinely bad stuff.
Layout shifts are unitless (CLS), so they are scaled onto the same model:
value × 250 pseudo-ms. At the default 16 ms threshold, shifts below ~0.064 stay
silent and a "poor" 0.25 CLS lands near max severity.
Autoplay policy note
Browsers block audio until the user interacts with the page. The engine handles this
for you: the AudioContext is created lazily and one-time pointerdown/keydown
listeners resume it on the first gesture. Until then, cues are silently dropped (not
queued) — so click or press a key once before expecting sound. Incidents are
still reported to onIncident the whole time.
API reference
<SonicProfiler>
| Prop | Type | Default | Description |
| ------------ | ---------------------------------- | ------------------- | ---------------------------------------------------------------------- |
| mode | 'ambient' \| 'geiger' \| 'pulse' | 'pulse' | Auditory style (see Modes). |
| threshold | number | 16 | Duration (ms) above which an incident fires. |
| enabled | boolean | true | Master switch. When false, no observers run and no audio is created. |
| volume | number | 0.2 | Master volume, 0..1. |
| muted | boolean | false | Silence audio but keep reporting incidents via onIncident. |
| onIncident | (incident: PerfIncident) => void | — | Called for every detected incident (also when muted). |
| id | string | 'sonic-profiler' | Id passed to the underlying React <Profiler>. |
| children | ReactNode | — (required) | The subtree to profile. |
What it detects:
slow-render— React<Profiler>commits whoseactualDurationexceedsthreshold.long-task—PerformanceObserverlongtaskentries (main thread blocked > 50 ms).layout-shift—layout-shiftentries without recent input, scaled from the CLS value.hydration-delay— measured once per page load, from module load to the first effect.
All observers are feature-detected; unsupported environments simply skip that signal.
PerfIncident
interface PerfIncident {
kind: "long-task" | "layout-shift" | "slow-render" | "hydration-delay";
durationMs: number; // observed (or synthesized) duration
severity: number; // 0..3, see threshold guidance
timestamp: number; // performance.now()-based
detail?: string; // human-readable context
}computeSeverity(durationMs, thresholdMs): number
Pure helper: 0 at/under threshold, else (duration − threshold) / threshold
clamped to 0..3.
mapSeverityToSound(severity, mode): SoundCue
Pure helper mapping a severity + mode to a playable cue
({ frequencyHz, gain, durationMs, kind: 'click' | 'blip' | 'drone', clickCount? }).
SonicAudioEngine
The Web Audio wrapper used internally — exported for advanced use and testing.
const engine = new SonicAudioEngine({
volume: 0.2,
muted: false,
contextFactory: () => new AudioContext(), // optional; inject a fake in tests
});
engine.play(mapSeverityToSound(2, "pulse"));
engine.setVolume(0.5);
engine.setMuted(true);
engine.dispose();All methods are SSR-safe and no-op gracefully when the Web Audio API is unavailable.
License
MIT © Dinesh Kumar
