Skip to content

Performance Optimizations for Auth & Profile Loading โ€‹

Date: January 7, 2026 Issue: Page refresh and login taking significant time, features appearing unavailable during load

Root Causes โ€‹

  1. Synchronous getCurrentUser() blocking: The app waited for Firebase's onAuthStateChanged callback to resolve before starting to load anything
  2. Sequential profile loading: App tried to load and decrypt the entire profile at once, requiring encryption key availability
  3. No encryption key caching optimization: Key derivation was done fresh on every login without avoiding duplicate work
  4. No loading states: Features appeared unavailable while waiting for async operations

Solutions Implemented โ€‹

1. Persistent Auth State Listener (App.jsx) โ€‹

Before:

javascript
// Blocks app initialization, waits for promise
async function loadUser() {
  const user = await getCurrentUser()
  // ... only after this completes
}

After:

javascript
// Sets up listener immediately, fires when auth state is ready
useEffect(() => {
  const unsubscribe = onAuthChange(async (user) => {
    setCurrentUser(user)
    // Profile loading starts in parallel
  })
}, [])

Impact: User restored immediately from Firebase's local cache on page refresh, no waiting for promise resolution.

2. Split Public/Encrypted Profile Loading (profileService.js) โ€‹

Before: Single getUserProfile() call that tried to decrypt everything at once, failed if encryption key not available.

After: Three-phase approach:

javascript
// Phase 1: Load public data only (FAST - no decryption)
const publicProfile = await getPublicProfile(userId)
setUserProfile(publicProfile)

// Phase 2: Check if encryption key is available
if (hasEncryptionKey()) {
  // Phase 3: Decrypt sensitive fields (birthDate) in background
  const fullProfile = await decryptProfileFields(publicProfile)
  setUserProfile(fullProfile)
}

Impact:

  • Dashboard renders immediately with public data (lantern name, interests, mood, location preference)
  • Encrypted fields (birth date) load as soon as encryption key is available
  • No "loading indefinitely" experience

3. Smarter Key Derivation (encryption.js) โ€‹

Before: Each login call to unlockEncryption() independently derived the key, even if multiple requests happened simultaneously.

After:

javascript
// If key derivation is already in progress, wait for it instead of duplicating
if (keyDerivationUserId === userId && keyDerivationPromise) {
  return keyDerivationPromise
}

Impact: Avoids expensive PBKDF2 key derivation duplicates when multiple components request encryption on login.

New exports:

  • hasEncryptionKey(): Check if key is currently cached
  • getCachedUserId(): Get the user ID for cached key

4. Improved Login Flow (LoginFlow.jsx โ†’ App.jsx) โ€‹

After login, the auth state listener automatically:

  1. Detects new user auth state
  2. Loads public profile immediately
  3. Decrypts fields when key is ready

No need to manually reload profile in login handler.

Technical Details โ€‹

Encryption Key Derivation Cost โ€‹

PBKDF2 with 600,000 iterations (OWASP 2023 standard) is intentionally expensive:

  • Local machine: ~100-200ms
  • Mobile device: ~500-1000ms
  • On refresh: Key was being re-derived every time

Optimization: Key is now cached in memory for the session and only re-derived on actual login.

Public vs Encrypted Data โ€‹

Public (fast):

  • Email (not shown in UI for privacy)
  • Lantern Name
  • Interests
  • Mood
  • Location Tracking preference
  • Salt (needed for key derivation)

Encrypted (requires key):

  • Birth date

Testing Checklist โ€‹

  • [ ] Local dev: Page refresh shows profile instantly
  • [ ] Local dev: Features available immediately without "loading" state
  • [ ] Dev site (dev.ourlantern.app): Mobile load time significantly reduced
  • [ ] Login flow: Profile visible immediately after successful login
  • [ ] Login flow: Birth date decrypts once encryption is unlocked
  • [ ] Profile settings: Can edit profile without re-login
  • [ ] Logout: Encryption key properly cleared
  • [ ] Subsequent login: New key derived and cached

Browser DevTools Inspection โ€‹

To verify optimization is working:

  1. Network tab:

    • /users/{uid} Firestore fetch should be fast (~50-200ms)
    • Key derivation happens in JS, not network
  2. Console logs (dev mode):

    • "๐Ÿ”‘ Auth state changed" appears immediately on refresh
    • "๐Ÿ“Š Public profile loaded" appears without "encryption key not cached" warning
    • "๐Ÿ”“ Birth date decrypted" appears after login
  3. Performance tab:

    • Main thread should be responsive within 100-300ms on local
    • On mobile/dev site, should see clear improvement vs before

Future Optimizations โ€‹

  1. Service Worker offline support: Cache public profile in IndexedDB
  2. Firestore persistence: Already enabled - provides instant access to previously loaded data
  3. Lazy encryption: Decrypt birth date only when needed (profile view, not dashboard)
  4. Skeleton screens: Show placeholder UI while public profile loads (optional but improves perceived performance)

Built with VitePress