Firebaseと画像とUXを考える

どうも[Firebase Japan User Group](https://firebase.asia/)オーガナイザーの[@1amageek](https://twitter.com/1amageek)です。
この記事ではUX向上のための画像の取り扱い方について解説しようと思います。

# サービスと画像

#### 画像のロードはサービスのUXに大きな影響を与える

WEBでは、1秒の遅れで**CVRが7%ダウン**すると言われています。[Instagram](https://www.instagram.com/)がもし画像のロードに1秒もかかっていたとすれば、おそらくここまでは大きなサービスにはならなかったでしょう。
画像の取り扱いには、時間をかけてUXを向上させる意味があります。

#### ロード速度も重要だが、待ち時間こそ重要

画像のロードは、サイズと通信速度に依存します。つまり技術的観点からすれば、とにかく画像を小さくすること以外は、解決方法はありません。

既存のあらゆるサービスは、待ち時間をいかに低減させるかを工夫してUXを向上させてきました。この記事ではその手法に焦点をあてて紹介しようと思います。

また、Firebaseでは**Client Side Join**により**REST API**を利用した場合より問題が顕著になる場合があります。まずそこについて言及します。


# Firebaseにおける画像の取り扱い

**CloudFirestore**ではデータベースの特性上、画像の取得までに時間がかかる場合があります。主な原因は[Client Side Join](https://qiita.com/1amageek/items/afc1c0ceb15ffc2372fd)にあります。
**CloudFirestore**の通信速度の中央値は約200ms ~ 300msほどです。
**Client Side Join**により直列的な通信が複数回発生した上に画像のロードが発生したとすると1000msを超えてしまう場合もあります。

データ構造を最適化して、直列的な通信（複数回のClientSideJoin）を防ぎましょう。

#### データ構造の最適化

ここでは、次の画像のように`Group`に所属する`User`の`thumbnail`をリスト表示するUIを考えて見ましょう。

<img width="396" alt="スクリーンショット 2018-07-22 11.48.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/6e6d233f-a8a4-1eca-1300-c8446ccb2f26.png">

まずはアンチパターンを紹介します。

**アンチパターン**

`User`に注目してください。`thumbnailImage`が`Image`によって抽象化されています。

```swift
class Group: Object {
    var users: ReferenceCollection<User> = []
}

class User: Object {
    var name: String?
    var thumbnailImage: Relation<Image> = .init()
}

class Image: Object {
    var url: URL?
}
```

このパターンでは、3回の通信が発生した後に画像のURLを取得でき、ほぼ1秒後に画像がロード可能な状況になります。
<img width="393" alt="スクリーンショット 2018-07-22 11.58.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/0f5eb662-1dee-0e20-d60d-c646286df014.png">

**解決方法**

可能な限りメディア系のモデルの抽象化はしないようにしましょう。

```swift
class Group: Object {
    var users: ReferenceCollection<User> = []
}

class User: Object {
    var name: String?
    var url: URL?
}
```

しかし、これにもトレードオフはあります。次のように大量な画像を複数保持する場合は別です。なぜなら`User`自体が肥大化していくことを意味し、データベース全体の性能を下げることになります。

```swift
class Group: Object {
    var users: ReferenceCollection<User> = []
}

class User: Object {
    var name: String?
    var url: URL?
    var images: [URL] = []//数枚保存するだけなら問題ない。
}
```

大量と言うのは無限に増加可能性がある場合です。例えば**Google Photo**のようにユーザーの撮った写真を保存するようなサービスの場合です。このようなプロパティを**Growth Property**と呼びます。

Growth Propertyについては[Realtime Database Design](https://speakerdeck.com/1amageek/realtime-database-design)で紹介してるのでこちらをご覧下さい。

数枚保存するくらいであれば全く問題ありません。

[Pring](https://github.com/1amageek/Pring)でも複数のFileを配列で保存できるようにしています。

# 画像ロードのUX改善方法

今回は以下の手法について言及します。

- キャッシュを利用する
- 動的`Placeholder`を利用する
- `Preheating`を利用する
- `Multiplex Image Loading`を利用する
- HEIF

ここからはFirebaseはあまり関係ありません。一般的なサービスでも十分に役にたつテクニックですので参考にしてもらえると嬉しいです。またこの手法は**Pinterest**で多く利用されているので**Pinterest**を例に説明していこうと思います。

![static1.squarespace.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/3eb7e6d4-27af-2537-5a74-70aa95b3b6eb.gif)


# キャッシュを利用する

すでにサービスで実装されていることがほとんどなので深くは説明しませんが、簡単に言うと一度取得したコンテンツをローカルで保持し、再利用する技術です。

あえて注意点をあげるとすれば以下の2点です。

##### キャッシュを保持する期間
同じURLで別の画像をロードしたいときに困ります。同じURLで画像をロードすることは避けましょう。

##### ローカルリソースを圧迫する
たくさんの画像を取り扱うサービスでサイズの大きい画像をキャッシュし続けると困ります。必ずキャッシュサイズには制限を設けて[FILO](https://ja.wikipedia.org/wiki/LIFO)になるように設計しましょう。

# 動的`Placeholder`を利用する

動的Placeholderは**Facebook**, **Slack**で見ることが出来ます。
![loading-state-shimmer.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/c94e1164-bd0f-f451-183e-dad781490002.gif)
アニメーションすることで、コンテンツのロードまでの待ち時間のストレスを緩和することが出来ます。また、描画的な観点から見ても、同じ大きさのセルのあらかじめ用意しておくことでコンテンツが挿入された時にレイアウトでガタつくのを低減できます。

**Client Side Join**によって画像以外のコンテンツもロードできてない可能性があるFirebaseでは、動的なPlaceholderはとても相性の良い技術です。

また**Pinterest**の**First View**では次の画像のように5個程度のコンテンツしかロードしてません。

<img width="568" alt="スクリーンショット 2018-07-22 14.14.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/e16cdb2c-dcb5-e909-cfc2-a471a3859b02.png">


5個と言うのはちょうど**First View**を覆うことができる量です。2回目のフェッチで30個ほどのコンテンツをロードしています。ここにも待ち時間を少なくするための工夫が入っています。

# `Preheating`を利用する

## Preheatingの歴史
**Preheating**は聞き慣れない言葉だと思います。この言葉が使われたのはiOS8で登場したPhotoKitの[サンプルコード](https://developer.apple.com/library/archive/samplecode/UsingPhotosFramework/Listings/Shared_AssetGridViewController_swift.html#//apple_ref/doc/uid/TP40014575-Shared_AssetGridViewController_swift-DontLinkElementID_6)です。実際はそれ以前にFacebookの[Scott Goodson](https://github.com/appleguy)氏によって発表されました。
彼はAppleでApplication開発やFrameworkの開発を手がけたあとFacebookで[Texture](http://texturegroup.org/)の前進となるAsyncDisplayKitを開発しました。現在は、Instagramを経てPinterestで開発を続けているようです。

当時の発表がこちらです。
https://youtu.be/ZPL4Nse76oY?t=24m23s

## Preheatingとは

簡単に言うと、コンテンツが描画される前にコンテンツを準備する技術です。

<img width="428" alt="スクリーンショット 2018-07-22 9.38.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/e3d51988-14fc-769e-40dd-d062f421fdf9.png">

わかりやすい動画があるのでこちらをご覧ください。
https://www.youtube.com/watch?v=wrctPJxskhI

描画前にコンテンツを取得する**Preheating**は、Firebaseの**Client Side Join**ととても相性の良い技術です。

#### iOSでの利用
iOSでは`iOS10`から[Prefetching](https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data)が利用可能です。しかし、AutoLayoutでこれを利用するとメインスレッドで大量のセルをレイアウトすることになりフレーム落ちやカクついた動作になるので注意が必要です。[iOS12](https://developer.apple.com/videos/play/wwdc2018/220/)からはレイアウトのタイミングが最適化されるので改善されるようです。

- [iOS12のレイアウト最適化](https://developer.apple.com/videos/play/wwdc2018/220/)

# Multiplex Image Loadingの利用する

## Multiplex Image Loadingとは

この技術は複数サイズにリサイズされた画像を並列にロードしていく技術です。
次の例では3つの画像サイズを用意して、各々を同時にロードしています。

![Multiplex.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/d220e534-cca7-09a7-9fb4-9a93f242bcdf.gif)

個人的にはこの技術は非推奨ですが、ほとんどの開発環境では複数サイズの画像をすでにサーバーに用意しているのでいますぐUXを改善させるには、この方法を選ばざるを得ない状況だと思います。
推奨しない理由は、利用しないデータを取得しまっているところです。
実は、この問題を解消する技術はかなり古くから存在します。**Progressive JPEG**です。
できる事であればこちらを利用することをお勧めします。

## Progressive JPEGとは

一般的なJPEGは上から順にデータをロードしていきます。**Progressive JPEG**ではまず、画像全体を解像度の低いデータをロードし、徐々に解像度を上げていくようにデータをロードします。

![68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f636f6e74656e7466756c2d6c6162732f436f6e636f7264652f6d61737465722f7573652e676966.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/46f6ad0f-dbcd-4430-88a0-725126a8b6e0.gif)


**Multiplex Image Loading**が複数ファイルで同じようなことを実現しているのに対して、**Progressive JPEG**は1ファイルでこれを実現します。

Progressive JPEGはサイズの大きい画像をロードする場合は有効です。
サムネイルなどの小さいサイズの画像は適切なサイズの画像を準備しましょう。

- [JPEGには2種類ある！プログレッシブとベースラインの違いは何？](https://naifix.com/jpeg/)

# HEIF

## HEIFとは

**High Efficiency Image File Format**の頭文字をとって**HEIF**と呼ばれています。特徴は画像データだけでなく豊富なメタデータを含むことができます。

Appleが一番詳しく説明しているのでこちらをどうぞ
https://developer.apple.com/videos/play/wwdc2017/513/

HEIFの最大の特徴は圧縮率にあります。JPEGよりもさらにファイルサイズを半分程度にできます。
さらにthumbnailというメタデータを指定することです。高画質な画像を低解像度で表示することもできます。

こちらも個人的には非推奨です。ライセンスに問題があるので自由に利用するのは難しいためです。

- [H.265/HEVC特許暗黒時代](https://qiita.com/yohhoy/items/c2579097a507b1fbdddb)

# まとめ

今後もロード時間を低減するための工夫は、次々考案されていくはずです。
サービスを開発するエンジニアであればひたすらUXの向上に励みたいですね💪🏻

Firebaseに興味が沸いた方は、こちらの記事もおすすめです。
[あなたが知らないFirestoreのコアテクノロジー](https://qiita.com/1amageek/items/175305687acbe39b47d9)