RORK LABJP
APPLE-AI — Apple opens Foundation Models free to developers under 2M first-time downloads, slashing the cost of adding AI to indie appsSWIFT-API — Foundation Models server-side integration lets you call Claude and Gemini through the same Swift API, now with image inputKOTLIN-MIGRATION — Android Studio's migration agent converts React Native apps into native Kotlin automatically — a future path for Rork-built appsRORK-MAX — Rork Max generates native Swift code ($200/mo), covering iPhone, iPad, Watch, TV, Vision Pro, and iMessageSIMULATOR — A browser-based streaming iOS simulator lets you test on a real Apple environment without Xcode or Mac hardwareSWIFTUI — SwiftUI evolves at WWDC 2026 with reorderable containers, swipe actions for any container, and layouts up to 2x fasterAPPLE-AI — Apple opens Foundation Models free to developers under 2M first-time downloads, slashing the cost of adding AI to indie appsSWIFT-API — Foundation Models server-side integration lets you call Claude and Gemini through the same Swift API, now with image inputKOTLIN-MIGRATION — Android Studio's migration agent converts React Native apps into native Kotlin automatically — a future path for Rork-built appsRORK-MAX — Rork Max generates native Swift code ($200/mo), covering iPhone, iPad, Watch, TV, Vision Pro, and iMessageSIMULATOR — A browser-based streaming iOS simulator lets you test on a real Apple environment without Xcode or Mac hardwareSWIFTUI — SwiftUI evolves at WWDC 2026 with reorderable containers, swipe actions for any container, and layouts up to 2x faster
Articles/Dev Tools
Dev Tools/2026-06-12Intermediate

Your Rork App's 'Documents & Data' Keeps Growing — Taming expo-image's Disk Cache

My wallpaper app's binary was 40 MB, yet 'Documents & Data' had ballooned to 2.4 GB. Here is how I diagnosed expo-image's unbounded disk cache and fixed it with cachePolicy tuning, thumbnail URLs, and generational cache clearing.

Rork380expo-image3cachingstorage2troubleshooting66React Native152

It started with a low-storage warning on my own iPhone. I opened Settings → General → iPhone Storage, and near the top of the list sat one of the wallpaper apps I run as an indie developer. The app binary itself was around 40 MB. "Documents & Data," however, had grown to 2.4 GB.

That number stung. I had already moved every image out of the bundle and onto remote delivery to keep the binary small — and yet the app was quietly eating users' storage from the other side. The culprit turned out to be expo-image's disk cache.

Since I ship the same architecture across several apps on both the App Store and Google Play, I want to walk through the symptoms, how to reproduce the problem, and the three-layer fix that worked for me.

Where the symptom shows up — app size vs. Documents & Data

There are two places to check first:

  • iOS: Settings → General → iPhone Storage → your app. The screen separates "App Size" from "Documents & Data"
  • Android: Settings → Apps → your app → Storage & cache. The growth lands in the "Cache" bucket

"App Size" is the binary plus bundled assets — the part you control at build time. "Documents & Data" is everything the app writes at runtime, and downloaded image caches live there.

In other words, no matter how aggressively you trim your binary, an unbounded runtime cache means the storage footprint users actually see keeps climbing.

If you are lucky, someone leaves a review saying the app "eats storage." In my experience, most users simply uninstall without a word. A bloated footprint also makes your app a prime candidate when iOS suggests offloading unused apps under storage pressure.

Reproduction and root cause — the disk cache only grows

expo-image's cachePolicy defaults to memory-disk. Every image you display gets cached in memory and on disk, which makes the next render fast. As a loading-speed optimization this design is excellent — I wrote about that side of it in Slow Images in My Rork Wallpaper App: Switching to expo-image and What Actually Changed.

The problem is that there is no public API to cap the disk cache size from your app. The cache implementation is delegated to SDWebImage on iOS and Glide on Android, so eviction timing and limits depend on each library's defaults and on OS behavior.

A wallpaper app hits this weakness head-on:

  • Each full-resolution image weighs several megabytes
  • Users scroll through hundreds of items in the grid
  • Previews open full-resolution images one after another

You can reproduce the bloat on a real device by scrolling through roughly 300 grid items, opening 30 previews, and then checking the Settings app — that alone adds hundreds of megabytes. Your most loyal daily users are exactly the ones whose cache grows into gigabytes.

iOS does reserve the right to purge the Caches directory under storage pressure, but in my observation it rarely happens before the user has already noticed the problem. I recommend treating cache management as your app's job, not the OS's.

Fix 1 — split cachePolicy by screen role

The first change that paid off was giving thumbnails and full-resolution previews different caching strategies.

// components/WallpaperThumbnail.tsx — grid thumbnail
import { Image } from 'expo-image';
 
type Props = { thumbUrl: string };
 
