KomercoアプリでFirebaseからの画像取得を速くした話

こんにちは。Komerco事業部エンジニアの高橋(id:yosuke403)です。

Komercoは、「料理が楽しくなるマルシェアプリ」をコンセプトに、料理が楽しくなる器やカトラリー、リネン雑貨等を出品/購入できるサービスです。現在はiOS版のアプリケーションを提供しています。

komer.co

Komerco - コメルコ - by クックパッド

Komerco - コメルコ - by クックパッド

  • Cookpad Inc.
  • ショッピング
  • 無料

先日、Komercoアプリの画像表示の速度を改善したので、それについて書こうと思います。

背景と成果

Komercoで商品を選ぶユーザにとって、商品画像は当然重要なものです。 しかし以前は、アプリを起動してみると画像の表示が遅く、商品一覧をスクロールするとしばらく経ってから画像が表示される状況でした。

こちらは改善前のバージョンで、会社のWiFiに接続し、初回起動(キャッシュなし)から新着商品一覧を表示したときの様子です。

f:id:yosuke403:20181101175645g:plain

セルが表示されても画像が表示されるまで少し時間がかかり、特に素早くスクロールされると画面上のセルが全てプレースホルダーになってしまいます。

次に改善後のバージョンで同環境で操作した様子です。

f:id:yosuke403:20181101175724g:plain

プレースホルダーはほとんど表示されなくなりました!

原因

商品画像に関するシステム構成

KomercoはFirebaseを利用したサービスで、データベースとしてCloud Firestoreを、画像データの置き場としてCloud Storageを使用しています。

f:id:yosuke403:20181101175944p:plain

Firestoreはデータをドキュメントという単位で扱い、ドキュメントには数値や文字列といった値を格納することができます。また、ドキュメントはコレクションというグループでまとめられています。

Komercoの場合、「Product」コレクションには商品名や画像のIDを持つドキュメントが格納され、「Image」コレクションにはCloud Storageのリファレンスを持つドキュメントが格納されています。画像のIDはImageドキュメントのIDに対応しており、これによりアプリはProductドキュメントから、対応するImageドキュメントを取得できます。

画像取得のフローに原因

商品一覧画面が表示されてから商品画像を取得するまでのフローは次のようになっています。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageIDからImageドキュメントをFirestoreから取得
  • ③ ImageドキュメントのstorageRefから画像のURLをCloud Storageから取得
  • ④ Cloud Storageから画像データの取得

f:id:yosuke403:20181101180034p:plain

ここから分かるように画像を取得するまでにFirebaseと何度か通信する必要があります。 しかも②〜④についてはセルの表示ごとに発生するため、素早く商品一覧をスクロールした際などは、リクエストが大量に発生して処理に時間がかかっていました。 加えて、個々のリクエストについてもレスポンスが遅く、こちらもリクエストが詰まる要因になっていました。

既存の対応策

この課題は以前から認識しており、対策としてキャッシュを利用したリクエスト数の低減をすでに行っています。 一度取得したImageドキュメントや画像データをローカルにキャッシュして、何度も通信が発生しないようにしています。

speakerdeck.com

しかし初回起動時や久しぶりに起動したときなど、キャッシュがない状態のときはやはり画像取得は遅い状態でした。

そこで今回は、キャッシュに無い画像データを取得する際に必要な通信回数を減らすことと、各通信自体を速くすることを考えました。

改善内容

改善後の画像取得フロー

ProductドキュメントにimageURLという画像のURLを格納するフィールドを追加しました。 これによりフローは次のようになりました。

  • ① ProductドキュメントのリストをFirestoreから取得
  • ② UI上にセルが表示される直前に、そのセルに対応したProductドキュメントのimageURLから画像データを取得

画像取得に必要な通信は②のみになりました。また、②の通信速度もかなり改善しています。

f:id:yosuke403:20181101180132p:plain

以下では行ったことについて詳しく説明します。

