production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

Expo SDK v36.0.0で追加されたMediaLibraryのお気に入り写真の読み込みが遅い問題と回避策

概要

React Native 0.61からCameraRollが削除されました。
そのため、Expo SDK v36.0.0以降でExpoマネージドアプリを作成している場合は、MediaLibraryを使用して写真を読み込まなければならなくなりました。
CameraRolleでは、「お気に入り」がアルバムの一つとして識別されていたため、.getPhotosのオプションとして"Favorites"を渡せば一度で取得できました。
しかし、MediaLibraryは「お気に入り」はassetsの属性としてしか取得できないため、全ての写真を読み込まなければ「お気に入り」写真を全て表示することができません。
この処理が非常に遅いので、この記事ではExpo SDK v36.0.0で追加されたMediaLibraryのお気に入り写真の読み込みが遅い問題と回避策について、書こうと思います。

MediaLibraryの仕様

MediaLibrary - Expo Documentation

MediaLibraryの全件取得はMediaLibrary. getAssetsAsync()で行えます。
上記の返り値のassets(assetの集合)をループで回し、MediaLibrary.getAssetInfoAsync(asset)を実行します。
このMediaLibrary.getAssetInfoAsync(asset)の返り値にisFavoriteが含まれています。

問題点

await MediaLibrary.getAssetInfoAsync(asset)が異様に遅いです。

上記をコードに表すとざっくり以下のようになります。

const assets = await MediaLibrary.getAssetsAsync(options).assets
for (const asset of mediaLibrary.assets) {
  const assetInfo = await MediaLibrary.getAssetInfoAsync(asset) // <= ここの応答速度が問題
  if(assetInfo.isFavorite){
    // お気に入りの場合の処理
  }
}

具体的には、await MediaLibrary.getAssetInfoAsync(asset)の呼び出しに750ms ~ 1,000ms程かかります

回避策

Promise.allを使用する

const assets = mediaLibrary.assets.map((asset) => {
  return MediaLibrary.getAssetInfoAsync(asset)
  });
});
await Promise.all(assets).then(function(assetInfos) { 
  assetInfos.forEach((assetInfo, key) => {
    // お気に入りの場合の処理
  });
});

MediaLibrary.getAssetInfoAsync(asset)を並列で実行しようと試みるも、iPhone XSでは全てのUIスレッドをPromiseの処理に使用されてしまいます。端的に言うと固まります
そのため、戻るボタンの操作、画面遷移も全くできない状態が続く上、端末がめちゃくちゃ熱くなります*1 スマホではこの処理は向かないため断念せざるを得ません...。

hasNextPageとendCursor-afterを使用する

MediaLibrary.getAssetsAsync()の返り値にhasNextPageとendCursorが含まれます。
これは以下の役割があります。

  • hasNextPage: 取得するアセットが他にあるかどうか。(totalCount >= assets.lengthであるか)
  • endCursor: 最後に取得したアセットのID。afterオプションにこの値を渡すと、直後のアセットが取得できる。

要は、hasNextPageがtrueの間、endCursorを使用したページングをhasNextPageがfalseになるまで使用することができます。
これはwhile文とforEach文で実装が行えるため、forEach文の終わりにユーザーに何らか変化が行ったことを知らせることができます。

例えば以下のような処理のイメージです。

let mediaLibrary = await MediaLibrary.getAssetsAsync(options)
while (mediaLibrary.hasNextPage) {
for (const asset of mediaLibrary.assets) {
  let assetInfo = await MediaLibrary.getAssetInfoAsync(asset)
  if(assetInfo.isFavorite){
    // お気に入りの場合の処理
  }
}
Alert.alert("ユーザーに対しての何かしらの処理")
mediaLibrary = await MediaLibrary.getAssetsAsync({...options, after: mediaLibrary.endCursor})
}

処理開始から終了までの時間は、当初と変わりありません。 しかし、画像表示処理を行う場合は、1スクロールで表示される画像には限りがあるため、ある小さな塊内で表示できるものがあれば、すぐに返し、それを繰り返すことで、ユーザーが「遅延している」と感じさせないようにすることも可能であるはずです。

キャッシュを使用する

assetInfo.isFavoriteがTrueとなったアセットのIDをAsyncStorageで格納する方法です。 2回目に表示する場合に、Keyが存在すれば、それを使用することで、表示速度を速めることができます。 しかし、懸念点は以下の通りです。

  1. お気に入りから削除された場合に対応できない
  2. 古い写真をお気に入りに追加された場合に対応できない

新しく追加された写真かつ、お気に入りに追加された写真しか対応できません。
そのため、速度面以外でのお問い合わせが増えてしまう可能性があります。

お気に入りから写真を選択することをやめる

CameraRolleからMediaLibraryの切り替えによって、お気に入りからの写真選択以外は全て高速化されます。*2 また、写真アプリでお気に入りに追加する手間と、アルバムに追加する手間はさほど変わりません。
ただ、ユーザーが「アルバムを作成し、そこ写真を入れることができる」というiOSの機能を認知していない可能性があります。(社内比) その点は、お問い合わせが増加する可能性もあります。

ただ、

  • お気に入りから選択ができなくなったことを明示
  • 代替案として、アルバムを作成し、そこに追加することができる操作方法を示したページへのリンクを設置し、ユーザーに認知させる
  • 上記を事前告知

といった、導線を確保することで、お問い合わせは増加を未然に防げることは可能なのではないかと思います。(私は一人では決めきれませんでした)

結論

まずは、現時点で最も良いUXを提供できるであろう、「hasNextPageとendCursor-afterを使用」し、実装を行うのが、現時点で最良の選択だと思います。
また、「キャッシュ使用する」実装をした場合のお問い合わせ(=懸念点を実行するユーザー数)がどれだけ増加するかは未知数であるため、段階的リリースなどで、様子をみつつ、リリースチャレンジをするのも検討できるではないかと思います。

*1:熱くなってきたぜ🔥💪🔥💪🔥

*2:Expoバージョンアップ時の記事を参照