Memoize only to skip work

Put work below early returns, keep props stable, skip trivial memos.

React docsreact.dev
If your project has React Compiler enabled, manual memoization with memo() and useMemo() is not necessary.
React CompilerRead docs ↗

From vercel-react-best-practices: rerender-memo and 3 more.

useMemo before an early return

Loading is on. Type a name and feel the lag.

useMemo, then returnBad
computeAvatarId calls
0
while loading
0
main thread blocked
0 ms
useMemo(…); if (loading) return
memo(UserAvatar)Good
computeAvatarId calls
0
while loading
0
main thread blocked
0 ms
if (loading) return; <UserAvatar />

const UserAvatar = memo(function UserAvatar({ user }) {
  const id = useMemo(() => computeAvatarId(user), [user])
  return <Avatar id={id} />
})

function Profile({ user, loading }) {
  if (loading) return <Skeleton />
  return <div><UserAvatar user={user} /></div>
}

Inline defaults break memo()

Press Start and watch the avatar ring.

Default made in renderBad
00:00
Ada Lovelace
Header renders
0
UserAvatar renders
1
Object.is(prev.onClick, next.onClick)
n/a
function Header({ onAvatarClick = () => {} })
Hoisted defaultGood
00:00
Ada Lovelace
Header renders
0
UserAvatar renders
1
Object.is(prev.onClick, next.onClick)
n/a
const NOOP = () => {} · { onAvatarClick = NOOP }

const NOOP = () => {}

function Header({ onAvatarClick = NOOP }) {
  const now = useClock()
  return <><time>{now}</time><UserAvatar user={user} onClick={onAvatarClick} /></>
}

useMemo costs more than a || b

Pick a render count and run.

Time per renderMeasured
useMemo(() => a || b, [a, b])–
a || b–

function Header({ user, notifications }) {
  const isLoading = user.isLoading || notifications.isLoading
  if (isLoading) return <Skeleton />
}

Why it matters, pros and cons

Why it matters

Memo pays only if it skips work. Before a return or with unstable props, it can't.

Pros

  • No work for UI that is off screen
  • Memoized children stay skipped
  • Less code where a hook was overhead

Cons

  • React Compiler memoizes for you
  • The extracted component recomputes once when it remounts
  • memo() on cheap components is pure overhead