docs(29): research context optimization phase domain

Phase 29: Context Optimization
- Standard stack identified (useMemo, useCallback, memo built-ins)
- Architecture patterns documented (memoized provider values)
- Pitfalls catalogued (stale closures, missing dependencies)
This commit is contained in:
Nuwan 2026-03-05 17:15:44 +05:30
parent 393bdfe057
commit ceec2342fe
1 changed files with 496 additions and 0 deletions

View File

@ -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 (
<MixersContext.Provider value={value}>
{children}
</MixersContext.Provider>
);
};
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 <VuContext.Provider value={value}>{children}</VuContext.Provider>;
};
// MixersContext.js - Low-frequency updates (user interactions)
export const MixersProvider = ({ children }) => {
const mixerHelper = useMixerHelper();
const value = useMemo(() => mixerHelper, [mixerHelper]);
return <MixersContext.Provider value={value}>{children}</MixersContext.Provider>;
};
```
### 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 (
<MixersContext.Provider value={value}>
{children}
</MixersContext.Provider>
);
};
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 (
<GlobalContext.Provider value={value}>
{children}
</GlobalContext.Provider>
);
};
```
### 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)