From a020e27260fbfa3a40f5d141e8eafeba03ff36d7 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 3 Mar 2026 20:26:16 +0530 Subject: [PATCH] refactor(28-02): rewrite SessionTrackVU with direct DOM updates - Remove useVuContext - directly import vuStore - Remove VuMeter component usage - render lights directly - Add RAF loop polling vuStore.getLevelSnapshot at ~60fps - Store light element refs for direct className assignment - Wrap with React.memo to prevent parent re-renders - Zero React re-renders for VU visual updates Co-Authored-By: Claude Opus 4.5 --- .../src/components/client/SessionTrackVU.js | 116 +++++++++++++----- 1 file changed, 87 insertions(+), 29 deletions(-) diff --git a/jam-ui/src/components/client/SessionTrackVU.js b/jam-ui/src/components/client/SessionTrackVU.js index 411eb4cf7..9009a8ff0 100644 --- a/jam-ui/src/components/client/SessionTrackVU.js +++ b/jam-ui/src/components/client/SessionTrackVU.js @@ -1,44 +1,102 @@ -import React, { useEffect, useRef } from 'react'; -import { useVuContext } from '../../context/VuContext'; +import React, { useEffect, useRef, memo } from 'react'; +import { vuStore } from '../../stores/vuStore'; -function SessionTrackVU({ lightCount, orientation, lightWidth, lightHeight, side, ptr, mixers }) { - const { VuMeter, updateVuState } = useVuContext(); - const ptrRef = useRef(ptr || `STV${Date.now()}`); +const SessionTrackVU = memo(function SessionTrackVU({ + lightCount = 16, + orientation = 'vertical', + lightWidth = 15, + lightHeight = 10, + mixers +}) { + const containerRef = useRef(null); + const lightsRef = useRef([]); const mixerIdRef = useRef(null); + const rafIdRef = useRef(null); + // Update mixerId when mixers prop changes useEffect(() => { const mixer = mixers?.vuMixer; - - if (!mixer) { - mixerIdRef.current = null; - return; - } - - // Create a unique ID for this VU meter - const mixerId = `${mixer.mode ? 'M' : 'P'}${mixer.id}`; - mixerIdRef.current = mixerId; - - //console.debug("SessionTrackVU: VU registered for mixer", mixerId); + mixerIdRef.current = mixer ? `${mixer.mode ? 'M' : 'P'}${mixer.id}` : null; return () => { - // Cleanup: reset VU state when component unmounts or mixer changes + // Cleanup VU state on unmount if (mixerIdRef.current) { - updateVuState(mixerIdRef.current, 0, false); - mixerIdRef.current = null; + vuStore.removeLevel(mixerIdRef.current); } }; - }, [mixers, updateVuState]); + }, [mixers]); - // Use the React component for rendering + // Animation loop for direct DOM updates - separate from React lifecycle + useEffect(() => { + const updateVisuals = () => { + const mixerId = mixerIdRef.current; + + if (!mixerId) { + rafIdRef.current = requestAnimationFrame(updateVisuals); + return; + } + + const data = vuStore.getLevelSnapshot(mixerId); + const level = data?.level ?? 0; + const clipping = data?.clipping ?? false; + const activeLights = Math.round(level * lightCount); + + // Direct DOM manipulation - no React involvement + for (let i = 0; i < lightCount; i++) { + const light = lightsRef.current[i]; + if (!light) continue; + + const positionFromBottom = lightCount - 1 - i; + const isActive = positionFromBottom < activeLights; + + // Use className assignment for efficient class manipulation + // (faster than classList.add/remove for multiple classes) + if (isActive) { + if (clipping || positionFromBottom >= Math.floor(lightCount * 0.75)) { + light.className = 'vu-light vu-bg-danger'; + } else if (positionFromBottom >= Math.floor(lightCount * 0.5)) { + light.className = 'vu-light vu-bg-warning'; + } else { + light.className = 'vu-light vu-bg-success'; + } + } else { + light.className = 'vu-light vu-bg-secondary'; + } + } + + rafIdRef.current = requestAnimationFrame(updateVisuals); + }; + + rafIdRef.current = requestAnimationFrame(updateVisuals); + + return () => { + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current); + } + }; + }, [lightCount]); + + // Render ONCE - never re-renders for VU updates return ( - +
+
+ {Array.from({ length: lightCount }).map((_, i) => ( +
lightsRef.current[i] = el} + className="vu-light vu-bg-secondary" + style={{ + height: `${lightHeight}px`, + width: '25px', + marginTop: '1px', + borderRadius: '2px', + border: '1px solid #eee' + }} + /> + ))} +
+
); -} +}); export default SessionTrackVU;