綴Photo で、「写真なし月を隠す」というボタンがあります。1年分の写真をスキャンして、その月に写真がない月を一覧から消す機能。
これがそこそこ遅かった。実装は AI に書いてもらっているので、レビューで上がってきたコードを後から眺めていて「OS への問い合わせを12回やってる」のに気づいた。直してみたら、構造としては 性能改善の典型パターン に当てはまる作業だった。記録しておく。
旧実装: 月ごとに12回スキャン
元のコードはこんな感じだった。
for (var m = 1; m <= 12; m++) {
final hasPhotos = await hasAnyPhotosInYearMonth(year: year, month: m);
if (!hasPhotos) {
result.add((year, m));
}
}1月、2月、3月…と12回ループして、各月ごとに「この月に写真ある?」を判定。素直で読みやすい。
問題は hasAnyPhotosInYearMonth の中身。この関数は呼ばれるたびに「OS から写真アルバムを取得して、新しい順にめくって、対象月の写真を探す」という流れ。
中身を雑に書くとこう:
Future<bool> hasAnyPhotosInYearMonth(int year, int month) async {
final album = await PhotoManager.getAssetPathList(...); // ← ここが重い
// album から指定月の写真を探す
}PhotoManager.getAssetPathList という、OS とのやりとりが必要な重い処理 が毎回走ります。これを12回繰り返してた。
なぜ遅いか
OS への問い合わせ(写真ライブラリへのアクセス)は、コードの中の他の処理と比べて 桁が違うレベルで遅いです。配列をループする処理が数マイクロ秒で終わるところを、OS への問い合わせは数十〜数百ミリ秒かかります。
それを12回繰り返すと、ボタン1個押しただけで秒単位の待ち時間が生まれます。写真の枚数が多い端末ほど顕著になります。
新実装: 年単位で1回スキャン
直したコード。
final photoMonths = await scanPhotoMonthsInYear(year); // 年単位で1回だけ
for (var m = 1; m <= 12; m++) {
if (!photoMonths.contains(m)) {
result.add((year, m));
}
}scanPhotoMonthsInYear(year) の中身は、OS への問い合わせを1回だけ にして、その中で「何月に写真があるか」を集めます。
Future<Set<int>> scanPhotoMonthsInYear(int year) async {
final album = await PhotoManager.getAssetPathList(...); // ← 1回だけ
final months = <int>{};
// album から年内の写真を順番にめくって、撮影月を集める
for (final asset in assets) {
months.add(asset.createDateTime.month);
}
return months; // {1, 3, 5, ...} みたいな「写真がある月の集合」
}構造の入れ替えがしてること
これは性能改善の典型パターンで、特に「ネットワーク」「ファイルIO」「OS API」のように コードの外との通信が伴う重い処理 で効きます。
「重い処理を減らすために、軽い処理(メモリ上の計算)でカバーする」という発想。プログラミングをやってると、いろんな場面でこの構造に出会います。
おまけ: EXIF 欠落への耐性も入れた
ついでに副次的な改善も入れた。エミュレーターで動作確認してた時に気づいた罠で、
- 写真の撮影日時(EXIF)が欠落していると、
createDateTimeが1970-01-01にフォールバックします - これを「日付が分かる写真」として扱うと、月の集計や最古判定が壊れます
なので、新しい scanPhotoMonthsInYear には「1990年より前の日付は信用しない」という閾値を入れた。エミュレーターの写真は全部 EXIF なしで、これがないと判定が全滅します。
実機なら大半の写真に EXIF があるので、ほぼ起きない問題。でも「1枚でも EXIF 欠落写真が混ざってると判定が汚れる」リスクは現実にあるので、保険として入れておく。
学んだこと
- 「ボタン1個押すと裏で OS に12回問い合わせてた」みたいなのはよくあります。素直に書くとそうなります。性能を測ってから「これ多すぎない?」と気づいて初めて見えます
- 構造の入れ替えで桁レベルで速くなります。「12回」と「1回」の差は、コードの行数では小さいけど、体感は桁違い
- 「重い処理は外、軽い処理は中」が原則。OS / ネットワーク / DB はコードの「外」で、これらの呼び出し回数を意識すると性能の天井が変わります
- 副次的な耐性追加はついでにやります。同じ関数に手を入れるなら、EXIF 欠落みたいな「気になってた罠」も一緒に塞いでおきます
おわりに
性能改善は「測ってから直す」が鉄則だけど、今回は「コード見て構造として変だな」で気づいた。OS への問い合わせを12回繰り返してたら、それは構造として赤信号。
「重い処理の回数」を意識するのは、コードの 横の長さ(行数) じゃなくて 縦の深さ(外との通信の頻度) を見る視点。性能を見るときはここを数えるようにしています。
この記事は2026年5月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。