mui / mui/base-ui

[avatar] Fallback flashes for cached images on SSR hydration

Open
#4,468 2 comments 2 reactions 0 assignees View on GitHub
component: avatar type: bug
Dominant language
TypeScript
Stars
10.9k
Forks
543
Avg merge
1d 20h
Merged PRs (30d)
101

Description

# Bug report

## Current behavior

`Avatar.Fallback` produces a visible flash during SSR hydration even when the image source is already in the browser cache.

`useImageLoadingStatus` has no synchronous cached-image fast-path. It always initializes as `'idle'` and the only path to `'loaded'` is the async `onload` callback. Since `useIsoLayoutEffect` is a no-op on the server, the fallback is baked into the server HTML with no Avatar `` at all. After hydration, the layout effect fires but never checks `image.complete`, which is already `true` synchronously for cached images.

https://github.com/mui/base-ui/blob/master/packages/react/src/avatar/image/useImageLoadingStatus.ts#L17

![Image](https://github.com/user-attachments/assets/280a6ea5-df74-4bed-8cc5-1a09591c4a7b)

Left: base-ui Avatar shows red fallback. Right: plain `` with the same cached URL renders instantly.

Measurements with 4x CPU throttle on a minimal page (image shown above):

| Framework | Red fallback frames | Flash duration |
|-----------|:---:|:---:|
| TanStack Start | 14 | ~304ms |
| Next.js | 3 | ~52ms |
| Vite SPA (client-only) | 1 | ~16ms |

The client-side case is a single-frame flash but the SSR case is the real problem. The flash duration scales with hydration time and gets worse on slower devices or larger pages.

## Expected behavior

For cached images, `Avatar.Image` should render immediately without showing `Avatar.Fallback`. A plain `` with the same cached URL renders instantly on first paint.

## Reproducible example

Any `Avatar` with a cached `src` and a visible fallback during SSR hydration. Tested with both Next.js and TanStack Start.

## Base UI version

v1.3.0

## Which browser are you using?

Chrome, Firefox

## Which OS are you using?

Windows, MacOS, Linux

## Additional context

Radix Avatar handles this with an `image.complete` check in the `useState` initializer to skip the fallback on the first render for cached images: [radix-ui/primitives](https://github.com/radix-ui/primitives/blob/22473d16404bfd446305db5b6c9308aece99fdec/packages/react/avatar/src/avatar.tsx)

Happy to open a PR that addresses this.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.