Caching Strategies for Expo Apps: A Practical Guide to Speed
Master caching strategies for Expo and React Native apps. Learn client, edge, and database techniques to build faster, cheaper, and more reliable mobile

Your Expo app works perfectly on a strong Wi-Fi connection. Then a user opens it on a train, taps into a screen, waits through a spinner, leaves, and returns to the same screen. The app fetches the same data again. That repeated request adds another round trip, another origin query, and another opportunity for the cellular radio to wake up.
The fix isn't one cache added at the end of development. In an AppLighter-style architecture, caching strategies need to work across React Native on the device, Hono at the edge, and Supabase over PostgreSQL. Each layer has a different job. Client caching makes navigation feel immediate, edge caching keeps reusable responses close to users, and database-aware caching protects the origin from repeated work.
A hand holds a smartphone displaying a loading icon against a blurred green tree background.
Table of Contents
- Why Caching Matters for Mobile-First Apps
- Understanding Caching Fundamentals
- Client-Side Caching for React Native
- HTTP Headers and Edge Caching with Hono
- Database and Query Caching with Supabase
- Cache Invalidation Patterns and Mistakes
- Measuring Cache Performance and Hit Rates
- Building a Production-Ready Caching Stack
Why Caching Matters for Mobile-First Apps
A loading spinner is often a symptom of an architectural decision, not just a slow endpoint. If a user opens a dashboard, switches tabs, and returns to the dashboard, a naïve Expo app may perform the same request each time. Supabase then processes repeated reads, Hono waits for the database, and the device waits for the full network path before it can render useful content.
On mobile, that cost appears in several places at once. A request consumes bandwidth, extends the time before useful content appears, and can require the cellular radio to wake up. Mobile-app caching research separates response caching from object caching, and finds that caching and prefetching work best when they mask network bottlenecks rather than compute bottlenecks, as described in the mobile caching study.
Start with the data users can safely reuse
The first production question isn't “How do I cache this endpoint?” It's “Who can reuse this response, and for how long?”
A product catalog, public configuration, or immutable image can often remain available locally or at an edge point of presence. A signed-in user's notification count needs a shorter freshness window or explicit refresh behavior. A profile mutation needs targeted invalidation, because showing an old name after the user just changed it feels broken even if the rest of the screen is fast.
A useful layered split looks like this:
- Device layer: Reuse data during navigation and offline gaps.
- Edge layer: Reuse public responses across requests and locations.
- Origin layer: Reduce expensive Supabase queries and coordinate freshness.
The result isn't “cache everything.” It's fewer unnecessary requests, less origin work, and a better chance that the user sees meaningful content before the network finishes. The strongest caching strategies reduce waiting without hiding important changes.
Understanding Caching Fundamentals
A screen opens on a train with an unstable connection. The user should still see recently loaded content while the app checks for fresher data. That behavior depends on choosing the right cache layer and accepting the trade-off between access speed and data freshness. In an AppLighter-based Expo + Supabase + Hono stack, the client, edge, and database caches must agree on what can be reused and when it expires.
An in-memory copy is fast to read but disappears when the process ends. Device storage survives navigation and sometimes app restarts, though its data can become stale. An edge copy can serve many users efficiently when the response is safe to share and the cache key identifies the response correctly.
The field has a long history. A 2023 survey of caching research describes early CPU and database support from 1965 to 1990, followed by the expansion of web caching in the mid-1990s. It also identifies LRU, FIFO, and clock-based schemes as foundational strategies proposed and evaluated early in the field.
A diagram comparing client-side and server-side caching on a spectrum between speed and data freshness.
A cache hit rate alone doesn't tell the full mobile story
A cache hit means the requested representation was available under the lookup key. A miss sends the application to a slower source. A revalidated response means the client or intermediary checked whether its copy remained current before using it.
Historical web data provides useful context. A study of six caches in October 1997 recorded hit rates from 16% to 53%, with a mean of about 30%, and noted that even infinite disk space was thought unlikely to push web cache hit rates beyond 50%. The environment has changed, but the lesson remains: traffic patterns, key design, object reuse, and freshness policy shape results. Storage capacity alone does not determine cache performance. See the academic web caching benchmark.
For an Expo app, fewer network requests can improve perceived latency, p99 response time, and battery life even without a perfect hit ratio. Cache immutable assets, responses with stable freshness windows, and data that can be prefetched before navigation. Rapidly changing user state needs explicit freshness rules instead of a better dashboard metric. If cached data makes the interface show an incorrect value, the strategy has failed, regardless of latency.
Client-Side Caching for React Native
React Native gives you several storage choices, and they aren't interchangeable. The right choice depends on whether the data is transient, small and persistent, or large enough to need structured queries.
In-memory state is the fastest option. TanStack Query's memory cache can retain server responses while screens mount and unmount, while Zustand or Redux can hold UI state such as filters, draft values, and navigation-related selections. Memory is ideal for data that can be discarded without harming the user. It disappears when the JavaScript process ends, so it shouldn't be your only store for an offline-capable screen.
AsyncStorage persists small key-value values across launches. It fits preferences, lightweight session metadata, feature flags, and other compact values that don't need relational queries. It isn't a good substitute for a local database holding a growing collection of API rows, because you'll eventually have to serialize, replace, and search larger blobs.
Use a local database when the screen needs real queries
SQLite or WatermelonDB makes sense when your app needs collections, filters, sorting, relationships, or offline mutations. In an Expo project, a local database can hold normalized API records while TanStack Query manages freshness and synchronization around them. Vibecode DB patterns can also provide a local persistence layer with a Supabase adapter, which is useful when the same application needs a structured local model and a hosted backend.
A practical division looks like this:
| Storage | Best fit | Main trade-off |
|---|---|---|
| Memory | Screen data, filters, temporary state | Lost when the process ends |
| AsyncStorage | Preferences and small persistent values | Limited querying and blob management |
| SQLite or WatermelonDB | Offline records and structured collections | More schema and sync complexity |
Optimistic updates belong at the interaction boundary. Update the local representation immediately, send the mutation in the background, then reconcile the server response or roll back on failure. Keep user-specific state under explicit invalidation rules, and give static assets a much longer lifetime than volatile records.
For a detailed decision on query-layer and UI-state responsibilities, compare TanStack Query, Zustand, and Redux for React Native. The key production rule is simple: put server freshness policy in the query or persistence layer, not in scattered component effects.
HTTP Headers and Edge Caching with Hono
A user opens an Expo app on a slow connection, and the same public response is requested repeatedly. If Hono sends explicit cache headers, the client and edge can reuse that representation instead of contacting Supabase for every read. The result is lower p99 latency, fewer origin requests, and less radio time on the device.
At the Hono layer, HTTP headers define the storage and freshness contract. A response can state whether clients or shared intermediaries may store it, how long it stays fresh, and whether a validator should be checked before reuse.
For a public, read-heavy endpoint, set a public policy with a defined freshness period and revalidation support. Personalized data usually needs a private policy or no-store when shared storage could expose it. The rule is direct: cacheability must follow data ownership.
Design the cache key before setting the header
A cache key must contain every input that can change the response. Depending on the endpoint, that may include a resource identifier, locale, API version, or selected query parameter. Exclude irrelevant headers and user-specific values from representations intended for broad reuse.
A narrow, correct key improves reuse. A careless key can leak one user's response to another or split a reusable response into many low-hit-rate entries. Hono is a practical control point because the route already knows whether it serves public content, tenant-scoped content, or authenticated state.
Set policy by response type:
- Public resources: Cache at the edge when the representation is safe to share.
- Semi-static responses: Use a controlled freshness window with revalidation.
- Personalized responses: Keep them private, close to the origin, or uncached.
- Mutation endpoints: Treat writes separately from cacheable reads, and purge affected representations when necessary.
Reusable objects can return far faster from a nearby edge point of presence than from the origin, but the gain depends on geography, request reuse, and key correctness. It is an optimization to verify with p99 measurements, not a guarantee for every route. These edge caching and API gateway guidance principles also fit an AppLighter-based Expo, Supabase, and Hono stack, where client, edge, and database policies must agree.
For an example of structuring a production mobile backend and API surface, see this booking source code guide. If the Expo app also runs on the web, Workbox-style service-worker patterns can reuse browser requests, but they do not replace native persistence or server-side authorization.
Database and Query Caching with Supabase
Supabase gives an Expo application a PostgreSQL-backed origin, but the database shouldn't have to answer every identical read. The most practical pattern is cache-aside caching at the application or edge layer. Hono checks a key, queries Supabase on a miss, returns the result, and stores a representation for later requests.
This approach keeps cache knowledge in your API rather than coupling a generic cache directly to database internals. It also makes the policy visible in code. You can choose a short-lived cache for an expensive aggregate that changes occasionally, while leaving sensitive, personalized queries uncached or protected by precise keys.
A modern server rack in a data center displaying a database status monitor screen.
Protect the origin during misses
The difficult moment is often not the hit. It's the coordinated miss after an entry expires or a purge arrives. If many clients request the same key at once, each request may reach Supabase before any one request repopulates the cache. That creates a thundering-herd effect and can overload the exact query the cache was meant to protect.
Use request coalescing or a short refresh lock for hot keys. Let one request rebuild the value while others wait briefly or receive a stale-but-allowed response. Add jitter to refresh timing so related entries don't all expire simultaneously, and use backoff when the origin is already under pressure.
Practical rule: A cache miss path needs its own design. Treat it as production traffic, not an exceptional branch.
Database triggers or webhook events can publish precise invalidation signals after a mutation. A profile update can invalidate the profile representation and related user-specific keys, while leaving unrelated public configuration untouched. This is safer than flushing every edge entry whenever one row changes.
The distributed caching survey frames cache placement, object selection, and operation as continuing open problems, while practical distributed systems guidance increasingly favors event-based, tag-based, and versioned invalidation over blanket purges. For Supabase-backed apps, the best architecture usually combines a modest query cache with explicit mutation events, rather than trying to make TTL carry the entire consistency burden.
Cache Invalidation Patterns and Mistakes
TTL is a useful safety net, but it isn't an invalidation strategy by itself. A single expiration value treats a public settings response, a product description, a user profile, and a notification count as if they changed at the same rate. They don't.
The worst mistake is applying a global purge when one record changes. That removes useful entries for unrelated users and endpoints, forces a burst of misses, and may increase origin load precisely when the system is processing a mutation.
Build keys around ownership and dependencies
Design keys so the application can invalidate the smallest safe unit. A profile response might be keyed by user identity and representation version. A collection response may need tags for the collection and the entities it contains. A versioned key can make a broad content change easy to roll forward without deleting every old object synchronously.
Useful patterns include:
- Entity keys: Invalidate the specific record after an update.
- Tag keys: Remove entries associated with a product, user, tenant, or collection.
- Versioned keys: Change the version when the representation changes, allowing old entries to age out.
- Event-driven purges: Emit invalidation from the mutation path, database trigger, or webhook.
Freshness isn't only about correctness. It affects the user experience directly. If a user edits a profile and the app continues reading a stale cached representation, the interface contradicts the user's action. If the app invalidates too broadly, unrelated screens lose their fast path.
For user-specific state, event-based invalidation should be the default. Use TTL as a fallback for missed events, not as permission to ignore mutation relationships. Precise invalidation preserves reuse while limiting stale data, which is the balance a multi-layer Expo stack needs.
Measuring Cache Performance and Hit Rates
Adding Cache-Control does not demonstrate a measurable win. A cache may show a strong hit rate while MISS requests still create unacceptable tail latency. It may also improve the median while leaving a smaller group of users waiting on a slow origin.
Measure each path separately across the complete Expo, Hono, and Supabase stack. Track p50, p95, and p99 latency for HIT, MISS, and REVALIDATED requests. This separates fast cache delivery from origin cost and shows whether freshness checks add delay.
Instrument the journey from device to database
In Expo, record request counts, cache reads and writes, stale responses, failed revalidations, and synchronization results. In Hono, add cache status to structured logs. At the Supabase boundary, correlate misses with query duration, connection pressure, and database load.
A diagnostic table can expose the next investigation:
| Traffic path | What it reveals | Action when it degrades |
|---|---|---|
| HIT | Cache delivery speed | Check edge location and serialization |
| MISS | Origin and database cost | Inspect keys, query plans, and coalescing |
| REVALIDATED | Freshness overhead | Review validators and update frequency |
Break results down by endpoint and response class instead of reporting one application-wide rate. A public catalog and a personalized account response have different reuse and privacy requirements. If a reusable endpoint records frequent misses, inspect key variation before adding storage. If MISS p99 rises, investigate the origin path rather than relying on a better median.
Battery and bandwidth need separate measurements. Client caching can reduce repeated fetches and radio activity, provided the app avoids unnecessary background refetches and invalidation on every screen focus. Compare those metrics with navigation frequency and offline recovery to distinguish useful refreshes from request churn.
For an AppLighter-based Expo architecture, the AppLighter app performance monitoring guide offers a practical starting point for observing app behavior. A caching strategy earns its place when HIT, MISS, REVALIDATED, p99, origin load, and device behavior support the same conclusion.
Building a Production-Ready Caching Stack
A sluggish MVP usually doesn't need a complicated distributed cache on day one. It needs clear ownership at each layer.
Start in Expo with memory-backed server-state caching and persistence for data that users reasonably expect to survive navigation or a restart. Prefetch the next screen's stable data when the current interaction makes that destination likely, but don't prefetch every possible route. Keep optimistic updates limited to mutations where rollback and reconciliation are well-defined.
At Hono, classify endpoints before adding headers. Public and read-heavy responses can use edge caching with carefully designed keys. Authenticated responses need private handling, explicit variation, and conservative reuse. Add cache status and latency fields to logs from the first deployment, not after a performance incident.
Supabase remains the source of truth. Cache expensive reads through a controlled application layer, then connect mutations to precise invalidation events. Add miss coalescing and refresh jitter for hot data, and make stale fallback behavior deliberate rather than accidental.
AppLighter packages an Expo and React Native foundation with Vibecode DB, a Supabase adapter, Hono and TypeScript API patterns, authentication, navigation, state management, and AI-assisted development tooling. That makes it one practical starting point for wiring these layers together, while the cache policy still belongs to the application's data model and freshness requirements.
A production cache is an operational system. It needs ownership, observability, and a failure path.
Review the stack whenever the product changes. A new personalization dimension may invalidate an edge policy. A new realtime feature may make a long TTL inappropriate. A new offline workflow may require a local database instead of a memory cache. The durable pattern is layered: reuse safely on the device, share carefully at the edge, and protect Supabase with precise origin-aware invalidation.
AppLighter gives you a structured Expo foundation with Supabase-backed data and an edge-ready Hono API layer, so you can implement these caching strategies without assembling every integration from scratch. Visit AppLighter to start with the mobile, database, and API pieces already organized for production development.