空き容量の警告が出た自分の iPhone で、設定アプリの「iPhoneストレージ」を開いたときのことです。リストの上位に、私が運用している壁紙アプリが並んでいました。アプリ本体のサイズは 40 MB 台。それなのに「書類とデータ」が 2.4 GB に達していました。
画像はバンドルから切り離し、リモート配信に移行済み。バイナリは軽くなったはずなのに、ユーザーの端末側でストレージを圧迫していたのでは本末転倒です。調べていくと、原因は expo-image のディスクキャッシュでした。
同じ構成のアプリを App Store と Google Play で複数運用している個人開発の立場から、この問題の症状・再現条件・対処を整理してお伝えします。
症状はどこに現れるか — 「アプリのサイズ」と「書類とデータ」の違い
最初に確認する場所は次の2つです。
- iOS: 設定 → 一般 → iPhoneストレージ → 該当アプリ。「アプリのサイズ」と「書類とデータ」が分かれて表示されます
- Android: 設定 → アプリ → 該当アプリ → ストレージとキャッシュ。「キャッシュ」の区分に積み上がります
「アプリのサイズ」はバイナリとアセットの容量で、開発者がビルド時に制御できる領域です。一方の「書類とデータ」は、アプリが実行時に書き込んだファイルの合計。ダウンロードした画像のキャッシュはこちらに含まれます。
つまり、バイナリサイズをどれだけ削っても、実行時のキャッシュが無制限に貯まれば、ユーザーから見た「このアプリが占有している容量」は膨らみ続けます。
レビュー欄に「容量を食う」と書かれる前に気づければまだ良い方で、実際には何も言わずにアンインストールされるケースの方が多い、というのが私の実感です。ストレージ逼迫時に iOS が提示する「非使用のアプリを取り除く」候補にも入りやすくなります。
再現条件と原因 — expo-image のディスクキャッシュは貯まる一方
expo-image の cachePolicy は、既定で memory-disk です。一度表示した画像はメモリとディスクの両方にキャッシュされ、次回の表示が高速になります。読み込み体験の改善という意味では、壁紙アプリの画像が遅かった:Rork で expo-image に切り替えて分かったことに書いた通り、この仕組み自体は優秀です。
問題は、ディスク側のキャッシュにアプリ側からサイズ上限を設定する公開 API がないことです。キャッシュの実体は iOS が SDWebImage、Android が Glide に委ねられており、削除のタイミングや上限は各ライブラリの既定値と OS の挙動に依存します。
壁紙アプリの利用条件は、この弱点を直撃します。
- 1枚あたり数 MB のフル解像度画像を扱う
- ユーザーは一覧を何百枚もスクロールする
- プレビューでフル解像度を次々に開く
実機で一覧を 300 枚ほどスクロールし、プレビューを 30 枚開いてから設定アプリを確認する——これだけで数百 MB 単位の増加が再現できます。毎日使ってくれるユーザーほど、キャッシュが GB 級に育っていきます。
なお「iOS の Caches ディレクトリはストレージ逼迫時にシステムが削除する場合がある」という仕様はありますが、私の観測では、ユーザーが容量不足を自覚する頃まで放置されることがほとんどでした。OS 任せにせず、アプリ側で管理する前提に立つことをおすすめします。
対処 1 — 画面の役割で cachePolicy を分ける
最初に効いたのは、サムネイル一覧とフル解像度プレビューでキャッシュ方針を分けることでした。
// components/WallpaperThumbnail.tsx — 一覧のサムネイル
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" // 一覧は再訪が多いのでディスクに残す
/>
);
}// screens/WallpaperPreview.tsx — フル解像度のプレビュー
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" // フル解像度はディスクに書かない
/>
);
}判断基準はシンプルで、「同じ画像を近いうちにもう一度表示する見込みがあるか」です。一覧のサムネイルは何度も再表示されるため、ディスクキャッシュがよく効きます。一方、フル解像度のプレビューは一期一会に近く、ディスクに残しても再利用される確率は低めです。それなら最初から memory に留めて、ディスクを汚さない方が得策と考えました。
この変更だけで、「書類とデータ」の増加ペースは目に見えて落ちました。フル解像度1枚のバイト数は、サムネイルの数十倍あるためです。
対処 2 — ディスクに書くバイト数そのものを減らす
cachePolicy の調整は「書き込む場所」の話ですが、並行して「書き込む量」も減らせます。一覧用には縮小版の画像 URL を配信し、フル解像度は本当に必要な場面でだけ取得する構成です。
私のアプリでは画像をリモート配信に切り替えた際、サムネイル用と壁紙設定用の2系統の URL を用意しました。経緯は壁紙アプリのバイナリサイズを抑える — 画像をバンドルから切り離す設計判断に書いていますが、キャッシュ肥大の観点でもこの2系統化が効きます。サムネイルが 1 枚 100 KB なら、300 枚スクロールしてもディスク書き込みは 30 MB 程度。フル解像度をそのまま一覧に流した場合の数十分の一で済みます。
Rork でアプリを生成した直後の構成は、同じ URL を一覧とプレビューで使い回していることが多いので、最初に確認しておきたいポイントです。
対処 3 — 世代別クリアと「キャッシュを削除」ボタン
それでも長期利用ではキャッシュが積もります。最後の砦として、次の2つを実装しました。
// lib/cache-maintenance.ts — 30日ごとにディスクキャッシュをリセット
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日
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()));
}これをアプリ起動時(ルートレイアウトの useEffect など)に呼びます。初回起動でも一度クリアが走りますが、キャッシュが空の状態なので実害はありません。
あわせて、設定画面に手動の削除ボタンを置きます。
// 設定画面の「画像キャッシュを削除」ボタンのハンドラ
import { Alert } from 'react-native';
import { Image } from 'expo-image';
export async function onClearImageCache(): Promise<void> {
await Image.clearMemoryCache();
await Image.clearDiskCache();
Alert.alert('完了', '画像キャッシュを削除しました。');
}「アプリ側で消す手段を用意しておく」こと自体に意味があります。Android はシステム設定からユーザー自身がキャッシュを削除できますが、iOS には標準で、アプリごと削除する以外の手段が用意されていないためです。容量を理由にアプリを消されるくらいなら、キャッシュだけ消してもらえる導線を作っておく方がずっと良い、という判断でした。
30日という間隔は、私のアプリの利用頻度から決めた値です。「ヘビーユーザーでも1ヶ月分の閲覧キャッシュなら数百 MB に収まる」という実測が根拠なので、画像の更新頻度が高いアプリではもっと短くしても良いと思います。
予防策 — リリース前に「書類とデータ」を見る習慣
この問題の厄介なところは、開発中に気づきにくい点です。開発ビルドは頻繁に入れ直すためキャッシュが育たず、シミュレータでは端末の空き容量を気にする機会がありません。
私はリリース前のチェックに次の項目を入れています。
- 実機で一覧を 300 枚スクロールし、プレビューを 30 枚表示した後、設定アプリで「書類とデータ」を確認する
- 数値が想定(サムネイル合計 + α)を大きく超えていたら、フル解像度がディスクに書かれていないか
cachePolicyを見直す
地味な手順ですが、ストアのレビューで指摘されてから慌てるより、リリース前の 10 分で済ませる方がずっと安上がりです。
まずはお手元のアプリを実機に入れて、設定アプリの「書類とデータ」を一度確認してみてください。想像より大きな数字が出たら、対処 1 の cachePolicy の見直しから順に試していただければと思います。同じ問題に向き合っている方の参考になれば幸いです。