SvelteとWebSocketでリアルタイム共同ホワイトボードを構築する
現代のコラボレーションツールを考えると、MiroやFigJamといった名前がすぐに思い浮かびます。その滑らかな動作の背後には、状態同期、競合管理、ほぼゼロのレイテンシといった手ごわい技術的課題が潜んでいます。しかし、特殊なプロトコルやリアルタイムデータベースで車輪を再発明する必要があるのでしょうか?必ずしもそうではありません。SvelteとWebSocketを適切に使いこなせば、シンプルさを犠牲にすることなく、高性能な共同ホワイトボードを構築できます。
この記事では、類似プロジェクトの経験とコミュニティからのフィードバックに基づき、そのようなアプリケーションをステップバイステップで構築する方法を案内します。アーキテクチャ、同期、ユーザー管理、そして違いを生む最適化について説明します。
基盤:SvelteとWebSocket、勝利のコンビ
Svelteはネイティブのリアクティビティで際立っています。リアクティブ変数はオーバーヘッドなくDOMを更新します。双方向の永続通信を可能にするWebSocketと組み合わせることで、リアルタイムに理想的な基盤が得られます。
このケースでは、描画アクション(線画、消去、要素の移動)はすべてWebSocketを介してNode.jsサーバーに送信され、サーバーが他のクライアントに配信します。グローバル状態はサーバー側で維持されますが、各クライアントは即座の応答を保証するためにローカルコピーを持ちます。
アーキテクチャ:指揮者としてのサーバー
サーバーは中心的な役割を果たします。イベントを受信し、検証し、再配信します。競合を避けるため、各イベントにはタイムスタンプと一意のIDが付けられます。同じ形状に対する競合が発生した場合、最後に受信したイベントが優先されます。これは単純ですがホワイトボードには効果的な「最終書き込み優先」アプローチです。
セッション管理は不可欠です。各ホワイトボードには一意のIDがあります。クライアントは特定のルームに接続し、サーバーはそのルームのメンバーにのみイベントを送信します。これにより負荷が制限され、プライバシーが保証されます。
状態管理:ローカルとリモートの適切なバランス
よくある落とし穴は、形状のホバーなどの一時的な状態も含めてすべてをリアルタイムで同期することです。以下のように区別するのが賢明です。
- 永続的なイベント:線画、移動、プロパティ変更 – サーバーに送信され、永続化されます。
- 一時的なイベント:他のユーザーのカーソル、一時的な選択 – WebSocketを介してローカルに配信されますが、永続化されません。
この区別によりサーバーの負荷が軽減され、データベースが不要な情報で溢れるのを防ぎます。
ユーザーエクスペリエンス:他者のカーソル、コラボレーションへの窓
共同作業者のカーソルをリアルタイムで見ることで、存在感が生まれます。位置データは高頻度(50ミリ秒ごと)で送信されますが、配信は保証されません。飽和を防ぐため、クライアント側でスロットリングを使用します。
注意点:ホワイトボードのスクロールやズームを考慮する必要があります。各マウスイベントは、画面ピクセルではなく、キャンバスに対する論理座標に変換され、すべてのユーザーがカーソルを正しい位置で見られるようにします。
競合管理:マルチプレイヤーでのアンドゥ/リドゥ
アンドゥ/リドゥはコラボレーション環境では頭痛の種です。採用した解決策:各ユーザーは独自のローカルアンドゥスタックを持ちますが、アンドゥアクションは「逆の」イベントに変換されてサーバーに送信されます。つまり、線をアンドゥすることは、全員に「この線を削除」イベントを送信することと同じです。このアプローチは複雑ですが、不整合を防ぎます。
詳細については、Liveblocksがマルチプレイヤー環境でのアンドゥ/リドゥ戦略に関する優れた分析を提供しており、それが私たちの実装の参考になりました。
パフォーマンスとスケーラビリティ
中程度の使用(同じボードに同時に100ユーザー未満)であれば、単一のNode.jsサーバーで十分です。それを超える場合は、インスタンス間で状態を共有するためにRedisを使用した分散アーキテクチャを検討する必要があります。類似プロジェクトで言及されているNATSもメッセージングバックボーンとして使用できます。
クライアント側では、レンダリングパフォーマンスのためにSVGよりもHTML5 Canvasが好まれます。形状は毎フレーム再描画されますが、ダーティ矩形を使用して変更された領域のみが更新されます。
デプロイと実用的な考慮事項
WebSocketサーバーはHerokuやFly.ioなどのプラットフォームにデプロイできます。長時間接続に注意:ロードバランサーのタイムアウトはWebSocketを切断しないように設定する必要があります。本番環境ではSSLの使用が必須です。
ストレージに関しては、SQLiteやPostgreSQLなどのシンプルなデータベースでボードの最終状態(プロパティ付きの形状リスト)を保存できます。より高度なリアルタイム要件には、Supabase Realtimeがターンキーソリューションを提供します。
経験からの教訓
プロトタイプ構築中にいくつかの落とし穴が特定されました:
- 圧縮を軽視しない:WebSocketメッセージはpermessage-deflateで圧縮して帯域幅を削減できます。
- 再接続を処理する:接続が切れた後、クライアントは再同期のためにボードの完全な状態を要求する必要があります。
- 実際のユーザーでテストする:単体テストは、数十人が同時に描画する実際のセッションに代わりません。
結論
SvelteとWebSocketで共同ホワイトボードを構築することは、アクセスしやすく勉強になるプロジェクトです。シンプルでありながら堅牢なアーキテクチャを守り、状態と競合を適切に管理することで、スムーズで快適なアプリケーションが得られます。Svelteのような最新ツールはリアクティビティを簡素化し、WebSocketはリアルタイム通信を保証します。
さあ、始めてみませんか?エディタを開き、いくつかの依存関係をインストールし、まずは簡単なマウス座標の交換から始めましょう。残りは反復によって実現します。
さらに詳しく
- Medium - Building a Real-Time Collaborative Whiteboard Backend with NestJS and Socket.IO - NestJSを使用した同様のアプローチを詳述した記事。
- Supabase Realtime Docs - ストレージと同期に役立つSupabaseのリアルタイム機能のドキュメント。
- Reddit - How to start learning WebSockets - 初心者向けの実用的なアドバイスが記載されたディスカッション。
- Reddit - Reactive, optimistic-by-default WebSocket library - コラボレーションアプリ向けのリアクティブWebSocketライブラリの紹介。
- Hacker News - Matrix-CRDT: real-time collaborative apps using Matrix - MatrixとCRDTに基づく代替アプローチ。
- Hacker News - How to build undo/redo in a multiplayer environment by Liveblocks - マルチプレイヤー環境でのアンドゥ/リドゥ戦略の分析。
- Reddit - itty-sockets: dead-simple realtime messaging in Svelte - Svelte向けの軽量リアルタイムメッセージングライブラリ。
