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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-03-03 20:26:16 +05:30
parent f8214854d9
commit a020e27260
1 changed files with 87 additions and 29 deletions

View File

@ -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;
mixerIdRef.current = mixer ? `${mixer.mode ? 'M' : 'P'}${mixer.id}` : null;
if (!mixer) {
mixerIdRef.current = null;
return () => {
// Cleanup VU state on unmount
if (mixerIdRef.current) {
vuStore.removeLevel(mixerIdRef.current);
}
};
}, [mixers]);
// Animation loop for direct DOM updates - separate from React lifecycle
useEffect(() => {
const updateVisuals = () => {
const mixerId = mixerIdRef.current;
if (!mixerId) {
rafIdRef.current = requestAnimationFrame(updateVisuals);
return;
}
// Create a unique ID for this VU meter
const mixerId = `${mixer.mode ? 'M' : 'P'}${mixer.id}`;
mixerIdRef.current = mixerId;
const data = vuStore.getLevelSnapshot(mixerId);
const level = data?.level ?? 0;
const clipping = data?.clipping ?? false;
const activeLights = Math.round(level * lightCount);
//console.debug("SessionTrackVU: VU registered for mixer", mixerId);
// 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 () => {
// Cleanup: reset VU state when component unmounts or mixer changes
if (mixerIdRef.current) {
updateVuState(mixerIdRef.current, 0, false);
mixerIdRef.current = null;
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, [mixers, updateVuState]);
}, [lightCount]);
// Use the React component for rendering
// Render ONCE - never re-renders for VU updates
return (
<VuMeter
mixerId={mixerIdRef.current}
orientation={orientation}
lightCount={lightCount}
lightWidth={lightWidth}
lightHeight={lightHeight}
<div ref={containerRef} className="vu">
<div className="d-flex flex-column" style={{ height: `${lightCount * (lightHeight + 1)}px` }}>
{Array.from({ length: lightCount }).map((_, i) => (
<div
key={i}
ref={el => lightsRef.current[i] = el}
className="vu-light vu-bg-secondary"
style={{
height: `${lightHeight}px`,
width: '25px',
marginTop: '1px',
borderRadius: '2px',
border: '1px solid #eee'
}}
/>
))}
</div>
</div>
);
}
});
export default SessionTrackVU;