Tonamelのフロントエンド開発で、パフォーマンス改善に向けてやったこと

こんにちは!GC事業部の熊谷です。 この記事はTech KAYAC Advent Calendar 2022 17日目の記事です。

TonamelのWebフロントエンド開発において、パフォーマンスについて考える機会が最近あったため、振り返りつつ書いていきます。

サービスの紹介

Tonamelをかんたんに説明すると「トーナメントプラットフォーム」です。 具体的に言うと、大会の作成から運営、トーナメント表の管理を行うWebサービスです。

こちらの機能説明画像が、どういうサービスかを分かりやすく示しています。

Tonamelの機能説明

パフォーマンスに向き合う機会

Tonamelには様々な機能・ページがありますが、ここではメインとなる「トーナメント表」に限って説明します。

トーナメント表は、大会の参加者が増えるほど大きくなります。 大会の最大人数は256人、限定機能も含めると1024人までのトーナメント表を作ることができます。

1000人規模のトーナメントをリアルタイム更新するためには、いくつかの工夫があります。事前知識としてこれについて説明していきます。

大規模なシングルエリミネーションのトーナメント表

今まで行われていた工夫

大規模なトーナメント表をスムーズに表示するための工夫を、いくつかピックアップして紹介していきます。

差分ポーリング

リアルタイム更新を行うためにはポーリングを行っています。 初回取得とポーリングでデータを分けて、ポーリングではトーナメント表で更新があった部分のみを取得しています。

GraphQLのページング

初回取得では大きなデータを取得する必要があります。分割してデータを取得することで、リクエスト単位の容量を削減しています。 TonamelではGraphQLを採用しており、クライアントとしてApolloを使用しています。GraphQLではcursorに基づいたページネーションを行うことができ、簡単にページングを実装することができます。

relay.dev

www.apollographql.com

スケルトンスクリーン

スケルトンスクリーンとは、コンテンツが表示されるまでの間にワイヤーフレームのような空のコンテンツを表示するUIです。 これまでのパフォーマンス改善とは少し意味合いが異なりますが、読み込み時間の体感的な改善やレイアウトシフトの対策に繋がります。

トーナメント表のスケルトンスクリーンを示すGIF画像
トーナメント表のスケルトンスクリーン

ロード中に空の要素を決め打ちで表示するのが簡単な実装ですが、シングル・ダブルエリミネーションではトーナメント表の形状がシード設定に応じて異なるため、決め打ちすると表示に違和感があるという問題がありました。 そこで、トーナメント表の取得を「概形を取得→対戦カードの中身を取得」 と2段階に分け、前半から後半に移るまでの待ち時間にスケルトンスクリーンを表示しています。

やったこと

ここまでがTonamelで既に行われていた工夫です。 ここからは最近筆者が担当したスイスドロー形式のリプレースにおいて、パフォーマンス改善に向けて「やったこと」を書いていきます。

計測する

まずは現状把握が必要です。「推測するな、計測せよ」という言葉があります。 リプレースが進んで最低限の動作確認ができるようになったところで、要件的に作成できる最大規模のトーナメント表を作成して動作パフォーマンスを見ることとしました。

計測には主にDeveloper Toolsのパフォーマンスタブを使い、処理に時間がかかっている関数を特定して改善する方針としました。

Developer Toolsでのパフォーマンス計測

また、基本的に大会が進むほど表示に必要なデータ数が増えるため、トーナメント表が重くなっていきます。 最も重い状況をシミュレートするために大会の自動進行スクリプトを作成して、1024人規模・最大30回戦を完了させた大会を再現した上で、パフォーマンス測定を行いました。

設計を見直す

パフォーマンス測定を行ったところ、1024人規模で10回戦目に突入したあたりからパフォーマンスが著しく低下することが分かりました。

そこで設計を見直すこととしました。

シングル/ダブルエリミネーションでは初回取得で全ての対戦カードを取得していました。これらの形式では対戦カード数は以下のようになります。 - シングルエリミネーション: 参加人数 - 1 - 3位決定戦を含む場合、さらに+ 1 - ダブルエリミネーション: 参加人数 * 2 - 1 - リセット戦を含む場合、さらに+ 1

多くとも対戦カード数は1024および2048に収まるため、問題がありませんでした。

一方でスイスドローで作られる対戦カード数は、参加人数 * 回戦数 / 2となります。1024人規模では最大で15360の対戦カードが作られることとなります。このため、全ての対戦カードを取得することは現実的ではありません。

そこでスイスドローの特徴に着目します。進行が自由なシングルエリミネーション・ダブルエリミネーションと違って、スイスドローでは最新ラウンドの全ての対戦結果が確定しないと次の回戦に進めません。そのため、差分が発生するのは最新の回戦のみという特徴があります。また、プレイヤーの棄権機能についても最新回戦のみとなります。 そのため、「現在開いている回戦」と「最新の回戦」のみを取得することで、大きく負荷を減らすことができます。

ここで考える必要があるのは、初回取得後に主催者が回戦を進めたり戻したりするパターンです。

これについてはポーリング対象に現在進行中の回戦を含め、差分があれば再取得を行うことで対応できました。 対象を柔軟に変えられるのは、GraphQLの利点ですね。

再取得を行う条件については、回戦の変化と状態遷移図を作成することで、抜け漏れが発生しないようにしました。

回戦の状態遷移図

実装を見直す

実装面においても、細かいパフォーマンス改善を行っていきました。 いくつかピックアップして紹介します。

  • filter, mapのメソッドチェーンをflatMapで一本化する
  • 非同期処理を適切に使う。awaitの連続はPromise.allに代替できないか検討するなど
  • (Vue Composition APIの文脈で)大きなreactiveを書き換えを行わない。MapSetを使えないか検討する
  • モーダルのような、すぐに読み込む必要のないコンポーネントはdynamic importで読み込む

おわりに

ここで挙げた内容は、パフォーマンス改善における手段としてはごく一部です。

フロントエンドのパフォーマンス改善について体系的に学ぶにあたっては、こちらの書籍が大変参考になります。

また、今回取り上げなかったパフォーマンス改善手段として、容量の削減やアプリケーション層以外の最適化も挙げられます。 これらを学ぶにあたっては「Web Speed Hackathon」が題材として素晴らしく、おすすめです。

github.com

明日は、ひだかさんの「app-ads.txt(ads.txt)を自動テストする」です。お楽しみに!