export function WallpaperThumbnail({ thumbUrl }: Props) {
  return (
    <Image
      source={{ uri: thumbUrl }}
      style={{ width: '100%', aspectRatio: 9 / 16 }}
      contentFit="cover"
      recyclingKey={thumbUrl}
      transition={150}
      cachePolicy="memory-disk" // grids get revisited, so keep these on disk
    />
  );
}
// screens/WallpaperPreview.tsx — full-resolution preview
import { Image } from 'expo-image';
import { StyleSheet } from 'react-native';
 
export function WallpaperPreview({ fullUrl }: { fullUrl: string }) {
  return (
    <Image
      source={{ uri: fullUrl }}
      style={StyleSheet.absoluteFill}
      contentFit="contain"
      cachePolicy="memory" // never write full-res images to disk
    />
  );
}

The decision rule is simple: will this exact image likely be displayed again soon? Grid thumbnails get re-rendered constantly, so the disk cache earns its keep. A full-resolution preview is closer to a one-time view; keeping it on disk rarely pays off. Better to hold it in memory only and leave the disk clean.

This single change visibly slowed the growth of Documents & Data, because one full-resolution image weighs as much as dozens of thumbnails.

Fix 2 — shrink the bytes you write in the first place

Tuning cachePolicy controls where bytes go; you can also reduce how many bytes there are. Serve a downscaled thumbnail URL for grids, and fetch full resolution only when it is genuinely needed.

When I moved my images to remote delivery, I set up two URL variants per image: one for thumbnails and one for actually setting the wallpaper. I covered the background in Keeping a wallpaper app's binary small: moving images out of the bundle, and the same two-tier setup turns out to matter for cache bloat too. If a thumbnail is 100 KB, scrolling 300 items writes about 30 MB to disk — a tiny fraction of what full-resolution images in the grid would cost.

Apps freshly generated by Rork often reuse one URL for both the grid and the preview, so this is worth checking before anything else.

Fix 3 — generational clearing plus a "Clear cache" button

Even with both fixes, caches accumulate over months of use. As the last line of defense I added two things.

// lib/cache-maintenance.ts — reset the disk cache every 30 days
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Image } from 'expo-image';
 
const KEY = 'imageCacheClearedAt';
const INTERVAL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
 
export async function maintainImageCache(): Promise<void> {
  const raw = await AsyncStorage.getItem(KEY);
  const lastCleared = raw ? Number(raw) : 0;
 
  if (Date.now() - lastCleared < INTERVAL_MS) return;
 
  await Image.clearDiskCache();
  await AsyncStorage.setItem(KEY, String(Date.now()));
}

Call this on app launch — a useEffect in your root layout works fine. It also fires on the very first launch, but the cache is empty at that point, so nothing is lost.

Then give users a manual escape hatch in the settings screen:

// handler for the "Clear image cache" button in settings
import { Alert } from 'react-native';
import { Image } from 'expo-image';
 
export async function onClearImageCache(): Promise<void> {
  await Image.clearMemoryCache();
  await Image.clearDiskCache();
  Alert.alert('Done', 'Image cache cleared.');
}

Having an in-app way to clear the cache matters more on iOS than you might expect. Android users can clear an app's cache from system settings, but iOS offers no standard way short of deleting the app entirely. If someone is about to remove your app over storage, I would much rather they clear the cache and stay.

The 30-day interval comes from my own usage data: even heavy users accumulate only a few hundred megabytes of browsing cache per month in my apps. If your content rotates faster, a shorter interval makes sense.

Prevention — check Documents & Data before every release

What makes this issue sneaky is that development workflows hide it. Dev builds get reinstalled constantly, so the cache never matures, and nobody watches free space on a simulator.

My pre-release checklist now includes:

  • On a real device, scroll about 300 grid items and open 30 previews, then check Documents & Data in the Settings app
  • If the number far exceeds the expected total (thumbnails plus a margin), audit cachePolicy to see whether full-resolution images are being written to disk

It is an unglamorous ten minutes, but far cheaper than reading about it in a store review.

As a next step, install your current build on a real device and look at Documents & Data once. If the number surprises you, start with Fix 1 and work down the list. I hope this saves you the gigabytes it cost me to learn.

References

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-24
Reanimated Worklet Errors in Rork Apps — Six Things to Check Before You Panic
React Native Reanimated throws a lot of worklet errors the moment you add it to a Rork project. This walkthrough covers the six most common causes, from a missing Babel plugin to closure capture bugs, in the order you should investigate them.
Dev Tools2026-04-23
State Updated in Rork But UI Won't Re-render? Five Patterns and Fixes
setState is firing in your Rork-generated code but the UI refuses to update. Five common root causes — mutation, stale closures, Zustand selectors, missed dependencies, unmounted updates — each with Before/After code.
Dev Tools2026-04-20
Rork App Data Not Saving or Disappearing: Causes and Fixes
When Rork app data isn't saving or disappears after a restart, a handful of root causes explain most cases. This guide covers AsyncStorage pitfalls, async timing bugs, key mismatches, and when to switch to MMKV.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →