本連載は分散型マイクロブログ用ソフトウェアMisskeyの開発に関する紹介と、関連するWeb技術について解説を行っています。
Misskeyでは、新機能の追加や改修・
今回はそういった最近のMisskeyのパフォーマンス改善の取り組みについて紹介します。
Reactions Boost Technology(RBT)
Misskey® Reactions Boost Technology™
ノート: Misskey®︎ Fanout Timeline Technology™︎
リアクションを効率化させたいというのは昔から構想があり、Issue
背景
Misskeyでは以前から、ノートの取得時のパフォーマンスを向上させるために、ノートレコードに非正規化したリアクション情報を持つようにしています。
リアクション情報は2種類あり、ひとつは
前者は要素数に制限のある文字列の配列として保存されており、
後者はJSON型で、Keyにリアクションの種類、Valueにそのリアクションがされている数を持ちます。
PostgreSQLでは、MongoDBなどのNoSQLよろしくJSON型のアトミックな更新を行えるクエリが用意されており、特定のキーの値をインクリメント/デクリメントするのにクエリ発行は1回だけで済みます。
ノート:具体的には以下のようなクエリでアトミックな操作が可能です。
jsonb_set("reactions", '{👍}', (COALESCE("reactions"->>'👍', '0')::int + 1)::text::jsonb)
1回のクエリで済むので、トランザクションやロックの必要もありません。
……と、ここまでは良いのですが、PostgreSQLにおいてレコードの更新は内部的に
解決策
そこで実装されたのがRBTです。このRBTですが、実装中はReactions Buffering Technologyという呼称でした
この名前からある程度想像がつくかと思いますが、RBTは以下のアプローチでリアクション処理の効率化を図っています。
- リアクションが作成されてもすぐにはDBのノートレコードを更新せず、一旦Redisに情報を溜めておく
(バッファリング) - 一定の間隔でRedisにバッファリングされているリアクション情報をデータベースにまとめて書き込む
(ベイクと呼んでいます[1]) - ノート情報を取得する際は、Redisにもクエリを発行し、バッファリングされているリアクション情報も持ってきてAPIのレスポンスを行う
これにより、同じノートにリアクションが複数回行われても、ノートレコードの更新はベイク時の1回だけで済むようになります。
いわゆるバズったような投稿だと、短時間に数百・
Redisの負荷は増えますが、PostgreSQLに比べるとはるかにこういった処理に向いていて、スケールもしやすいです。
デメリットはリアクションの頻度が少ないサーバーだと効果が薄い点と、Redisにバッファリングするためサーバーのメモリ使用量が増加する点です。
実装
主な実装はReactionsBufferingService.
Redisにも、ノートレコードと同じく2種類のリアクション情報を持つようにしています。
「誰がどのリアクションをしたのか」
Sorted Setsを使う理由は、この情報はデータ量の観点から無制限に保存し続けるわけにはいかず、数に制限を設けて古いものから消していく必要があるためです。
Sorted Setsはメンバーごとに異なったスコアを持たせることができ、そのスコアによってソートした結果を効率的に得ることができます。
日時情報をスコアとして利用すれば、メンバーを新しい順・
「どのリアクションがどれだけ行われたか」
Redisでも、Hashの特定のKeyのValueをインクリメント/デクリメントする操作は1コマンドでアトミックに行えます
これらのデータ構造を活用することでRBTを実現し、データベースの負荷を低減させることができました。
content-visibility: auto
Misskeyではv2024.content-visibility
プロパティを利用してクライアントのパフォーマンスを向上させています。
content-visibility
の値をauto
に指定すると、その要素が不可視の場合にレンダリングがスキップされるようになるという、ブラウザネイティブの半仮想スクロールのような機能です。
ノート:仮想スクロール
主に大量の要素があるリストのレンダリング
前からBlinkやGeckoでは実装されていたのですが、WebkitはiOS 18からの対応なのでメジャーなブラウザすべてで使えるようになったのはつい最近です。
なお対応していないブラウザで使用しても無視されるだけなので、後述するデメリットを許容できるユースケースであれば使っておいて損はありません。
vs 仮想スクロール
ReactにしろVueにしろ、仮想スクロールはトリッキーな実装になり、
- ユースケースによっては上手く動かない
- バグを生みやすい
- アクティブにメンテナンスされているライブラリが少ない
- 導入コストが高い
- DOMが作られないので、ページ内検索やフォーカスが機能せずアクセシビリティが低い
などの理由があってなかなか採用が難しい面があります。
もちろん、
またMisskeyにおいては、タイムラインは単なるリストのレンダリングではなく、
- 引っ張って更新できる機能
(pull to refresh) がある - 途中で日付の区切りをレンダリングするようになっている
- ノートごとの高さが一定でなく、しかもリアクションがついたりすると動的に高さが変わる
といった事情があるため、実装が不可ではないにしても仮想スクロールとあまり相性が良くないのではないかという懸念がありました。
実装できたとしても、コンポーネントが比較的複雑で
ノート:Misskeyにおける仮想スクロール導入はIssue
その点、content-visibility
はブラウザネイティブで実装されている機能ですからそういったデメリットは少なく、気軽に導入できます。使用するのもCSSを1行追加するだけです。
パフォーマンス
パフォーマンスについては、
ところが、筆者が試した限りでは大量のDOMがあるケースでもcontent-visibility
をauto
に設定するとスイスイで、明らかに重くなるようなことは確認できませんでした。大げさかもしれませんが、
おそらくブラウザの実装が優秀で、レンダリングの対象にならなければDOMが多くてもそこまでの負荷にはならないのかもしれません。
ノート:仮想スクロールとの違いが明確に表れるほどの長大なリストをレンダリングするようなユースケースでは、そもそもUIの設計が良くない可能性があります。ユーザビリティの観点からも、ページングなどの方式も検討するべきでしょう。
詳細な検証はまだ行っていませんが、いずれにせよなかなかのポテンシャルはありそうです。
注意点
使用する際の注意点は、
高さが分からないと、実際に最後までスクロールしてみるまでスクロールバーの正確な表示が行えないことになります。
この問題を緩和するためにcontain-intrinsic-size
というプロパティが用意されており、要素の大体の
ただ、それを使ったとしても完全に正確にはできないので、ある程度のスクロールバーの精度低下はパフォーマンス向上のためのトレードオフとして受け入れる必要があります。
とはいえ、仮想スクロールやcontent-visibility
を使わなくても動的なコンテンツの読み込みなどでスクロールバーがズレることはよくありますし、そもそもスクロールバーが完璧であることが求められるシチュエーションは少ない思うので、このデメリットは無視できることが多いのではないでしょうか。
なお、一度でもレンダリングされた要素のサイズはブラウザが記憶してくれ、可視範囲外になってもレイアウトシフトが発生するようなことはありません。
ノート:筆者が3年ほど前にこの機能をChromeで試したときは、下のほうにスクロールしていくと上にあった要素のレンダリングがスキップされ、かつサイズが記憶されなかったためにレイアウトシフトが発生していました。現在は同様の現象は起こらないのでバグだったのでしょう。
まとめ
今回RBTの実装で、リアクションのパフォーマンス向上を図りました。
動的なデータの扱いはPostgreSQLは苦手としていて、Redisなどのインメモリデータベースは得意としている傾向があります。このようにデータベースはそれぞれ得手不得手があるので、適材適所で使っていくことが必要です。
リストのレンダリングについては、仮想スクロールは銀の弾丸ではありません。一方、content-visibilityは大きなデメリットもなく手軽に導入できますので、一旦使ってみてそれでもパフォーマンスの問題がある場合に別の方法を検討するのでもよいと思います。
ブラウザは、Webアプリが複雑化していくのをただ黙って見ているわけではなく、それに耐えられるように改修や新しい仕組みを実装します。仮想スクロールのような複雑な実装をしなくても済む未来を夢見て、今後もブラウザネイティブでアプリケーションのパフォーマンスを向上させる仕組みが実装されていくことを期待しています。