632 lines
27 KiB
Markdown
632 lines
27 KiB
Markdown
# Phase 30: Component Memoization - Research
|
|
|
|
**Researched:** 2026-03-05
|
|
**Domain:** React component memoization with React.memo
|
|
**Confidence:** HIGH
|
|
|
|
## Summary
|
|
|
|
Phase 30 focuses on wrapping parent and container components (JKSessionAudioInputs and JKSessionRemoteTracks) with React.memo to prevent unnecessary re-renders when parent components re-render with unchanged props. Phase 29 already completed the foundational work: context providers are memoized, function references are stable, and child components (SessionTrackVU, SessionTrackGain, JKSessionMyTrack) are already wrapped with React.memo.
|
|
|
|
The remaining work is straightforward: apply React.memo to the two container components that orchestrate track rendering. However, success depends critically on prop stability - memoization is completely useless if props passed are always different (inline objects, new function references, or unstable context values). Since Phase 29 stabilized the context and helper functions, the conditions for effective memoization are already in place.
|
|
|
|
React.memo performs shallow comparison of props by default using Object.is. Custom comparison functions are rarely needed and can hurt performance if they're more expensive than re-rendering. The key insight is that React.memo alone is not enough - it must be combined with useMemo and useCallback at the call site to ensure stable prop references.
|
|
|
|
**Primary recommendation:** Wrap JKSessionAudioInputs and JKSessionRemoteTracks with React.memo using default shallow comparison. Add displayName for debugging. Verify with React DevTools Profiler that render counts remain stable when parent re-renders without prop changes.
|
|
|
|
## Standard Stack
|
|
|
|
The established libraries/tools for this domain:
|
|
|
|
### Core
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| React.memo | built-in (16.13.1) | Prevent component re-renders | Official React API for memoizing functional components |
|
|
| React DevTools | latest | Verify memoization effectiveness | Official debugging tool with Profiler for tracking renders |
|
|
|
|
### Supporting
|
|
| Library | Version | Purpose | When to Use |
|
|
|---------|---------|---------|-------------|
|
|
| useMemo | built-in (16.13.1) | Stabilize object/array props | Parent components passing complex props to memo-wrapped children |
|
|
| useCallback | built-in (16.13.1) | Stabilize function props | Parent components passing callbacks to memo-wrapped children |
|
|
| displayName | built-in property | Component naming in DevTools | All memo-wrapped components for debugging |
|
|
|
|
### Alternatives Considered
|
|
| Instead of | Could Use | Tradeoff |
|
|
|------------|-----------|----------|
|
|
| React.memo (default) | React.memo with custom comparison | Custom comparison can be slower than re-rendering; use only when proven necessary |
|
|
| React.memo | PureComponent | PureComponent is for class components; functional components with memo are codebase standard |
|
|
| React.memo | React Compiler (React 19+) | React Compiler not available in React 16.13.1; future upgrade path |
|
|
|
|
**Installation:**
|
|
```bash
|
|
# No new dependencies required
|
|
# React.memo is built into React 16.13.1+
|
|
```
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended Project Structure
|
|
```
|
|
src/
|
|
├── components/
|
|
│ └── client/
|
|
│ ├── JKSessionAudioInputs.js # WRAP: React.memo + displayName
|
|
│ ├── JKSessionRemoteTracks.js # WRAP: React.memo + displayName
|
|
│ ├── JKSessionMyTrack.js # ALREADY MEMOIZED (Phase 29)
|
|
│ ├── SessionTrackVU.js # ALREADY MEMOIZED (Phase 29)
|
|
│ ├── SessionTrackGain.js # ALREADY MEMOIZED (Phase 29)
|
|
│ └── JKSessionScreen.js # PARENT: Verify stable props passed
|
|
```
|
|
|
|
### Pattern 1: Basic React.memo Wrapper
|
|
**What:** Wrap a functional component with React.memo to skip re-renders when props are shallowly equal.
|
|
**When to use:** For components that receive props from parent and re-render frequently despite unchanged props.
|
|
**Example:**
|
|
```javascript
|
|
// Source: React official docs https://react.dev/reference/react/memo
|
|
import React, { memo } from 'react';
|
|
|
|
// Named function expression for better DevTools display
|
|
const JKSessionAudioInputs = memo(function JKSessionAudioInputs({
|
|
myTracks,
|
|
chat,
|
|
mixerHelper,
|
|
isRemote = false,
|
|
mixType = 'default'
|
|
}) {
|
|
// Component implementation
|
|
return (
|
|
// JSX
|
|
);
|
|
});
|
|
|
|
// Add displayName for debugging (DevTools will show this name)
|
|
JKSessionAudioInputs.displayName = 'JKSessionAudioInputs';
|
|
|
|
export default JKSessionAudioInputs;
|
|
```
|
|
|
|
### Pattern 2: Memo with useMemo for Computed Props
|
|
**What:** Combine React.memo on child with useMemo on parent to stabilize computed prop values.
|
|
**When to use:** When parent component derives props through computation before passing to memoized child.
|
|
**Example:**
|
|
```javascript
|
|
// Source: React memo docs + codebase pattern
|
|
// Already implemented in JKSessionRemoteTracks.js
|
|
|
|
const JKSessionRemoteTracks = memo(function JKSessionRemoteTracks({
|
|
mixerHelper,
|
|
sessionModel
|
|
}) {
|
|
// Memoize expensive computation - stable result prevents child re-renders
|
|
const remoteParticipantsData = useMemo(() => {
|
|
// Complex computation deriving participant data
|
|
return participants.map(participant => ({
|
|
// ... computed data
|
|
}));
|
|
}, [currentSession, currentUser, sessionModel, mixerHelper]);
|
|
|
|
return (
|
|
<div>
|
|
{remoteParticipantsData.map(({ participant, tracks }) => (
|
|
<JKSessionAudioInputs
|
|
key={participant.client_id}
|
|
myTracks={tracks}
|
|
mixerHelper={mixerHelper}
|
|
isRemote={true}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
JKSessionRemoteTracks.displayName = 'JKSessionRemoteTracks';
|
|
```
|
|
|
|
### Pattern 3: Verify Prop Stability at Call Site
|
|
**What:** Ensure parent component passes stable references (not inline objects/functions) to memoized children.
|
|
**When to use:** Always, when wrapping components with memo - verify at call sites.
|
|
**Example:**
|
|
```javascript
|
|
// Source: Codebase pattern from JKSessionScreen.js
|
|
// BEFORE memo (if props are unstable):
|
|
<JKSessionAudioInputs
|
|
myTracks={mixerHelper.myTracks} // ✅ Stable - from memoized context
|
|
chat={chat} // ✅ Stable - state variable
|
|
mixerHelper={mixerHelper} // ✅ Stable - memoized context value
|
|
isRemote={false} // ✅ Stable - primitive literal
|
|
/>
|
|
|
|
// ANTI-PATTERN (breaks memoization):
|
|
<JKSessionAudioInputs
|
|
myTracks={mixerHelper.myTracks}
|
|
chat={chat}
|
|
mixerHelper={mixerHelper}
|
|
style={{ gap: '0.5rem' }} // ❌ Inline object - new reference every render
|
|
onTrackClick={() => {}} // ❌ Inline function - new reference every render
|
|
/>
|
|
```
|
|
|
|
### Pattern 4: Custom Comparison (Rare)
|
|
**What:** Provide custom comparison function as second argument to React.memo.
|
|
**When to use:** Only when default shallow comparison is insufficient AND profiling proves custom comparison is faster than re-rendering.
|
|
**Example:**
|
|
```javascript
|
|
// Source: React memo docs https://react.dev/reference/react/memo
|
|
// ONLY use if profiling shows benefit
|
|
|
|
const JKSessionAudioInputs = memo(
|
|
function JKSessionAudioInputs({ myTracks, chat, mixerHelper, isRemote }) {
|
|
// Component implementation
|
|
},
|
|
(oldProps, newProps) => {
|
|
// Return true if props are equal (skip render)
|
|
// Return false if props differ (allow render)
|
|
|
|
// Example: deep comparison of tracks array
|
|
if (oldProps.myTracks.length !== newProps.myTracks.length) {
|
|
return false; // Different length, must re-render
|
|
}
|
|
|
|
// MUST compare ALL props, including functions
|
|
return oldProps.myTracks.every((oldTrack, i) =>
|
|
oldTrack.track?.client_track_id === newProps.myTracks[i].track?.client_track_id
|
|
) && oldProps.mixerHelper === newProps.mixerHelper;
|
|
}
|
|
);
|
|
|
|
// WARNING: Custom comparison can be slower than re-rendering
|
|
// Always benchmark with React DevTools Profiler in production mode
|
|
```
|
|
|
|
### Anti-Patterns to Avoid
|
|
- **Inline objects/arrays as props:** `<Component style={{ width: 100 }} />` breaks memoization (new object every render)
|
|
- **Inline callbacks:** `<Component onClick={() => handler()} />` breaks memoization (new function every render)
|
|
- **Children prop without memoization:** Nesting JSX children defeats memoization unless children are memoized
|
|
- **Memoizing everything:** Don't use memo if component rarely re-renders or is cheap to render
|
|
- **Incomplete custom comparison:** If custom comparison doesn't check all props (especially functions), stale closures cause bugs
|
|
- **Deep equality checks without profiling:** Deep comparison can freeze app if data structure is large/nested
|
|
|
|
## Don't Hand-Roll
|
|
|
|
Problems that look simple but have existing solutions:
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Props equality comparison | Custom shallow equality function | React.memo default behavior | React's Object.is is optimized, handles edge cases, tested at scale |
|
|
| Component display names | Manual name tracking | displayName property | DevTools recognizes displayName; ESLint can enforce via eslint-plugin-react |
|
|
| Render counting | Manual render tracking | React DevTools Profiler | Profiler shows render counts, durations, actualDuration vs baseDuration |
|
|
| Memoization verification | Console.log render tracking | React DevTools Profiler Flamegraph | Profiler visualizes re-render cascades, highlights skipped renders |
|
|
|
|
**Key insight:** React.memo's default shallow comparison covers 95% of use cases. Custom comparison functions introduce maintenance burden and performance risk. Always measure with DevTools before adding custom comparison.
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Inline Props Break Memoization
|
|
**What goes wrong:** Component wrapped with memo still re-renders on every parent render.
|
|
**Why it happens:** Parent passes inline object (`style={{ gap: '0.5rem' }}`) or inline function (`onClick={() => {}}`) as prop, creating new reference each render.
|
|
**How to avoid:**
|
|
- Extract inline objects to useMemo: `const style = useMemo(() => ({ gap: '0.5rem' }), [])`
|
|
- Extract inline functions to useCallback: `const handleClick = useCallback(() => {}, [])`
|
|
- Pass primitive values instead of objects when possible
|
|
**Warning signs:** React DevTools Profiler shows memoized component re-rendering despite "no prop changes"
|
|
|
|
### Pitfall 2: Children Prop Defeats Memoization
|
|
**What goes wrong:** Memoized component re-renders because children prop always changes.
|
|
**Why it happens:** JSX children are props too - parent re-creating child JSX creates new reference.
|
|
**How to avoid:**
|
|
- Composition pattern: Let parent pass stable children as prop
|
|
- Memoize child components individually rather than wrapping parent with children
|
|
- If children are static, extract to constant outside component
|
|
**Warning signs:** Profiler shows re-render even when other props stable
|
|
|
|
**Example:**
|
|
```javascript
|
|
// ANTI-PATTERN: Children defeat memo
|
|
const Parent = () => {
|
|
return (
|
|
<MemoizedContainer>
|
|
<Child /> {/* New element every render, breaks memo */}
|
|
</MemoizedContainer>
|
|
);
|
|
};
|
|
|
|
// SOLUTION: Memoize children or extract
|
|
const Parent = () => {
|
|
const children = useMemo(() => <Child />, []);
|
|
return <MemoizedContainer>{children}</MemoizedContainer>;
|
|
};
|
|
```
|
|
|
|
### Pitfall 3: Context Changes Bypass Memo
|
|
**What goes wrong:** Memoized component re-renders when context value changes, ignoring memo.
|
|
**Why it happens:** Context subscriptions force re-renders regardless of memo. React reconciler marks context-dependent fibers for update, bypassing memo bailout.
|
|
**How to avoid:**
|
|
- Memoize context provider value (Phase 29 completed this)
|
|
- Split contexts by update frequency (Phase 28/29 completed this)
|
|
- If context changes frequently, move context consumption to child component
|
|
**Warning signs:** Memoized component re-renders on every context update
|
|
|
|
**Note:** Phase 29 already addressed this by memoizing MixersContext and VuContext values.
|
|
|
|
### Pitfall 4: Custom Comparison Slower Than Re-render
|
|
**What goes wrong:** Performance gets worse after adding React.memo with custom comparison.
|
|
**Why it happens:** Deep equality checks or complex comparison logic takes longer than re-rendering component.
|
|
**How to avoid:**
|
|
- Default to shallow comparison (no second argument to memo)
|
|
- Only add custom comparison if profiling proves benefit
|
|
- Benchmark custom comparison vs re-render with DevTools Performance panel
|
|
- Keep custom comparison simple - bail out early on obvious differences
|
|
**Warning signs:** Profiler shows increased overall render time after adding memo
|
|
|
|
### Pitfall 5: Missing displayName in DevTools
|
|
**What goes wrong:** Memoized components show as "Anonymous" or "Memo" in DevTools, making debugging difficult.
|
|
**Why it happens:** React can't infer name from arrow functions wrapped in memo.
|
|
**How to avoid:**
|
|
- Use named function expressions: `memo(function ComponentName() {})`
|
|
- Set displayName explicitly: `Component.displayName = 'ComponentName'`
|
|
- Enable ESLint rule `react/display-name` to catch missing names
|
|
**Warning signs:** DevTools component tree shows "Anonymous" or generic "Memo" labels
|
|
|
|
### Pitfall 6: Memoizing Components That Change Frequently
|
|
**What goes wrong:** Overhead of comparison checking outweighs re-render cost.
|
|
**Why it happens:** Component props change on most parent renders, so memo comparison always returns false.
|
|
**How to avoid:**
|
|
- Profile first with DevTools - only memoize if many renders with same props
|
|
- Don't memoize if component is cheap to render (simple JSX, no hooks)
|
|
- Don't memoize if component always gets different props
|
|
**Warning signs:** Profiler shows memo component renders just as often as non-memo version
|
|
|
|
## Code Examples
|
|
|
|
Verified patterns from official sources:
|
|
|
|
### Complete React.memo Implementation for JKSessionAudioInputs
|
|
```javascript
|
|
// Source: React memo docs + Phase 29 patterns
|
|
// File: jam-ui/src/components/client/JKSessionAudioInputs.js
|
|
|
|
import React, { memo } from 'react';
|
|
import JKSessionMyTrack from './JKSessionMyTrack.js';
|
|
import { getInstrumentIcon45, convertClientInstrumentToServer } from '../../helpers/utils';
|
|
|
|
// Named function expression wrapped in memo
|
|
const JKSessionAudioInputs = memo(function JKSessionAudioInputs({
|
|
myTracks,
|
|
chat,
|
|
mixerHelper,
|
|
isRemote = false,
|
|
mixType = 'default'
|
|
}) {
|
|
return (
|
|
<div className='d-flex' style={{ gap: '0.5rem' }}>
|
|
<div>
|
|
{myTracks.length === 0 && !chat ? (
|
|
<div>No tracks available</div>
|
|
) : (
|
|
<>
|
|
{myTracks.map((track, index) => {
|
|
const instrumentId = track.track?.instrument_id;
|
|
const serverInstrument = typeof instrumentId === 'number'
|
|
? convertClientInstrumentToServer(instrumentId)
|
|
: (instrumentId || track.track?.instrument);
|
|
|
|
let selectedMixers = track.mixers;
|
|
if (mixType === 'personal' && track.personalMixers) {
|
|
selectedMixers = track.personalMixers;
|
|
} else if (mixType === 'master' && track.masterMixers) {
|
|
selectedMixers = track.masterMixers;
|
|
}
|
|
|
|
return (
|
|
<div key={track.track.client_track_id || index}>
|
|
<JKSessionMyTrack
|
|
{...track}
|
|
mixers={selectedMixers}
|
|
instrument={serverInstrument}
|
|
mode={mixerHelper.mixMode}
|
|
isRemote={isRemote}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
{chat && (
|
|
<JKSessionMyTrack
|
|
key="chat"
|
|
{...chat}
|
|
trackName="Chat"
|
|
instrument="headphones"
|
|
hasMixer={true}
|
|
isChat={true}
|
|
isRemote={isRemote}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Critical for debugging in React DevTools
|
|
JKSessionAudioInputs.displayName = 'JKSessionAudioInputs';
|
|
|
|
export default JKSessionAudioInputs;
|
|
```
|
|
|
|
### Complete React.memo Implementation for JKSessionRemoteTracks
|
|
```javascript
|
|
// Source: React memo docs + codebase pattern
|
|
// File: jam-ui/src/components/client/JKSessionRemoteTracks.js
|
|
|
|
import React, { useMemo, memo } from 'react';
|
|
import { useSelector } from 'react-redux';
|
|
import { selectActiveSession } from '../../store/features/activeSessionSlice';
|
|
import { useAuth } from '../../context/UserAuth';
|
|
import JKSessionAudioInputs from './JKSessionAudioInputs';
|
|
import { getAvatarUrl, getInstrumentIcon45_inverted } from '../../helpers/utils';
|
|
|
|
const JKSessionRemoteTracks = memo(function JKSessionRemoteTracks({
|
|
mixerHelper,
|
|
sessionModel
|
|
}) {
|
|
const currentSession = useSelector(selectActiveSession);
|
|
const { currentUser } = useAuth();
|
|
|
|
// Memoize expensive computation - already present in current code
|
|
const remoteParticipantsData = useMemo(() => {
|
|
if (!currentSession || !currentUser) return [];
|
|
|
|
const allParticipants = sessionModel.participants();
|
|
const remoteParticipants = allParticipants.filter(participant =>
|
|
participant.user.id !== currentUser.id
|
|
);
|
|
|
|
return remoteParticipants.map(participant => {
|
|
const tracks = [];
|
|
const connStatsClientId = participant.client_role === 'child' && participant.parent_client_id
|
|
? participant.parent_client_id
|
|
: participant.client_id;
|
|
|
|
const photoUrl = getAvatarUrl(participant.user.photo_url);
|
|
const name = participant.user.name;
|
|
|
|
for (const track of participant.tracks || []) {
|
|
const mixerData = mixerHelper.findMixerForTrack(
|
|
participant.client_id,
|
|
track,
|
|
false,
|
|
mixerHelper.mixMode
|
|
);
|
|
const hasMixer = !!mixerData.mixer;
|
|
const instrumentIcon = getInstrumentIcon45_inverted(
|
|
track.instrument_id || track.instrument
|
|
);
|
|
const trackName = name;
|
|
|
|
tracks.push({
|
|
track,
|
|
mixerFinder: [participant.client_id, track, false],
|
|
mixers: mixerData,
|
|
hasMixer,
|
|
name,
|
|
trackName,
|
|
instrumentIcon,
|
|
photoUrl,
|
|
clientId: participant.client_id,
|
|
userId: participant.user.id,
|
|
connStatsClientId
|
|
});
|
|
}
|
|
|
|
return {
|
|
participant,
|
|
tracks,
|
|
hasChat: false
|
|
};
|
|
});
|
|
}, [currentSession, currentUser, sessionModel, mixerHelper]);
|
|
|
|
return (
|
|
<div className='d-flex' style={{ gap: '1rem' }}>
|
|
{remoteParticipantsData.map(({ participant, tracks, hasChat }) => (
|
|
<JKSessionAudioInputs
|
|
key={participant.client_id}
|
|
myTracks={tracks}
|
|
chat={hasChat ? {} : null}
|
|
mixerHelper={mixerHelper}
|
|
isRemote={true}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Critical for debugging in React DevTools
|
|
JKSessionRemoteTracks.displayName = 'JKSessionRemoteTracks';
|
|
|
|
export default JKSessionRemoteTracks;
|
|
```
|
|
|
|
### Verifying Memoization with React DevTools Profiler
|
|
```javascript
|
|
// Source: React DevTools Profiler best practices
|
|
// Manual testing procedure after implementing React.memo
|
|
|
|
/**
|
|
* VERIFICATION STEPS:
|
|
*
|
|
* 1. Open React DevTools (browser extension)
|
|
* 2. Go to Profiler tab
|
|
* 3. Click Record button (red circle)
|
|
* 4. Trigger parent re-render (e.g., adjust a session setting)
|
|
* 5. Stop recording
|
|
* 6. Examine Flamegraph and Ranked views
|
|
*
|
|
* SUCCESS CRITERIA:
|
|
*
|
|
* - JKSessionAudioInputs: Should show "Did not render" when myTracks unchanged
|
|
* - JKSessionRemoteTracks: Should show "Did not render" when mixerHelper/sessionModel unchanged
|
|
* - Render count: Should remain stable (not increment) when props don't change
|
|
* - actualDuration: Should be 0ms or very low for skipped renders
|
|
*
|
|
* COMPARISON METRICS:
|
|
*
|
|
* - actualDuration: Time spent rendering with memoization optimizations
|
|
* - baseDuration: Time without any optimizations (baseline)
|
|
* - If actualDuration << baseDuration, memoization is working
|
|
*
|
|
* COMMON ISSUES:
|
|
*
|
|
* - Component still renders: Check for inline props at call site
|
|
* - "Did not skip render": Verify parent passes stable references
|
|
* - Render count increments: Props are changing on every parent render
|
|
*/
|
|
|
|
// Example DevTools inspection code (for debugging):
|
|
// In component, add temporary effect to log renders
|
|
useEffect(() => {
|
|
console.log('JKSessionAudioInputs rendered with props:', {
|
|
myTracks,
|
|
chat,
|
|
mixerHelper,
|
|
isRemote
|
|
});
|
|
});
|
|
```
|
|
|
|
### Call Site Verification Pattern
|
|
```javascript
|
|
// Source: Codebase JKSessionScreen.js
|
|
// Verify props passed to memoized components are stable
|
|
|
|
// CORRECT (stable props):
|
|
const JKSessionScreen = () => {
|
|
const mixerHelper = useMixersContext(); // ✅ Memoized context
|
|
const chat = useChatState(); // ✅ State variable
|
|
const sessionModel = useSessionModel(); // ✅ Hook return value
|
|
|
|
return (
|
|
<>
|
|
<JKSessionAudioInputs
|
|
myTracks={mixerHelper.myTracks} // ✅ Property of memoized context
|
|
chat={chat} // ✅ Direct state reference
|
|
mixerHelper={mixerHelper} // ✅ Memoized context value
|
|
isRemote={false} // ✅ Primitive literal
|
|
/>
|
|
|
|
<JKSessionRemoteTracks
|
|
mixerHelper={mixerHelper} // ✅ Memoized context value
|
|
sessionModel={sessionModel} // ✅ Hook return (stable if hook memoizes)
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
// INCORRECT (breaks memoization):
|
|
const JKSessionScreen = () => {
|
|
const mixerHelper = useMixersContext();
|
|
|
|
return (
|
|
<>
|
|
<JKSessionAudioInputs
|
|
myTracks={mixerHelper.myTracks}
|
|
chat={null}
|
|
mixerHelper={mixerHelper}
|
|
isRemote={false}
|
|
config={{ showAvatar: true }} // ❌ Inline object - new reference
|
|
onTrackClick={() => {}} // ❌ Inline function - new reference
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
```
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| PureComponent (class) | React.memo (functional) | React 16.6 (2018) | Simpler API, works with hooks |
|
|
| Manual shouldComponentUpdate | React.memo default comparison | React 16.6 (2018) | Less boilerplate, handles most cases |
|
|
| Deep comparison libraries | Shallow comparison + useMemo | React 16.8+ (2019) | Cheaper, more predictable |
|
|
| Always memoize everything | Profile first, memoize strategically | Best practice (2020+) | Avoid premature optimization |
|
|
| Context + memo issues | Memoized context values | Best practice (2021+) | Memo works correctly with stable context |
|
|
| React.memo everywhere | React Compiler (auto-memoization) | React 19 (2024) | Compiler handles memoization automatically |
|
|
|
|
**Deprecated/outdated:**
|
|
- **PureComponent for functional components:** Use React.memo instead
|
|
- **Deep equality comparison by default:** Too expensive; use shallow comparison + useMemo for objects
|
|
- **Memoizing without measuring:** Always profile with DevTools first
|
|
- **Context without memoization:** Context providers must memoize value to make consumer memo effective
|
|
|
|
**Future direction:**
|
|
- **React Compiler (React 19+):** Automatically inserts memoization where needed, reducing manual React.memo usage
|
|
- **Selective Hydration (React 18+):** Works with memo to prioritize which components hydrate first
|
|
- **useContextSelector (React 19+):** Built-in context selectors make memo more effective with context
|
|
|
|
## Open Questions
|
|
|
|
Things that couldn't be fully resolved:
|
|
|
|
1. **Exact re-render frequency of JKSessionScreen parent**
|
|
- What we know: JKSessionScreen is the parent rendering JKSessionAudioInputs and JKSessionRemoteTracks
|
|
- What's unclear: How frequently does JKSessionScreen re-render, and what triggers those re-renders?
|
|
- Recommendation: Profile with DevTools before and after to measure actual impact of memoization
|
|
|
|
2. **sessionModel stability**
|
|
- What we know: sessionModel passed as prop to JKSessionRemoteTracks from useSessionModel hook
|
|
- What's unclear: Does useSessionModel return stable reference, or does it recreate object on each call?
|
|
- Recommendation: Audit useSessionModel.js to verify it uses useMemo or returns stable reference
|
|
|
|
3. **myTracks array reference stability**
|
|
- What we know: myTracks comes from mixerHelper.myTracks (Phase 29 memoized mixerHelper)
|
|
- What's unclear: Is myTracks array itself stable, or is it recreated (with same content) on updates?
|
|
- Recommendation: If myTracks array recreates with same tracks, might need custom comparison function (profile first)
|
|
|
|
4. **Testing strategy without automated tests**
|
|
- What we know: No existing tests for JKSessionAudioInputs or JKSessionRemoteTracks
|
|
- What's unclear: Best approach to verify memoization works without regression suite
|
|
- Recommendation: Manual DevTools Profiler testing for this phase; consider adding render count tests in future
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- [React.memo official docs](https://react.dev/reference/react/memo) - Complete API, comparison behavior, context caveats
|
|
- [React Profiler API docs](https://react.dev/reference/react/Profiler) - actualDuration vs baseDuration metrics
|
|
- [React DevTools Profiler usage](https://legacy.reactjs.org/docs/profiler.html) - Measuring render performance
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- [Use React.memo() wisely - Dmitri Pavlutin](https://dmitripavlutin.com/use-react-memo-wisely/) - When to use, when not to use
|
|
- [React.memo Optimization Guide - Strapi](https://strapi.io/blog/react-memo-optimize-functional-components-guide) - Practical patterns
|
|
- [Fixing memoization-breaking re-renders - Sentry](https://blog.sentry.io/fixing-memoization-breaking-re-renders-in-react/) - Common pitfalls
|
|
- [React Context Performance Trap - Azguards](https://azguards.com/performance-optimization/the-propagation-penalty-bypassing-react-context-re-renders-via-usesyncexternalstore/) - Context + memo interaction
|
|
- [How to Properly Memoize Context Values - Medium](https://medium.com/@aadil.shaikh04/how-to-properly-memoize-context-values-in-react-and-why-it-matters-8c18518ee1be) - Context memoization patterns
|
|
|
|
### Tertiary (LOW confidence)
|
|
- [Why children prop breaks React.memo - GitHub Gist](https://gist.github.com/slikts/e224b924612d53c1b61f359cfb962c06) - Children prop pitfall
|
|
- [React.memo displayName issues - GitHub](https://github.com/facebook/react/issues/18026) - DevTools display name handling
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH - React.memo is built into React 16.6+, well-documented
|
|
- Architecture: HIGH - Patterns verified against official React docs and established best practices
|
|
- Pitfalls: HIGH - Common issues documented extensively in React docs and community resources
|
|
|
|
**Research date:** 2026-03-05
|
|
**Valid until:** 2026-04-05 (30 days - stable React patterns, React 16.13.1 environment)
|
|
|
|
**Key constraints:**
|
|
- React 16.13.1 (no React Compiler auto-memoization available)
|
|
- Phase 29 completed context value memoization (MixersContext, VuContext stable)
|
|
- SessionTrackVU and SessionTrackGain already wrapped with memo (Phase 29)
|
|
- JKSessionMyTrack already wrapped with memo (Phase 29)
|
|
- Must verify prop stability at call sites (JKSessionScreen.js)
|
|
|
|
**Dependencies on Phase 29:**
|
|
- MixersContext.Provider value memoized - ensures mixerHelper reference stable
|
|
- useMixerHelper functions use useCallback - ensures function props stable
|
|
- Context consumers already memoized - prevents re-render cascades
|
|
- React.memo pattern established - this phase extends same pattern to parent containers
|