perf(P6A.T5): pre-warm Supabase + avatar loading hints

Fire a no-await profiles query in AuthContext on session establish to absorb
cold-connection latency before the first user-triggered request. Add
loading='lazy' default to the central Avatar component so all off-screen
avatars (chat list, friends list, popovers, message senders) skip eager
Supabase Storage fetches; set loading='eager' on ConversationHeader (active
conv header) and CallParticipantTile inline imgs (both AudioContent and
VideoStub) which are always above-the-fold when visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-16 23:43:04 +02:00
parent 36ab7eca8a
commit f3672e40c8
4 changed files with 19 additions and 3 deletions
+7
View File
@@ -11,6 +11,11 @@ interface Props {
// brand-tinted fallback; callers can override (e.g. to colour-by-id). // brand-tinted fallback; callers can override (e.g. to colour-by-id).
fallbackClass?: string; fallbackClass?: string;
alt?: string; alt?: string;
// Browser loading hint. Use 'eager' for above-the-fold avatars (e.g. the
// active conversation header, call tiles). Defaults to 'lazy' so off-screen
// avatars (chat list rows, friends list, popovers) don't hammer Supabase
// Storage on initial render.
loading?: 'eager' | 'lazy';
} }
export function Avatar({ export function Avatar({
@@ -19,6 +24,7 @@ export function Avatar({
className = 'h-10 w-10', className = 'h-10 w-10',
fallbackClass = 'bg-accent/20 text-accent', fallbackClass = 'bg-accent/20 text-accent',
alt, alt,
loading = 'lazy',
}: Props) { }: Props) {
const effectiveUrl = useCachedAvatarUrl(url); const effectiveUrl = useCachedAvatarUrl(url);
if (effectiveUrl) { if (effectiveUrl) {
@@ -28,6 +34,7 @@ export function Avatar({
alt={alt ?? displayName ?? ''} alt={alt ?? displayName ?? ''}
className={'shrink-0 rounded-full object-cover ' + className} className={'shrink-0 rounded-full object-cover ' + className}
draggable={false} draggable={false}
loading={loading}
/> />
); );
} }
@@ -291,6 +291,7 @@ function AudioContent({
src={avatarUrl} src={avatarUrl}
alt="" alt=""
className="relative h-full w-full rounded-full object-cover" className="relative h-full w-full rounded-full object-cover"
loading="eager"
/> />
) : ( ) : (
<span <span
@@ -366,7 +367,7 @@ function VideoStub({
} }
> >
{avatarUrl ? ( {avatarUrl ? (
<img src={avatarUrl} alt="" className="h-full w-full rounded-full object-cover" /> <img src={avatarUrl} alt="" className="h-full w-full rounded-full object-cover" loading="eager" />
) : ( ) : (
letter letter
)} )}
@@ -129,9 +129,9 @@ function HeaderBar({
className="relative shrink-0 rounded-full text-left transition enabled:cursor-pointer enabled:hover:ring-2 enabled:hover:ring-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-default" className="relative shrink-0 rounded-full text-left transition enabled:cursor-pointer enabled:hover:ring-2 enabled:hover:ring-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-default"
> >
{isDm ? ( {isDm ? (
<Avatar url={peerAvatar} displayName={title} className="h-10 w-10 text-base" /> <Avatar url={peerAvatar} displayName={title} className="h-10 w-10 text-base" loading="eager" />
) : peerAvatar ? ( ) : peerAvatar ? (
<Avatar url={peerAvatar} displayName={title} className="h-10 w-10 text-base" /> <Avatar url={peerAvatar} displayName={title} className="h-10 w-10 text-base" loading="eager" />
) : ( ) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-accent/20 text-accent"> <div className="flex h-10 w-10 items-center justify-center rounded-full bg-accent/20 text-accent">
<UsersIcon className="h-5 w-5" /> <UsersIcon className="h-5 w-5" />
+8
View File
@@ -204,6 +204,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
void registerWebPush(installId); void registerWebPush(installId);
}, [session]); }, [session]);
// Pre-warm Supabase: fires the first round-trip in the background so the
// first user-triggered query (e.g. loading conversations) doesn't pay
// the cold-connection latency.
useEffect(() => {
if (!session) return;
void supabase.from('profiles').select('id').limit(1).then(() => undefined);
}, [session]);
// Phase 3: ensure this install owns exactly one devices row. The row is // Phase 3: ensure this install owns exactly one devices row. The row is
// pure session-list telemetry — it does not carry any cryptographic // pure session-list telemetry — it does not carry any cryptographic
// material since the per-user-key refactor. We re-use the row across // material since the per-user-key refactor. We re-use the row across