😇
[flutter]hiveに大量のデータを保存すると重くなりすぎて厳しい
背景
- 個人アプリで flutter + ferry(GraphQLクライアント) + hive(アプリ内キャッシュ) を採用している。
- ferryはキャッシュ用にhiveとの連携をサポートしている。
- ちなみに 別のメジャーなGraphQLクライアントであるflutter-graphqlもhiveとの連携をサポートしている
課題
- 古めのスマホで1000件ほどのJSONを読み込むと、アプリの動作がもっさりしてきた。
- ペットのお世話を記録できるアプリを作っているのだが、記録を一日10件を3ヶ月くらい続けた場合1000件くらいになる。
- 自分はPixel3XLで試した。
- (※追記)debugモードで試してました。profileモードで試せばもっとサクサク動く可能性があります。
- cf. https://docs.flutter.dev/testing/build-modes
Application performance can be janky in debug mode. Measure performance in profile mode on an actual device.
- 特に記録の作成、更新がめっちゃ遅い
-
update_cache_handler で キャッシュを全ての記録を読み込み、(記録の作成なら)新しい記録を付け足し、キャッシュに書き込む処理をしている。ここがボトルネックになっている。
- update_cache_handlerとはmutationのレスポンスが返ってきた時に呼ばれるコールバックのことで、関連するQueryのキャッシュを更新するために用いる。
-
update_cache_handler で キャッシュを全ての記録を読み込み、(記録の作成なら)新しい記録を付け足し、キャッシュに書き込む処理をしている。ここがボトルネックになっている。
pet_with_record.graphql
fragment PetWithRecord on Pet {
...Pet
records { // この記録(Record)がめっちゃ多い場合を想定
...Record
}
}
create_record_cache_handler.dart
UpdateCacheHandler<GCreateRecordData, GCreateRecordVars> createRecordCacheHandler =
(CacheProxy proxy, OperationResponse<GCreateRecordData, GCreateRecordVars> response) {
if (response.needUpdateCache) {
final createdRecord = response.data!.createRecord;
final pet = proxy.readFragment(GPetWithRecordReq((b) => b..idFields = createdRecord.petId.idFields)) ??
GPetWithRecordData(); // キャッシュから読み込み
final createdRecordBeforeIndex =
pet.records.takeWhile((record) => record.recordAt.isBefore(createdRecord.recordAt)).length;
final newPet = pet.rebuild(
(builder) => builder.records
.insert(createdRecordBeforeIndex, GPetWithRecordData_records.fromJson(createdRecord.toJson())!),
); // 新しく作成した記録をinsert。recordAt昇順でソートされている前提
proxy.writeFragment(GPetWithRecordReq((b) => b..idFields = createdRecord.petId.idFields), newPet); // キャッシュに書き込み
}
};
原因
hiveは大量のデータの読み書きには向いていない
cf. https://github.com/hivedb/hive/issues/170
- RAM usage: Normal boxes keep all keys and values are in memory. Lazy boxes only keys. But 50,000 keys still add up.
- CPU: When a box is being opened, all of its entries have to be read and decoded. Your UI will freeze with a huge amount of entries.
This obviously depends on your device but I recommend keeping it below 1000 (5000 max) for best performance. In some cases 10,000 entries might work as well.
LazyBoxを使うと問題が軽減されるはずだが、flutterのGraphQLクライアントはLazyBoxをサポートしていない
Boxは全データをメモリに読み込むが、LazyBoxはkeyだけ読み込む。これによりアプリ起動時(= box open する時の)メモリやCPU負荷が軽減される可能性がある。
ただ、上記のようなupdate_cache_handlerの挙動には影響を与えないかもしれない(box open しているわけではないので)。
lazyBoxを使うとマシだよとの記述があるが、ferryやflutter-graphqlはlazyBoxをサポートしていない
具体的にいうと、lazyBoxだと以下のように型が合わない。
型は Box extends BoxBase、 LazyBox extends BoxBase という関係になっている。
解決策
SQLite用ORMのdriftを試すことにした。
GraphQL、手軽に正規化したキャッシュができて便利だなーと思ってたのに残念。
反省
- 多くのデータをキャッシュするケースを技術選定時にとテストするべきだった。
- 全データをメモリに読み込む、という時点でそこらへんの特性には気づくべきだった。
- 技術選定時にリポジトリのIssueはざっとみた方が良いかもしれない。
Discussion