綴Photo では、 端末のカメラロールから「まだ綴っていない写真」を集めてユーザーに見せます。 最初の素直な版で 5000 枚のカメラロールを舐めようとしたら、 普通にアプリが固まった。 そこから「全部読む」をやめて「必要なところで止める」に直していった話だ。
なぜスキャンが要るのか
綴Photo は、ユーザーがアプリ内に写真を取り込む形ではなく、 端末の写真ライブラリ(カメラロール)を直接見にいく作りです。
これは設計判断としてかなり意識的に選んだ。
- 写真を二重保存したくない(容量がもったいない)
- 取り込みのひと手間を省きたい(続けるためのハードルを下げたい)
- 既存の写真資産が、アプリを開いた瞬間にそのまま使える状態にしたい
代わりに、「まだ綴られていない写真はどれか」をアプリ側で計算する必要が出てきます。 ここで「写真ライブラリのスキャン」が登場します。
詰まったところ:全部舐めるとアプリが固まる
最初は素直な作りだった。
1. 端末の全写真を取得する
2. その中から、すでに綴った写真の ID を除外する
3. 残ったものを表示用にソートする
ロジックは合っているし、テスト用の少数写真だと一瞬で動きます。
ところが、自分の端末(写真 5000 枚以上)で動かしたら、 アプリのスプラッシュ後がほぼ凍った。
写真ライブラリへのアクセスは、ファイル名や日時のメタデータだけ取るのも含めて、 意外とコストが高かった。 端末によっては「サムネイル生成」が裏で走って、それで詰まることもあります。
「全部舐めるのは無理」という前提で、設計を組み直した。
解決1:新しい順にスキャンして、cutoff より古くなったら即終了
最初に入れた最適化はこれだった。
ユーザーが綴る写真は、ほとんどの場合「最近の写真」です。 3年前の旅行を毎日掘り起こす人はそんなにいません(綴Photo はそれを否定しませんけど、ベースは「日常を綴る」設計です)。
cutoff という変数を持って、ループの中でこういう判定を入れています。
for (final asset in assetsNewestFirst) {
if (asset.createdAt.isBefore(cutoff)) {
break; // ここで打ち切る
}
if (alreadyTsuzuredIds.contains(asset.id)) continue;
candidates.add(asset);
}このループ、重要なのは break の方で、continue じゃありません。
**「これより古いやつは全部スキップでいい」**と分かっている前提なので、
そこで読み込み自体を止められます。
これだけで、5000 枚舐めていたのが、最近の数百枚だけで済むようになった。 体感がぐんと速くなった。
解決2:ページングで一度に取りすぎない
もう1つ入れたのが、ページング(分割取得)だ。
photo_manager のような写真アクセスのライブラリは、
一度に「全件」を取らずに「N 件ずつ」取る API を持っていることが多いです。
最初は雑に「最大 1000 件」みたいな指定で取っていたんだけど、 それでも端末によっては最初の 1000 件取得で数秒固まります。
この変更で、「ユーザーが画面を開いて最初の写真が表示されるまで」の時間が ぐっと短くなった。
裏では追加スキャンが続いているけど、ユーザーから見ると最初の体験は速いです。 これは個人アプリの世界では結構大事だと思っている。
「すでに綴った写真」をどう持つか
ループの中の alreadyTsuzuredIds.contains(...) は、
「もう綴った写真かどうか」の判定です。
ここを毎回 DB に SQL で問い合わせていたら遅いです。
これで、ループの中の判定は Set.contains で O(1) になります。
いまはスキャンを呼ぶたびに、この Set を DB から作り直すシンプルな作りにしています。
起動時に1回だけ作ってメモリに常駐させ、綴ったら差分で足していく……という持ち方も考えられるけど、
そうすると「DB とメモリ上の状態をズレないように同期し続ける」責任を別途抱えることになります。
そこはまだ手をつけていなくて、改善余地があるなと思っている部分でもある。
この記事で書かなかった話
photo_managerのページング API の具体名- iOS と Android で挙動が違う部分
- バックグラウンドでの追加スキャン戦略
このあたりは実装の話に深く入るので、別記事に分けたい。
学んだこと
- 「全部舐める」発想を捨てます。「必要なところで止める」に切り替える
- 新しい順スキャン + cutoff という発想は、いろんな場面で応用が効く
- ユーザーに見える時間(最初の数秒)を最優先で速くする
- メモリ上の
Setと DB の状態を同期させる設計は、最初からちゃんと考える
5000 枚というのは、自分にとっては「ふつう」の枚数だった。 でも最初に書いてもらった版は、その「ふつう」を相手にできなかった。
テスト用の少数データで動いても、実機の「ふつうの量」で詰まったら意味がありません。 実装を AI に任せていても、実機の「ふつう」で試して詰まりを見つけるのは自分の仕事だ。 個人開発でも、自分の実機で試すフェーズはサボらないようにしたい。
この記事は2026年4月時点の自分の理解と実装方針で書いています。アプリは継続開発中なので、現在の実装と細部が違っている可能性があります。