1. はじめに
ソーイ株式会社の西浦です。
普段は受託のWebアプリケーション開発に携わっており、Figmaを日常的に使っています。共同開発者と同時に同じファイルを触りながら実装のすり合わせをする、という場面が多いのですが、あるとき、他人がドラッグしているオブジェクトの動きが、まるで自分が操作しているかのようにぬるぬると追従してくることにふと気づきました。「これ、本当にオンラインで同期されている共同作業の動きなのか?」と思うほど、かなりなめらかです。
改めてNotionやGoogleスプレッドシートでも同じように同時編集してみると、画面への反映のされ方にはそれぞれ違いがありました。
そこで、「Figmaは複数人の変更を裏側でどう同期し、競合をどう解決しているんだろう?」と気になり、仕組みを調べてみることにしました。
本記事では、この体感差そのものを厳密に説明するのではなく、その背景にあるFigmaの同期・競合解決の設計に焦点を当てます。
調べてみると、Figmaの同期・競合解決の設計には、大きく次の4つの工夫がありました。
- サーバー到着順を利用した、LWW register相当のプロパティ同期
- 楽観的更新と、それによって生じるチラつきを防ぐ一手間
- 循環参照という厄介なエッジケースを、「一時的な割り切り」で吸収する設計判断
- Fractional Indexingによる、挿入コストの低い兄弟順序管理
以下、それぞれ順に見ていきます。なお、描画やレンダリングの最適化については本記事では扱いません。
この記事の対象読者
- Figmaを日常的に使っていて、その同期のなめらかさに「なんで?」と思ったことがある方
- リアルタイム協調編集機能(チャット、共同編集ドキュメント、ホワイトボードツールなど)を自分のプロダクトに実装したいエンジニア
- OT(Operational Transform)やCRDT(Conflict-free Replicated Data Type)を名前だけ知っていて、実際の設計判断に興味がある方
参考にした一次情報
Figma社が自社ブログで、この仕組みをかなり詳細に公開しています。本記事はこの一次情報を読み解いた内容がベースです。
本記事はFigma公式ブログの内容を筆者なりに咀嚼・要約し、エンジニア視点の所感を加えたものです。正確な一次情報は上記リンクをご参照ください。
2. 調べるきっかけ:3つのツールで同時編集してみた
言葉で説明するより、実際の動きを見てもらうのが一番早いと思います。Figma・Notion・Googleスプレッドシートで、片方の画面でオブジェクト(セル/ブロック)を動かしたときに、もう片方の画面にどう反映されるか(ラグ、カクつき、反映されるタイミング)を撮ってみました。
Figma
Notion
Googleスプレッドシート
見ていただくと、今回の検証ではFigmaだけ
- 相手側の画面への反映にほぼ遅延がない
- カクつきや、位置が一瞬戻るような不自然な動きがほぼ起きない
という結果になりました。
今回の検証環境では、体感上Figmaが最も滑らかに同期されました。なお、各サービスで操作対象や内部実装が異なるため、厳密な性能比較ではありません。
3つとも仕組みとしては「複数人の同時編集を裏で調整する」という同じ課題に取り組んでいるはずなのに、この体感差はどこから来るのか?というのが今回の出発点です(前述の通り、本記事で扱うのは同期・競合解決の設計の部分です)。
比較対象の同期方式について
Figma公式ブログでは、Google DocsはOT(Operational Transform)を利用する代表的な共同編集アプリとして紹介されています。一方、今回比較したGoogleスプレッドシートについては、現在の具体的な競合解決方式を示す一次情報を確認できなかったため、本記事では方式を断定しません。
Notionは、2025年12月に公開した公式ブログで、オフライン利用の対象となるページを新しいCRDTベースのデータモデルへ動的に移行していることを説明しています。ただし、公式記事ではオフライン利用を支える競合解決の文脈で説明されており、今回の比較動画で操作したボードビューのカード移動と同じ仕組みが使われているかは確認できません。
そのため、本記事ではNotionとの体感差を「CRDTを使っているかどうか」で説明することはしません。
Figma公式: https://www.figma.com/blog/how-figmas-multiplayer-technology-works/
Notion公式: https://www.notion.com/blog/how-we-made-notion-available-offline
3. 調べてわかったこと:OTではなく、CRDTに着想を得た独自方式だった
複数人での同時編集を実現する代表的なアプローチの1つが OT(Operational Transform) です。
FigmaはOTを採用せず、代わりにCRDTの考え方に着想を得た独自方式を構築しました。
Figma公式ブログによると、OTはテキスト編集の文脈では強力な一方、操作の種類が増えるほど、それぞれの操作同士を正しく変換するためのルールが必要になり、実装・検証が複雑になります。Figmaはテキストエディタではなく、図形の移動やサイズ変更、色の変更、親子関係の変更など多様な操作を扱うため、OTの複雑さは自分たちのユースケースには過剰だと判断したとのことでした。
代わりにFigmaが採用しているのが、CRDT(Conflict-free Replicated Data Type) という考え方をベースにした独自の軽量プロトコルです。以降の章では、この仕組みを1つずつ分解していきます。
4. Figmaのアーキテクチャ前提
以下は2019年に公開されたFigma公式記事の設計をベースにしています。現在の内部実装は変更されている可能性があります。
技術の中身に入る前に、土台となるアーキテクチャを整理しておきます。
- クライアント/サーバー型で、Figmaのクライアント(Webページ)はサーバー群とWebSocketで通信する
- ドキュメントごとに専用のサーバープロセスが立ち上がり、そのドキュメントを編集している全員がそこに接続する
- ドキュメントを開くとまずファイルの全体をダウンロードし、以降の更新はWebSocket経由で双方向に同期される
- オフラインでも編集を続けられる。オンラインに復帰すると最新状態をダウンロードし、オフライン中の編集をその上に再適用してから同期を再開する
この「切断・再接続はシンプルに保ち、複雑さは"接続中のドキュメント更新"に閉じ込める」という設計方針が、後述する仕組み全体の土台になっています。
5. CRDTからの着想
CRDTとは、複数のレプリカが独立に更新されても、最終的に同じ状態へ収束できるよう設計されたデータ型・データ構造の総称です。
- どのレプリカ(複製)も、他と調整することなく独立・並行に更新できる
- 矛盾が起きても、アルゴリズムが自動的に解消する
- 一時的に状態が異なっても、最終的には必ず収束する
CRDTには色々な種類があり、例えば「Grow-only set(追加専用の集合。同じ要素を2回追加しても結果は変わらない)」や「Last-writer-wins register(最後に書き込まれた値を採用する、単一の値を保持する箱)」などがあります。
ここでのポイントは、Figmaは「純粋な」CRDTを使っているわけではないという点です。CRDTは中央の権威に依存せず収束できるよう設計されることが多い一方、Figmaには中央のサーバーが存在します。そこでFigmaはその前提を利用し、一般的なCRDTが必要とするメタデータなどを省略することで、オーバーヘッドを削減したシンプルで軽量な実装を実現しています。
つまり「CRDTの考え方に強くインスパイアされつつ、自分たちのユースケースに合わせて簡略化した独自プロトコル」というのがFigmaの立ち位置です。
6. Figmaドキュメントの構造
同期の話をする前に、そもそもFigmaのドキュメントがどんなデータ構造になっているかを押さえておきます。
Figmaのドキュメントは、HTMLのDOMツリーのような木構造です。ルートオブジェクトの下にページオブジェクトがあり、その下に各ページの中身を表すオブジェクトの階層が続きます。これがエディタ左側のレイヤーパネルに表示されているものです。
各オブジェクトはIDを持ち、プロパティと値の集まりを持っています。イメージとしては、Map<ObjectID, Map<Property, Value>> という2階層のマップ、あるいは (ObjectID, Property, Value) という3つ組をたくさん持つデータベースのようなものです。
この構造のおかげで、Figmaに新機能を追加するというのは、多くの場合「オブジェクトに新しいプロパティを追加する」ことに帰着します。
木構造で見るとレイヤーパネルそのものです。
その実体は、こういう2階層マップです。
ObjectID: "rectA"
├─ x: 120
├─ y: 80
├─ width: 200
├─ fill: #FF0000
└─ parent: "frame"
木構造は「parentプロパティ」というただの1プロパティから導かれているだけ、という点がポイントです。
7. プロパティ同期の仕組み
ここが個人的に一番「なるほど」と思った部分です。
Figmaのサーバーは、あるオブジェクトのあるプロパティについて、どのクライアントから送られてきた値が最新かを常に把握しています。これにより、
- 同じオブジェクトの別々のプロパティを別のクライアントが変更しても衝突しない
- 別々のオブジェクトの同じプロパティを別のクライアントが変更しても衝突しない
という性質が自然に成立します。衝突が起きるのは「同じオブジェクトの同じプロパティ」を複数人が同時に変更したときだけで、その場合は最終的にサーバーに最後に届いた値が採用されます。これはCRDT文献でいうLast-writer-wins register に近い考え方ですが、面白いのはタイムスタンプが不要という点です。サーバー自身が到着順序を決められる立場にあるため、タイムスタンプで順序を判定する必要がないのです。
この設計の直接的な帰結として、「変更はプロパティ値の単位でしか原子的にならない」という性質があります。テキスト値が B だった状態から、片方が AB に、もう片方が BC に同時に変更した場合、最終的に残るのは AB か BC のどちらかであり、ABC にはなりません。Figmaはテキストエディタではなくデザインツールなので、この挙動で十分と割り切っています。
「flickering」を防ぐ工夫
もう一つ重要なのが、クライアント側での見え方の制御です。
Figmaはレスポンシブに感じさせるため、自分が行った変更をサーバーの確認を待たずに即座に画面に反映します(楽観的更新)。しかしこれをそのままにして、サーバーから届く変更もそのまま即座に反映してしまうと、「まだサーバーに承認されていない自分の変更」が「サーバーから届いた(相対的に古い)値」で一瞬上書きされてしまう、いわゆる「flickering(チラつき)」が起きます。
Figmaの対処はシンプルです。「自分がまだ確認を受け取っていない変更」と衝突するサーバーからの変更は、意図的に無視(破棄)します。自分の変更こそが、自分が知っている中で最も新しい変更だからです。この一手間だけで、チラつきのないスムーズな体験が実現されています。
対策なし(チラつく)
Figmaの対策(無視して確定を待つ)
実務に置き換えると
受託のWebアプリケーション開発という自分の業務に置き換えて考えたとき、今回の設計で特に参考になったのは、「WebSocketを使うこと」そのものよりも、何を競合の単位として扱うかを先に決めることの重要性です。
Figmaではオブジェクト全体ではなく、「オブジェクト × プロパティ」という細かい単位で変更を扱っています。そのため、同じオブジェクトを複数人が触っていても、変更しているプロパティが異なれば衝突しません。
自分がWebアプリに共同編集機能を実装するとしたら、まず通信方式を決めるのではなく、
- どの状態は独立して変更できるのか
- 本当に競合として扱う必要がある範囲はどこか
- サーバー確定前にUIへ反映してよい操作は何か
を整理するところから始めるべきだと感じました。
これはホワイトボードやフォームビルダー、管理画面など、複数人が同じデータを扱うWebアプリにも応用できる考え方だと思います。
8. オブジェクトの生成・削除
オブジェクトの生成と削除は、明示的な操作として扱われます。存在しないIDにいきなりプロパティを書き込んでオブジェクトを"発生させる"ことはできません。
オブジェクトの削除は、サーバー上のそのオブジェクトに関する全データを消す、という思い切った実装になっています。削除された内容はサーバーではなく、削除を実行したクライアント側のUndoバッファに保持されます。これにより、長く使われるドキュメントがどんどん肥大化していくのを防いでいます。
またID生成については、各クライアントに一意なクライアントIDを割り当て、新規オブジェクトのIDにそのクライアントIDを含めることで、サーバーに問い合わせなくてもクライアント側だけでID衝突なくオブジェクトを作成できるようになっています。オフラインでもオブジェクト作成ができる必要があるため、これはサーバー側でIDを発行する方式では実現できません。
9. ツリー構造の同期
Figmaのブログの中で「一番複雑だった」と語られているのが、木構造(親子関係)の同期です。
設計上のゴールは2つありました。
- あるオブジェクトの親を変更する操作(reparenting)が、そのオブジェクトの無関係な他のプロパティの変更と衝突しないこと
- 同じオブジェクトに対する2つの同時reparenting操作が、ツリー上にそのオブジェクトの複製を生まないこと
「reparenting = 一度削除して新しいIDで再作成する」という実装も考えられますが、それだとオブジェクトのアイデンティティが変わってしまい、並行編集が失われてしまいます。そこでFigmaは、親へのリンクを子オブジェクト自身のプロパティとして持たせるという設計を採用しました。オブジェクトのIDは変わらず、また「1つのオブジェクトが複数の親から子として参照される」という不整合も起きません。
ただしこれには新たな問題があります。親へのリンクは単なる有向グラフの辺でしかないため、何も制約がないと循環参照が起こり得ます。たとえば「AをBの子にする」変更と「BをAの子にする」変更が同時に行われると、AとBが互いの親になってしまいます。
ここで問題になるのがサーバーとクライアントの非対称性です。
- サーバー側:循環を作る変更はそのまま拒否すればよい
- クライアント側:サーバーが権威であるため、サーバーから届いた変更を拒否することができない
つまり、あるクライアントが「BをAの下に」という未確認の変更をローカルに反映した状態で、サーバーから確定済みの「AをBの下に」という変更を受け取ると、そのクライアント上では一時的に循環が発生します。この時点ではクライアントはまだ、自分の「BをAの下に」という変更がサーバーに拒否されることを知りません。
Figmaの解決策は、このような一時的な循環が起きた場合、該当のオブジェクトたちを一旦ツリーから外し、サーバーが変更を拒否してオブジェクトが正しい位置に戻されるまで、お互いを親とした孤立状態にしておく、というものです。オブジェクトが一瞬見えなくなるという多少の犠牲を払ってでも、複雑な循環検出ロジックをクライアントに実装しないというシンプルさを優先した判断です。
兄弟の並び順:Fractional Indexing
木構造ができても、同じ親を持つ子同士の並び順を決める必要があります。Figmaはここで Fractional Indexing(分数インデックス) という手法を使っています。
各オブジェクトの、親の中での位置を0〜1の間の分数として表現し、その値でソートして並び順を決めます。2つのオブジェクトの間に新しいオブジェクトを挿入したい場合は、単純にその2つの位置の平均値を新しいオブジェクトの位置とすればよい、というシンプルな仕組みです。
重要なのは、この「位置」と「親へのリンク」は必ず1つのプロパティとしてまとめて保存され、原子的に更新される必要があるという点です。親が変わったのに、古い親のもとでの位置情報だけが引き継がれてしまっては意味がないためです。
なお、実際のFigmaでは64bit浮動小数点ではなく任意精度の分数を文字列として保持し、繰り返し挿入しても精度が尽きないようにしています。

