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:
| Priority | Provider | Method |
|---|---|---|
| 1 | Local Cache | IndexedDB instant lookup |
| 2 | Unavatar (Email) | Social profile resolution |
| 3 | Gravatar | SHA-256 hash of email address |
| 4 | Libravatar | Federated open-source avatar service |
| 5 | Favicon.im | Sender’s domain favicon |
| 6 | Unavatar (Domain) | Company logo lookup |
| 7 | Initials Fallback | Two-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.