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


はじめまして。
2次元ツリーを使ってゆるくチャットするサービスQ&Qを作っているあどにゃーです。

無料枠を超えて課金されていた

ユーザが増えずほぼ開発仲間とのチャットと化しているQ&Qですが、気づいたら無料枠を超えて課金されてました
ほぼ2人でチャットしているだけなのに、なぜfirestoreの1日の読み取り無料枠の5万回を超えるのかの調査が始まったのでした

onSnapshotとは

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

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

さらなる罠の4倍界王拳

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

 nodes = {
  createdAt: timestamp,
  content: text,
  updatedAt: timestamp
 }

こういう場合、firesotreの仕様上、同時にすべてのデータが更新されるわけではなくプロパティの更新に時間差が生まれ、各プロパティの更新の度にonSnapshotは発火します。
1. nodesのプロパティはまずcontentが更新されonSnapshotが発火
2. createdAtの時間が更新されonSnapshotが発火
3. updatedAtの時間が更新されonSnapshotが発火
のように、2倍、3倍界王拳が起きてリード回数が何倍にも膨らむケースがあるようです。Q&Qのケースでは4倍界王拳になっていました。
4 * 2550 = 10200回

50回の書き込みで1万回の読み込み

おお、なんという錬金術
これなら1日5万回の書き込みをかんたんに超えていくのも納得です。

docChangesで差分更新に変更

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

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

まとめ

onSnapshotをそのまま使うと読み込み回数が爆発したので、docChanges()を使用して差分更新にする修正を行いました。

これで多くの人が使っても無料枠で耐えられるはずなの、ぜひQ&Qを使ってみてください

スクショした記事