10. Undo/Redoの設計思想
マルチプレイヤー環境でのUndo/Redoは、直感に反する難しさがあります。自分が編集したオブジェクトを、他の人も編集していた場合にUndoしたら何が起きるべきか、という問いに答えなければなりません。
Figmaが行き着いた指針はシンプルで、「たくさんUndoして、何かをコピーして、Redoで現在に戻ってきたとき、ドキュメントの状態は変化していないべき」というものです。当たり前のようですが、シングルプレイヤーの感覚で「Redo = 自分がやったことを戻す」と実装してしまうと、その間に他の人が行った変更を意図せず上書きしてしまう危険があります。
これを実現するため、Figmaでは「Undo操作を行うと、そのタイミングのRedo履歴が更新される」「Redo操作を行うと、そのタイミングのUndo履歴が更新される」という形で、履歴そのものを動的に組み替える設計になっています。
11. まとめ
改めて冒頭の比較動画に立ち戻ると、Figmaの共同編集がなめらかに感じられる背景にある、同期・競合解決の設計の一端が見えてきます。
- サーバー到着順を利用した、LWW register相当のプロパティ同期
- 楽観的更新と、それによって生じるチラつきを防ぐ一手間
- 循環参照という厄介なエッジケースを、複雑なロジックではなく「一時的な割り切り」で吸収する設計判断
- Fractional Indexingによる、挿入コストの低い兄弟順序管理
これらはどれも「汎用的な理論をそのまま厳密に適用する」のではなく、「自分たちのユースケースでは何が本当に必要か」を見極めた上で、複雑さとユーザー体験のバランスを取った設計判断の積み重ねでした。CRDTという分散システムの理論を、中央集権的なアーキテクチャの制約の中でどう"いいとこ取り"するか、という視点は、協調編集機能に限らず参考になる考え方だと思います。
次回は、ここで紹介した設計をもとに、Node.js + WebSocketで最小構成のマルチプレイヤー同期システムを実装し、実際に動かしながら検証してみます。
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!


