Skip to content
Back to blog
Whispcal

Making it feel instant

performanceoptimistic-updatestanstack-queryreact-nativeux

There's a moment in every app's life where it goes from "working" to "slow." For WhispCal, that moment came in late January when I noticed a half-second delay between logging a meal and seeing it appear on the home screen. Half a second. Barely perceptible on a benchmark. Completely unacceptable in daily use.

The problem was simple: I was waiting for the server to confirm the save before updating the UI. The solution — optimistic updates — was conceptually simple but mechanically involved.

The TanStack Query migration

On January 5th, I pushed "Tanstack query implementation." This was the foundation for everything that followed. Before TanStack Query, my data fetching was a patchwork of useEffect calls and manual cache management. It worked, but it had no concept of cache invalidation, background refetching, or optimistic mutations.

The migration touched almost every screen in the app. Every API call needed to be wrapped in a query or mutation hook. Every loading state needed to be reconsidered. The payoff was immediate: automatic cache management, request deduplication, and — critically — the infrastructure for optimistic updates.

Optimistic updates: the theory

The idea is simple: when the user performs an action, update the UI immediately as if the server already confirmed it. If the server rejects the change, roll back. In practice, this means:

  1. User taps "log meal"
  2. UI immediately shows the meal as logged
  3. Navigation moves to the home screen
  4. In the background, the actual API call happens
  5. If it fails, show a toast and revert

The February 20th commit captures the shift: "Implement optimistic updates for meal logging, enhancing user experience with immediate navigation and error handling."

When optimism meets reality

Optimistic updates sound great until they don't. The bugs started appearing almost immediately. The commit on February 27th — "fix most optimistic update bugs (if not all)" — has a parenthetical that reveals my confidence level.

The hardest bugs were timing-related. What happens if the user:

  • Logs a meal, navigates to the home screen, then immediately taps on the meal they just logged — before the server confirms it exists?
  • Logs a meal, then switches to a different day before the save completes?
  • Logs two meals in rapid succession, and the first one fails but the second one succeeds?

Each scenario required careful handling of cache state, navigation timing, and error recovery. The "flash of empty items" bug — "Fix empty items flash on navigate back from meal editing" — was particularly nasty. The UI would briefly show an empty state between the optimistic update being applied and the server response being merged.

The loading state dance

Perceived performance isn't just about speed — it's about feedback. On February 21st: "Add a smooth loader when we switch day in the index page." This was a subtle but important change. When swiping between days (a feature added in January), there's a moment where the new day's data hasn't loaded yet. Without a loader, the screen shows stale data from the previous day, then jumps. With a smooth transition, the delay feels intentional rather than broken.

Similarly, haptic feedback — added way back in mid-December — plays a crucial role. A subtle vibration when you tap "log meal" confirms the action registered before the UI even updates. It buys you 100 milliseconds of perceived speed for free.

Toasts over alerts

One of the best UX changes came late in the process: "Add toast - Optimistic update on regular meals - Show toast instead of alert in most places." Native alerts (Alert.alert()) block the entire UI. They're modal, interruptive, and feel slow. Toasts appear at the top of the screen, don't block interaction, and dismiss automatically.

Replacing alerts with toasts throughout the app made everything feel more fluid. Errors became notifications rather than interruptions. Confirmations became acknowledgments rather than gates.

The numbers don't tell the whole story

WhispCal's actual performance didn't change dramatically through this period. The API response times are the same. The database queries are the same. But the app feels twice as fast because the UI never waits for the network.

This is the uncomfortable truth about mobile development: users don't experience your backend latency. They experience the gap between their action and your response. Close that gap — with optimistic updates, with haptics, with smooth transitions — and your app feels instant, regardless of what's happening behind the scenes.