Avatars make messaging interfaces feel human. But querying multiple third-party avatar providers introduces performance bottlenecks, race conditions, and memory leaks if not managed carefully.

Here’s how cji.email solves this with Svelte 5 runes ($state / $effect / $derived) on the frontend and a caching pipeline in Rust.

The Fallback Chain: 6 Providers, Zero Placeholder Avatars

A single lookup endpoint isn’t enough. When an avatar is needed, the system queries providers in a strict priority order, falling back automatically on each miss:

PriorityProviderMethod
1Local CacheIndexedDB instant lookup
2Unavatar (Email)Social profile resolution
3GravatarSHA-256 hash of email address
4LibravatarFederated open-source avatar service
5Favicon.imSender’s domain favicon
6Unavatar (Domain)Company logo lookup
7Initials FallbackTwo-letter initials, always available

Preventing Memory Leaks: Blob URL Lifecycle Management

The Rust backend returns avatar images as raw bytes. The frontend converts these to a temporary Blob URL via URL.createObjectURL(). Without explicit cleanup, these accumulate and consume memory indefinitely.

cji.email revokes old Blob URLs in two places:

On email change — when the component’s email prop updates, the previous URL is revoked before generating a new one.

On component destruction — using Svelte’s onDestroy lifecycle hook:

let blobUrls: string[] = [];

onDestroy(() => {
    blobUrls.forEach(url => URL.revokeObjectURL(url));
});

Eliminating Race Conditions with Async Guards

In a busy thread list, the same avatar component might receive several different email addresses in rapid succession as the user scrolls. A network response arriving late for userA@domain.com could overwrite the avatar already showing for userB@domain.com.

The fix is a simple identity guard using Svelte 5’s $derived:

const cleanEmail = $derived(email ? extractEmailAddress(email) : undefined);

async function fetchAvatar(targetEmail: string) {
    const bytes = await invoke('get_avatar_image', { email: targetEmail });

    // Discard stale responses
    if (cleanEmail !== targetEmail) return;

    // Safe to render
    const url = URL.createObjectURL(new Blob([new Uint8Array(bytes)]));
    avatarUrl = url;
}

Privacy by Design: Requests Routed Through Rust

All HTTP requests to avatar providers are made from the Rust backend, not the browser WebView. This means:

  • Your IP address is tied to the desktop app process, not a browser fingerprint.
  • No third-party cookies or tracking scripts can execute.
  • The WebView never directly contacts Gravatar, Libravatar, or Unavatar.

Persistent Caching = Zero Repeat Lookups

Successfully loaded avatars are stored in IndexedDB. On subsequent app launches, avatars render instantly from cache with no network round-trip. The cache is keyed by cleaned email address and provider, enabling fine-grained invalidation.