FirestoreのリアルタイムリスナーonSnapshotを使うと無料枠を飛び越えていた

はじめまして。
２次元ツリーを使ってゆるくチャットするサービス<a href="https://qnqtree.com" target="_blank">Q&Q</a>を作っているあどにゃーです。
![スクリーンショット 2021-05-30 15.56.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398972/0820ee50-c405-c633-8229-03c73ae4dc32.png)

#無料枠を超えて課金されていた
ユーザが増えずほぼ開発仲間とのチャットと化している<a href="https://qnqtree.com" target="_blank">Q&Q</a>ですが、気づいたら<b>無料枠を超えて課金されてました</b>:sob:
ほぼ２人でチャットしているだけなのに、なぜfirestoreの１日の読み取り無料枠の5万回を超えるのかの調査が始まったのでした:tired_face:
#![スクリーンショット 2021-05-30 15.46.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398972/3a0a64af-b555-c0d0-3370-d05ccd0a765b.png)

#onSnapshotとは
onSnapshotはfirestoreのリアルタイムリスナーをはる機能です。Twitterや5chに代表されるような昔の掲示板は<b>更新ボタンを押してリロード</b>しないと新しい書き込みをGETすることはできません。しかしonSnapshotを使うとLINEのようにリロードなしにリアルタイムで新しい書き込みを読み込むことができます。
２次元チャット機能でもリアルタイムで周りの書き込みを見たいため、このonSnapshotを下記コードのように使っていました。

```javascript
  this.listener = this.treeRef.collection('nodes')
      .onSnapshot(async nodes => {
        for (const node of nodes.docs) {
          // nodes array
          this.nodes[node.id] = node.data()
          // init for issue and questions
          let qData = node.data()
        }
      })
```

#onSnapshotの罠
onSnapshotは対象のcollectionに追加・変更・修正がある度にリアルタイムでデータを読み込みます。２次元チャットでいうと各付箋のnodeが変わる度に読み込まれるわけです。つまり、二人で50回チャットを繰り返すと、2人 x (1 + 2 + 3 + ... + 50)回の読み込みが走るわけです。この時の読み込み回数は
<b>2人 * 50*(50+1)/2 = 2550回 </b>
おそろしい:innocent::innocent::innocent:
たかだか50回のやりとりをすると<b>2550回読み込み</b>が起きているのです。

###さらなる罠の4倍界王拳
それでも１日にたかだか数100しかQ&Qしてないので、5万回も行かないだろうと思ったのですが、もう一つの罠がありました。それはnodeのデータの中に時間をタイムスタンプを司るcreatedAt, updatedAt
といったプロパティを持っているケースです。

```javascript
 nodes = {
  createdAt: timestamp,
  content: text,
  updatedAt: timestamp
 }
```
こういう場合、firesotreの仕様上、同時にすべてのデータが更新されるわけではなくプロパティの更新に時間差が生まれ、各プロパティの更新の度にonSnapshotは発火します。
1. nodesのプロパティはまずcontentが更新されonSnapshotが発火
2. createdAtの時間が更新されonSnapshotが発火
3. updatedAtの時間が更新されonSnapshotが発火
のように、２倍、３倍界王拳が起きてリード回数が何倍にも膨らむケースがあるようです。Q&Qのケースでは4倍界王拳になっていました。
<b> 4 * 2550 = 10200回 </b>
### 50回の書き込みで1万回の読み込み
<b>おお、なんという錬金術</b>:innocent:
これなら１日５万回の書き込みをかんたんに超えていくのも納得です。

#docChangesで差分更新に変更
onSnapshotにはcollectionの追加・削除・変更にその変更差分だけを取得するdocChanges()があります。これを使って差分だけを取得し、Vue.js側でwatchプロパティでnodesの変化を監視して変更があれば２次元ツリーを更新してあげるだけ。

```javascript
    // nodesのlistenrを作る
    this.listener = this.treeRef.collection('nodes')
      .onSnapshot((snapshot) => {
        snapshot.docChanges().forEach((change) => {
        // 追加時
          if (change.type === 'added') {
            this.nodes[change.doc.id] = change.doc.data()
          }
          // 修正（更新時）
          if (change.type === 'modified') {
            this.nodes[change.doc.id] = change.doc.data()
          }
          // 完全削除時
          if (change.type === 'removed') {
            delete this.nodes[change.doc.id]
          }
        })
        this.isListened = true
      })
  }
```

これで１回で読み込まれるのは、1ノードの変化になるので、つまり、二人で50回チャットを繰り返すても、2人 x (1 + 1 + 1 + ... +1)回の読み込みしか走りません。
つまり、
<b>2人 * 50 = 100回</b>
で100回の読み込みで済むことになります。nodesのプロパティ更新の時間差で重複更新がおきても、たかだか100~400回の範囲に収まるので気にする必要はありません:wink:

#まとめ
onSnapshotをそのまま使うと読み込み回数が爆発したので、docChanges()を使用して差分更新にする修正を行いました。
###これで多くの人が使っても無料枠で耐えられるはずなの、ぜひ<a href="https://qnqtree.com" target="_blank">Q&Q</a>を使ってみてください:sweat_smile:

###スクショした記事
https://qnqtree.com/tree/AtYVuJS5kdQPbaZfMvue