RORK LABEN
APPLE-AI — Appleが初回DL 200万未満の開発者にFoundation Modelsを無償開放。個人開発アプリへのAI組み込みコストが大幅に低下SWIFT-API — Foundation Modelsのサーバーサイド統合で、ClaudeやGeminiを同一Swift APIから呼び出し可能に。画像入力にも対応KOTLIN-MIGRATION — Android Studioの移行エージェントがReact Native製アプリをネイティブKotlinへ自動移行。Rork生成アプリの将来の選択肢にRORK-MAX — Rork MaxはネイティブSwiftコードを生成(月$200)。iPhone・iPad・Watch・TV・Vision Pro・iMessageまで対応SIMULATOR — ブラウザベースのストリーミングiOSシミュレータで、XcodeやMacなしに実機相当のApple環境で検証可能SWIFTUI — WWDC 2026でSwiftUIが進化。並べ替え可能コンテナ・任意コンテナのスワイプアクション・最大2倍速のレイアウトAPPLE-AI — Appleが初回DL 200万未満の開発者にFoundation Modelsを無償開放。個人開発アプリへのAI組み込みコストが大幅に低下SWIFT-API — Foundation Modelsのサーバーサイド統合で、ClaudeやGeminiを同一Swift APIから呼び出し可能に。画像入力にも対応KOTLIN-MIGRATION — Android Studioの移行エージェントがReact Native製アプリをネイティブKotlinへ自動移行。Rork生成アプリの将来の選択肢にRORK-MAX — Rork MaxはネイティブSwiftコードを生成(月$200)。iPhone・iPad・Watch・TV・Vision Pro・iMessageまで対応SIMULATOR — ブラウザベースのストリーミングiOSシミュレータで、XcodeやMacなしに実機相当のApple環境で検証可能SWIFTUI — WWDC 2026でSwiftUIが進化。並べ替え可能コンテナ・任意コンテナのスワイプアクション・最大2倍速のレイアウト
記事一覧/開発ツール
開発ツール/2026-06-12中級

Rork 製アプリの『書類とデータ』が数 GB に膨らむときの原因と対処 — expo-image のディスクキャッシュ管理

アプリ本体は小さいのに『書類とデータ』だけが数 GB に膨らむ——壁紙アプリの運用で実際に踏んだ expo-image のディスクキャッシュ肥大を、cachePolicy の使い分け・縮小版配信・世代別クリアの三段構えで解決した記録です。

Rork379expo-image3キャッシュ2ストレージ2トラブルシューティング72React Native152

空き容量の警告が出た自分の 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 の見直しから順に試していただければと思います。同じ問題に向き合っている方の参考になれば幸いです。

参考リンク

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-05-25
Rork で targetSdkVersion 35 にしたら Android のレイアウトが下まで埋まる問題の直し方
Android 15 (API 35) で強制されるエッジ・トゥ・エッジ表示によって、Rork で生成したアプリの下部タブやヘッダーがシステムバーに食い込む問題を、react-native-edge-to-edge と useSafeAreaInsets を使って実装パターン別に解決します。
開発ツール2026-05-22
FlatList の onEndReached が連続発火して API を叩きすぎる問題の原因と対処
FlatList の onEndReached が画面遷移時や初期描画時に何度も呼ばれて、ページネーション API を多重に叩いてしまう問題。Rork で生成したリスト画面でも頻発するこの挙動の原因と、実運用で使える対処パターンを整理しました。
開発ツール2026-05-19
Rork で AsyncStorage に大容量データを保存すると Android で CursorWindow エラーが出る原因と対処
Rork(React Native)で AsyncStorage に大きな JSON や画像メタデータを保存すると Android で発生する Row too big to fit into CursorWindow エラーの原因と、MMKV・SQLite・分割保存・圧縮など複数の現実的な対処をまとめています。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →