Note React.memo only does a shallow prop comparison. If you pass objects or functions that are recreated each render, memo has no effect. Pair with useMemo/useCallback for those props.
Note Route-based code splitting gives the biggest bundle size reduction because entire page components and their dependencies are loaded on demand. Place the Suspense boundary around the lazy components, not the entire app.
// 1. Move state down (closer to where it is used)// 2. Lift content up (pass components as children)// 3. Memoize with React.memo + useCallback/useMemo
example
// PROBLEM: typing in input re-renders the expensive listfunction BadPage() {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ExpensiveList /> {/* re-renders on every keystroke */}
</div>
);
}
// FIX 1: Move state down into its own componentfunction GoodPage() {
return (
<div>
<SearchInput /> {/* state lives here now */}
<ExpensiveList /> {/* no longer re-renders on keystrokes */}
</div>
);
}
function SearchInput() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// FIX 2: Lift content up via childrenfunction InputWrapper({ children }) {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{children} {/* children don't re-render */}
</div>
);
}
function Page() {
return (
<InputWrapper>
<ExpensiveList />
</InputWrapper>
);
}
Note Before reaching for React.memo, try restructuring your component tree. Moving state down and lifting content up are free optimizations that often eliminate the problem entirely.
avoid re-rendersreduce rendersmove state downlift content uprender optimization
Key Prop for Efficient Updates
syntax
// Stable keys help React identify which items changed
<Item key={item.id} />
// Reset component state by changing the key
<Form key={selectedUserId} />
example
// Using key to reset component statefunction UserEditor({ userId }) {
// When userId changes, React destroys the old Form// and mounts a fresh one with clean statereturn <ProfileForm key={userId} userId={userId} />;
}
function ProfileForm({ userId }) {
const [name, setName] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
fetchUser(userId).then(user => {
setName(user.name);
setBio(user.bio);
});
}, [userId]);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<textarea value={bio} onChange={e => setBio(e.target.value)} />
</form>
);
}
Note Changing a component's key forces React to unmount and remount it, resetting all internal state. This is a clean way to 'reset' a form or component when switching between items without manually clearing each state variable.
key propreset componentkey optimizationforce remountchange key reset state
import { Profiler } from'react';
function onRenderCallback(
id,
phase, // 'mount' or 'update'
actualDuration, // time spent rendering
baseDuration, // estimated time without memoization
startTime,
commitTime
) {
console.log(`${id} [${phase}]: ${actualDuration.toFixed(1)}ms`);
}
function App() {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList />
</Profiler>
);
}
Note Use the Profiler component or React DevTools Profiler tab to identify slow renders. Focus on components with high actualDuration. The baseDuration shows the worst-case cost without any memoization.
Note For lists with hundreds or thousands of items, virtualization dramatically improves performance by only rendering visible rows. react-window is lightweight; react-virtuoso supports dynamic heights. Measure first -- small lists do not need this.