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 (
-