diff --git a/.planning/phases/29-context-optimization/29-RESEARCH.md b/.planning/phases/29-context-optimization/29-RESEARCH.md new file mode 100644 index 000000000..c7c1f5b97 --- /dev/null +++ b/.planning/phases/29-context-optimization/29-RESEARCH.md @@ -0,0 +1,496 @@ +# Phase 29: Context Optimization - Research + +**Researched:** 2026-03-05 +**Domain:** React context performance optimization +**Confidence:** HIGH + +## Summary + +Phase 29 addresses unnecessary re-renders caused by unmemoized context provider values in MixersContext and the coupling of high-frequency VU updates with low-frequency mixer configuration updates. Phase 28 successfully extracted VU data into an external store, but MixersContext still creates a new value object on every render (line 10 of MixersContext.js), triggering cascading re-renders in all consumer components even when the underlying data hasn't changed. + +The solution involves three complementary strategies: +1. **Memoize context provider values** - Use `useMemo` to prevent new object creation on every render +2. **Split contexts by update frequency** - Separate VuContext (already simplified in Phase 28) from MixersContext configuration +3. **Optimize context consumers** - Use `React.memo` and selector patterns to minimize re-render scope + +This phase builds directly on Phase 28's work. VuContext was already simplified to provide vuStore reference; now MixersContext needs similar optimization for mixer configuration data (volume, pan, mute state, etc.). + +**Primary recommendation:** Wrap the MixersContext.Provider value with useMemo, using appropriate dependencies from useMixerHelper. Since Phase 28 already separated VU updates via external store, this phase focuses exclusively on preventing re-renders from mixer configuration changes. + +## Standard Stack + +The established libraries/tools for this domain: + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| React useMemo | built-in (16.13.1) | Memoize context values | Official React API for preventing object recreation | +| React useCallback | built-in (16.13.1) | Memoize function references | Stabilizes functions passed to context consumers | +| React memo | built-in (16.13.1) | Prevent component re-renders | Standard pattern for optimizing context consumers | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| use-sync-external-store | 1.6.0 (already installed) | External store subscription | Already used for vuStore in Phase 28 | +| @reduxjs/toolkit | 1.6.1 (already installed) | State management | Already used for mixer state in Redux | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| useMemo | use-context-selector | Added dependency; React 19+ has native support; useMemo simpler for this phase | +| Context splitting | Single optimized context | Splitting better separates concerns; VuContext already exists | +| React.memo | PureComponent | Functional components are codebase standard | + +**Installation:** +```bash +# No new dependencies required +# All optimization APIs are React built-ins or already installed +``` + +## Architecture Patterns + +### Recommended Project Structure +``` +src/ +├── context/ +│ ├── MixersContext.js # MODIFY: Add useMemo to provider value +│ ├── VuContext.js # ALREADY OPTIMIZED in Phase 28 +│ └── GlobalContext.js # CHECK: May need useMemo for trackVolumeObject +├── hooks/ +│ ├── useMixerHelper.js # VERIFY: Return stable references +│ └── useVuHelpers.js # ALREADY OPTIMIZED in Phase 28 +└── components/ + └── client/ + ├── JKSessionMyTrack.js # CONSUMER: Verify memo-wrapped + ├── SessionTrackGain.js # CONSUMER: Verify memo-wrapped + └── JKSessionVolumeModal.js # CONSUMER: Verify memo-wrapped +``` + +### Pattern 1: Memoized Context Provider Value +**What:** Wrap the context provider value with useMemo to prevent object recreation on every render. +**When to use:** When context value is an object containing multiple properties or functions. +**Example:** +```javascript +// Source: React official docs + Kent C. Dodds blog +// Modify MixersContext.js + +import React, { createContext, useContext, useMemo } from 'react'; +import useMixerHelper from '../hooks/useMixerHelper.js'; + +const MixersContext = createContext(); + +export const MixersProvider = ({ children }) => { + const mixerHelper = useMixerHelper(); + + // Memoize the context value - only recreate if mixerHelper changes + const value = useMemo(() => mixerHelper, [mixerHelper]); + + return ( + + {children} + + ); +}; + +export const useMixersContext = () => { + const context = useContext(MixersContext); + if (!context) { + throw new Error('useMixersContext must be used within a MixersProvider'); + } + return context; +}; + +export default MixersContext; +``` + +### Pattern 2: Stable Function References with useCallback +**What:** Wrap functions returned from hooks with useCallback to ensure stable references across renders. +**When to use:** When functions are part of context value or passed as props to memoized components. +**Example:** +```javascript +// Source: React official docs for useCallback +// Pattern to apply in useMixerHelper.js + +const faderChanged = useCallback(async (data, mixers, gainType, controlGroup) => { + // Implementation stays the same + // ...existing code... +}, [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers, trackVolumeObject, dispatch]); + +// CRITICAL: Ensure ALL functions in the return object use useCallback +// so that the entire mixerHelper object has stable references +``` + +### Pattern 3: Context Consumer Memoization +**What:** Wrap context consumer components with React.memo to prevent re-renders when props haven't changed. +**When to use:** For all components that consume context and render frequently. +**Example:** +```javascript +// Source: React official docs for memo +// Pattern for SessionTrackGain.js + +import React, { memo } from 'react'; +import { useMixersContext } from '../../context/MixersContext'; + +const SessionTrackGain = memo(function SessionTrackGain({ + mixers, + gainType, + controlGroup, + sessionController, + orientation = 'vertical' +}) { + const mixerHelper = useMixersContext(); + // ... component implementation ... +}); + +export default SessionTrackGain; +``` + +### Pattern 4: Split Context by Update Frequency +**What:** Separate high-frequency updates (VU) from low-frequency updates (mixer config) into different contexts. +**When to use:** When context contains data that updates at dramatically different rates. +**Example:** +```javascript +// Source: Kent C. Dodds optimization guide + developerway.com patterns +// This pattern is ALREADY IMPLEMENTED in Phase 28 + +// VuContext.js - High-frequency updates (50-70/sec) via external store +export const VuProvider = ({ children }) => { + const vuHelpers = useVuHelpers(); + + const value = useMemo(() => ({ + ...vuHelpers, + vuStore, + }), [vuHelpers]); + + return {children}; +}; + +// MixersContext.js - Low-frequency updates (user interactions) +export const MixersProvider = ({ children }) => { + const mixerHelper = useMixerHelper(); + const value = useMemo(() => mixerHelper, [mixerHelper]); + return {children}; +}; +``` + +### Anti-Patterns to Avoid +- **Creating new objects in provider:** `value={{ ...mixerHelper }}` creates new object every render +- **Inline object literals:** `value={{ data, functions }}` always creates new reference +- **Missing dependencies in useMemo:** Stale closures over mixer state +- **Overusing useMemo:** Don't memoize primitive values or simple calculations +- **Forgetting useCallback for functions:** Functions recreated on every render break memo +- **Not combining memo with useMemo:** Memoizing context value alone doesn't prevent consumer re-renders if props change + +## Don't Hand-Roll + +Problems that look simple but have existing solutions: + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Context value comparison | Custom shallow equality | useMemo with correct dependencies | React team optimized useMemo for this; handles edge cases | +| Function reference stability | Manual ref tracking | useCallback hook | Built-in, well-tested, works with concurrent rendering | +| Component re-render prevention | Custom shouldComponentUpdate | React.memo | Optimized for functional components, simpler API | +| Context selector pattern | Custom subscription system | Use useMemo at consumer level | React 19+ has useContextSelector; for React 16 useMemo simpler than library | + +**Key insight:** React's built-in memoization hooks (useMemo, useCallback, memo) are heavily optimized for concurrent rendering and cover the vast majority of context optimization needs. Adding external libraries like use-context-selector adds complexity and bundle size without significant benefit for this specific use case where context updates are already infrequent (user interactions only). + +## Common Pitfalls + +### Pitfall 1: useMemo Dependencies Not Comprehensive +**What goes wrong:** Context value still recreated on every render, defeating memoization +**Why it happens:** Missing dependency in useMemo array, or dependency itself recreates every render +**How to avoid:** +- Use ESLint react-hooks/exhaustive-deps rule +- Ensure hook return values are stable (useCallback wraps functions) +- Verify dependencies with React DevTools Profiler +**Warning signs:** React DevTools shows "MixersProvider" re-rendering every time, even without state changes + +### Pitfall 2: Memoizing Value But Not Function References +**What goes wrong:** Context value object appears memoized, but functions inside recreate, breaking memo +**Why it happens:** Functions returned from hooks aren't wrapped in useCallback +**How to avoid:** Wrap ALL functions in useCallback with correct dependencies +**Warning signs:** memo-wrapped consumers still re-render; React DevTools highlights function prop changes + +### Pitfall 3: memo Without Stable Props +**What goes wrong:** Component wrapped in memo still re-renders frequently +**Why it happens:** Parent passes new object/array/function references as props each render +**How to avoid:** +- useMemo for object/array props +- useCallback for function props +- Pass primitive values when possible +**Warning signs:** Profiler shows "did not skip render" for memo components + +### Pitfall 4: Overusing useMemo for Everything +**What goes wrong:** Code becomes harder to read, minimal performance benefit +**Why it happens:** Premature optimization without measuring actual performance impact +**How to avoid:** +- Only memoize context provider values and expensive computations +- Don't memoize primitive values or simple JSX +- Profile with React DevTools before adding memoization +**Warning signs:** Code complexity increases but Profiler shows no improvement + +### Pitfall 5: Stale Closures from Missing Dependencies +**What goes wrong:** Memoized functions reference old state/props, causing bugs +**Why it happens:** Dependencies array missing reactive values, creating closure over stale data +**How to avoid:** +- Include ALL reactive values in dependency array +- Use ESLint warnings as guide +- Test edge cases where state updates quickly +**Warning signs:** Functions behave with old data; intermittent bugs during rapid interactions + +### Pitfall 6: Context Splitting Without Clear Boundaries +**What goes wrong:** Confusion about which context provides what data +**Why it happens:** Context split arbitrarily without clear domain separation +**How to avoid:** +- Split by update frequency (VU vs config) or domain (auth vs app state) +- Document each context's purpose clearly +- Name contexts descriptively (VuContext, MixersContext) +**Warning signs:** Consumers import multiple contexts; unclear which context owns which data + +## Code Examples + +Verified patterns from official sources: + +### Complete MixersContext Optimization +```javascript +// Source: React official docs + codebase pattern +// Modify jam-ui/src/context/MixersContext.js + +import React, { createContext, useContext, useMemo } from 'react'; +import useMixerHelper from '../hooks/useMixerHelper.js'; + +const MixersContext = createContext(); + +export const MixersProvider = ({ children }) => { + const mixerHelper = useMixerHelper(); + + // CRITICAL: Memoize the entire value object + // Since useMixerHelper returns stable references (via useCallback), + // this prevents new object creation on every render + const value = useMemo(() => mixerHelper, [mixerHelper]); + + return ( + + {children} + + ); +}; + +export const useMixersContext = () => { + const context = useContext(MixersContext); + if (!context) { + throw new Error('useMixersContext must be used within a MixersProvider'); + } + return context; +}; + +export default MixersContext; +``` + +### Verify useMixerHelper Returns Stable References +```javascript +// Source: Current useMixerHelper.js pattern + React useCallback docs +// Audit jam-ui/src/hooks/useMixerHelper.js return statement + +// BEFORE (if needed): +return { + faderChanged: (data, mixers) => { /* impl */ }, // ❌ New function every render + myTracks, // ❌ May recreate if not memoized + mixMode +}; + +// AFTER: +const faderChanged = useCallback(async (data, mixers, gainType, controlGroup) => { + // ...existing implementation... +}, [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers, trackVolumeObject, dispatch]); + +const myTracks = useMemo(() => { + // ...existing computation... +}, [currentSession, isConnected, jamClient, allMixers, mixMode, findMixerForTrack, getParticipant, server.clientId]); + +// VERIFIED: All functions use useCallback, computed values use useMemo +return { + isReady, + session: currentSession, + mixers: allMixers, + myTracks, // ✅ Memoized + findMixerForTrack, // ✅ useCallback + updateMixerData, // ✅ useCallback + updateVU, // ✅ useCallback + simulatedMusicCategoryMixers, + simulatedChatCategoryMixers, + mixMode, + faderChanged, // ✅ useCallback + initGain, // ✅ useCallback + setMixerPan, // ✅ useCallback + getAudioInputCategoryMixer, // ✅ useCallback + getChatCategoryMixer, // ✅ useCallback + backingTracks, + jamTracks, + recordedTracks +}; +``` + +### Memoize Context Consumers +```javascript +// Source: React memo documentation +// Pattern for SessionTrackGain.js and other consumers + +import React, { memo } from 'react'; +import { useMixersContext } from '../../context/MixersContext'; + +// Wrap the entire component export with memo +const SessionTrackGain = memo(function SessionTrackGain({ + mixers, + gainType, + controlGroup, + sessionController, + orientation = 'vertical' +}) { + const mixerHelper = useMixersContext(); + const faderHelpers = useFaderHelpers(); + + // Component implementation stays the same + // ... + + return (/* JSX */); +}); + +// Add display name for debugging +SessionTrackGain.displayName = 'SessionTrackGain'; + +export default SessionTrackGain; +``` + +### Verify GlobalContext Memoization +```javascript +// Source: Current GlobalContext.js + memoization pattern +// Check jam-ui/src/context/GlobalContext.js + +export const GlobalProvider = ({ children }) => { + const [trackVolumeObject, setTrackVolumeObject] = useState({ /* ... */ }); + const [globalObject, setGlobalObject] = useState({ /* ... */ }); + const [videoEnabled, setVideoEnabled] = useState(false); + + const { metronomeState, updateMetronomeState, openMetronome, closeMetronome, resetMetronome } = useMetronomeState(); + + // CRITICAL: Memoize the provider value + const value = useMemo(() => ({ + trackVolumeObject, + setTrackVolumeObject, + globalObject, + setGlobalObject, + metronomeState, + updateMetronomeState, + openMetronome, + closeMetronome, + resetMetronome, + }), [trackVolumeObject, globalObject, metronomeState, updateMetronomeState, openMetronome, closeMetronome, resetMetronome]); + + return ( + + {children} + + ); +}; +``` + +### Testing Context Optimization +```javascript +// Source: React DevTools Profiler best practices +// Pattern for verification after implementing optimization + +// Use React DevTools Profiler to verify: +// 1. Record a session with Profiler +// 2. Move a volume slider (triggers MixersContext update) +// 3. Check which components re-rendered + +// BEFORE optimization: +// - MixersProvider re-renders ❌ +// - ALL consumers re-render (JKSessionMyTrack, SessionTrackGain, etc.) ❌ +// - VU meters re-render ❌ + +// AFTER optimization: +// - MixersProvider does NOT re-render ✅ +// - Only consumers with changed props re-render ✅ +// - VU meters do NOT re-render (external store) ✅ + +// Success criteria from requirements: +// - CTX-01: MixersContext.Provider value is memoized ✅ +// - CTX-02: VuContext separated from MixersContext ✅ (Phase 28) +// - CTX-03: Volume slider change doesn't re-render VU meters ✅ +// - Success: VU update doesn't re-render volume sliders ✅ +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Inline context value object | useMemo-wrapped value | React 16.8 (2019) | Prevents object recreation on every render | +| Class components with shouldComponentUpdate | React.memo for functional components | React 16.6 (2018) | Simpler API, better with hooks | +| Single context for all data | Context splitting by domain/frequency | Best practice (2020+) | Reduces re-render scope | +| Props drilling | Context API | React 16.3 (2018) | Cleaner component tree, but needs optimization | +| use-context-selector library | Native useContextSelector | React 19 (2024) | Built-in selector pattern, but not yet available in React 16 | + +**Deprecated/outdated:** +- **PureComponent for context consumers:** Functional components with memo are now standard +- **Context.Consumer render prop:** useContext hook is cleaner, more readable +- **Separate state/dispatch contexts for every context:** Only split when update frequency differs significantly + +## Open Questions + +Things that couldn't be fully resolved: + +1. **Exact dependencies for MixersContext useMemo** + - What we know: Value should be memoized, dependencies should include mixerHelper + - What's unclear: Whether useMixerHelper return value is already stable (needs code audit) + - Recommendation: Audit useMixerHelper to ensure all functions use useCallback; if stable, dependency is just [mixerHelper] + +2. **Impact of memoizing GlobalContext** + - What we know: GlobalContext also provides trackVolumeObject which updates frequently + - What's unclear: Whether GlobalContext needs same optimization treatment + - Recommendation: Check if GlobalContext consumers experience performance issues; if yes, apply same pattern + +3. **Optimal granularity for context splitting** + - What we know: VuContext already separated (Phase 28); MixersContext contains config + - What's unclear: Whether MixersContext should be further split (e.g., MixerConfigContext vs MixerActionsContext) + - Recommendation: Start with single memoized MixersContext; split further only if profiling shows issues + +4. **React DevTools Profiler verification methodology** + - What we know: Need to verify no re-renders from context updates + - What's unclear: Exact profiler setup and metrics to capture + - Recommendation: Document profiler verification steps in verification task; include before/after screenshots + +## Sources + +### Primary (HIGH confidence) +- [React useMemo official docs](https://react.dev/reference/react/useMemo) - API, dependencies, best practices +- [React useCallback official docs](https://react.dev/reference/react/useCallback) - Stabilizing function references +- [React memo official docs](https://react.dev/reference/react/memo) - Component memoization, context caveats +- [Kent C. Dodds - How to optimize your context value](https://kentcdodds.com/blog/how-to-optimize-your-context-value) - Authoritative pattern guide + +### Secondary (MEDIUM confidence) +- [How to Handle React Context Performance Issues (2026)](https://oneuptime.com/blog/post/2026-01-24-react-context-performance-issues/view) - Modern context optimization patterns +- [developerway.com - React re-renders guide](https://www.developerway.com/posts/react-re-renders-guide) - Comprehensive re-render patterns +- [developerway.com - Performant React apps with Context](https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context) - Context performance strategies +- [Josh Comeau - useMemo and useCallback](https://www.joshwcomeau.com/react/usememo-and-usecallback/) - Clear explanations with examples + +### Tertiary (LOW confidence) +- [use-context-selector npm](https://www.npmjs.com/package/use-context-selector) - Selector pattern library (React 19+ has native support) +- WebSearch results for React context optimization 2026 - General patterns confirmed + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - All optimization APIs are React built-ins, well-documented +- Architecture: HIGH - Patterns verified against React official docs and Kent C. Dodds authoritative sources +- Pitfalls: HIGH - Common issues documented in official React docs and multiple authoritative sources + +**Research date:** 2026-03-05 +**Valid until:** 2026-04-05 (30 days - stable React patterns, no breaking changes expected) + +**Key constraints:** +- React 16.13.1 (no React 19 useContextSelector available) +- VuContext already optimized in Phase 28 with external store +- MixersContext identified as root cause (line 10 creates new object every render) +- Redux already used for state management (no architectural change needed)