diff --git a/src/pages/Player/RealtimeWaveform.tsx b/src/pages/Player/RealtimeWaveform.tsx
index d9e8e3a..53c5153 100644
--- a/src/pages/Player/RealtimeWaveform.tsx
+++ b/src/pages/Player/RealtimeWaveform.tsx
@@ -13,7 +13,7 @@
* and a dotted placeholder when idle.
*/
-import { createEffect, on } from "solid-js";
+import { createEffect, createMemo, on, Show } from "solid-js";
import { useTerminalDimensions } from "@opentui/solid";
import { useVisualizer } from "@/stores/visualizer";
import { useTheme } from "@/context/ThemeContext";
@@ -45,46 +45,40 @@ export function RealtimeWaveform() {
createEffect(on(numBars, (n) => viz.setBarCount(n)));
// ── Rendering ──────────────────────────────────────────────────────
-
- const renderLine = () => {
+ //
+ // The two bar rows are CONTENT of fixed text nodes — the element tree is
+ // created once and only the strings change. Rebuilding the box/texts per
+ // bar update (30/s) tears down a renderable per frame, and the renderer
+ // leaks ~44KB of native memory per teardown: gigabytes over one long
+ // playback session.
+ const lines = createMemo(() => {
const bars = viz.barData();
- const count = numBars();
-
- // Loading state: the braille spinner shows while the pipeline is
- // warming up — cold start (first play / after an unload), resume
- // into undecoded audio, or a stalled position clock (mpv
- // re-buffering after a long pause on a network stream). The store
- // clears it the moment the first fresh frame renders, so stale
- // bars never masquerade as live data while the pipeline re-arms.
- if (viz.isLoading() || viz.isStalled()) {
- return ;
- }
-
if (bars.length === 0) {
- const placeholder = ".".repeat(count);
- return (
-
- {placeholder}
- {placeholder}
-
- );
+ const placeholder = ".".repeat(numBars());
+ return { top: placeholder, bottom: placeholder };
}
-
const pairs = bars.map((v) => barChars(Math.floor(v * BAR_LEVELS)));
- const top = pairs.map((pair) => pair.top).join("");
- const bottom = pairs.map((pair) => pair.bottom).join("");
-
- return (
-
- {top}
- {bottom}
-
- );
- };
+ return {
+ top: pairs.map((pair) => pair.top).join(""),
+ bottom: pairs.map((pair) => pair.bottom).join(""),
+ };
+ });
return (
- {renderLine()}
+ {/* Loading: braille spinner while the pipeline warms up — cold
+ start, resume into undecoded audio, or a stalled position
+ clock. The store clears it the moment the first fresh frame
+ renders, so stale bars never masquerade as live data. */}
+ }
+ >
+
+ {lines().top}
+ {lines().bottom}
+
+
);
}