Productドキュメントに画像のURLが付与されるようにする

元々画像データを取得する際は、Imageドキュメントを介することで、Cloud Storage上の画像データの在り処を他ドキュメントと共有したり、Imageドキュメントから得られる画像リサイズの完了イベントを取得したりすることができました。しかし今回はパフォーマンスを優先し、Imageドキュメントを介さず、Productドキュメントに付与された画像のURLを使うようにしました。これにより、改善前の画像取得フローの②と③の通信を省略することができました。

ここで言う画像のURLは、以前のフローの③で得られるダウンロードURLとは違うのですが、理由は次の節で説明します。

画像のURLの付与はCloud Functionsを利用しました。 Cloud FunctionsではFirestoreのドキュメントの作成・更新をトリガーに処理を実行できます。 これを利用して、Productドキュメントに更新がかかったときにimageIDをチェックし、変化があれば画像のURLを取得してProductドキュメントに付与します。

f:id:yosuke403:20181101180154p:plain

Cloud Functions側の実装は概ね次のようになっています。 これはProductドキュメントの新規追加時の例ですが、更新の際もimageIDが変更されたかをチェックしている以外は同じような実装になります。

export const productCreated = functions.firestore.document('/Product/{productID}').onCreate(async (snapshot, context) => {
  const db = firebase.firestore()
  const imageDoc = await db.collection('Image').doc(product.data.imageID).get()
  return product.ref.update({
    imageURL: makeImageURL(imageDoc.ref.id, imageDoc.data))
  })
})

Cloud Functionsを利用するメリットは、Productドキュメントを更新するクライアントのアップデートが不要であることです。 Komercoでは購入用のKomercoアプリとは別に出品用のアプリがあるのですが、出品用アプリはimageURLの存在を気にする必要はありません。 そのため、このアプリの強制アップデートはせずに今回の対応が可能でした。

画像データを取得する時間を短縮する

全体的にレスポンスが遅い原因はリージョンの問題でした。 画像データについては、デフォルトの「Multi-Regional 米国」ではなく「Regional アジア太平洋(東京)」に画像データを移すだけで、かなり改善しました。

さらにFirebaseのCloud Storageではなく、その背後にあるGCPのCloud Storageを直接見に行くようにすると更に速くなりました。 これは、Firebaseではリクエストの検証をし、アクセス権の有無を確認しているためと思われます。 今回の商品画像は公開しても問題なかったので、全ユーザに閲覧権限をつけて公開状態にしています。 GCPのCloud StorageのURLはFirebaseから取得できないので、Imageドキュメントの内容を基にCloud Functions自身がURLを生成してProductドキュメントに付与します。

ちなみに、Firestoreも「Multi-Regional 米国」のためにレスポンスが遅いのですが、現状では東京リージョンは利用できません。 リリースは予定されている模様なので、もう少し待ちたいと思います。

改善結果の数値

改善前のユーザのデータは無いのですが、手元のビルドで計測してみたところ、セルの表示が始まってから画像が表示されるまでに平均1.5秒ほどかかっていました。 これは会社のWiFi環境で計測したので、実際ユーザの手元ではもっとかかっていると思います。

改善後については、画像データ取得自体の時間は中央値で66ミリ秒程度、95パーセンタイル値で841ミリ秒でした。 画面上での表示も概ねこれに近い時間で行われていると思います。

f:id:yosuke403:20181101180220p:plain

最後に

Komercoは6月にリリースしたばかりのサービスで、改善の余地はたくさんある状態です。 作りたい機能もまだまだありますが、こういったサービスのパフォーマンスの改善も怠らないようにしたいと思います。

また、先日Firebase Summit 2018が催され、様々なニュースが発表されましたね。

firebase.google.com

個人的にはFirestoreのlocal emulatorと、Management APIが気になっています。 進化するFirebaseの機能を積極的に導入して、よりスピーディーにサービスを成長させていきたいと思います!

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/