Skip to content

[avatar] Fallback flashes for cached images on SSR hydration #4468

@erdinctekay

Description

@erdinctekay

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 <img> 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

Left: base-ui Avatar shows red fallback. Right: plain <img> 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 <img> 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

Happy to open a PR that addresses this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: avatarChanges related to the avatar component.type: bugIt doesn't behave as expected.
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions