Real-Time Backend

Design: Figma Multiplayer

В 2022 году Adobe заплатила USD 20 миллиардов за Figma. Не за пиксели и не за UI-кит. За архитектуру multiplayer, которую никто не смог скопировать за 6 лет. Как они это сделали?

  • Figma обрабатывает миллионы concurrent сессий. Один документ Airbnb Design System содержит 15,000+ компонентов - и редактируется 50 дизайнерами одновременно без конфликтов.
  • Google Docs выбрал тонкий сервер + умный клиент для текста. Figma выбрала fat WASM-клиент + stateful сервер для canvas. Разные задачи требуют разных архитектур.
  • Linear, Miro, FigJam, Canva - все строят multiplayer canvas по похожим принципам: OT или CRDT, WebSocket, spatial indexing. Понимание Figma - это понимание целого класса систем.
  • Notion купил за USD 2B, Miro поднял USD 400M. Multiplayer collaborative tools - один из самых дорогих сегментов SaaS. Архитектура напрямую влияет на стоимость продукта.

Архитектура Figma Multiplayer

В 2016 году Figma привлекла USD 14M и сделала ставку на браузер. Конкуренты смеялись: «нельзя сделать Photoshop в браузере». Через 6 лет Adobe купила Figma за USD 20B. Ключ - не WebGL и не React. Ключ - архитектура multiplayer, которую конкуренты не смогли скопировать.

Figma построена на трёх слоях. **Клиент** (браузер) держит весь документ in-memory через WebAssembly-движок на C++. **Сервер синхронизации** - stateful процессы, каждый из которых владеет одним документом целиком. **Persistence layer** - PostgreSQL + S3 для snapshots. Это не microservices. Это намеренно monolithic ownership.

Каждый Figma-документ живёт в памяти ровно одного сервера. Маршрутизация через consistent hashing по document_id. Если сервер падает - документ переезжает на другой узел, клиенты переподключаются. Stateful single-owner = простая модель конкурентности.

Сравни с Google Docs: там сервер - тонкий relay, клиент не держит весь документ. Figma выбрала fat client + smart server, потому что canvas-документы весят 50-500 MB и рендер требует GPU. Латентность сети на каждый пиксель - смерть.

  • Single-owner per document - нет distributed locks, нет split-brain
  • WASM-движок на клиенте - рендер офлайн, без RTT на каждое действие
  • WebSocket постоянный - push от сервера без polling
  • Ops-based sync - передаются операции, не снапшоты

Почему Figma держит каждый документ в памяти одного сервера, а не распределяет по кластеру?

Canvas Rendering в браузере

Figma рендерит canvas через WebGL, а движок документа написан на C++ и скомпилирован в WebAssembly. Это не performance hack - это фундамент. С++ позволяет держать 10,000+ слоёв без GC-пауз, которые убивают анимацию.

Документ хранится как **дерево узлов** (SceneGraph). Каждый узел - frame, component, vector, text. При изменении узла пересчитывается только поддерево от этого узла до корня. Это классический **dirty-flag propagation**: помечаем узел dirty, при следующем frame traversal перерисовываем только грязные узлы.

Figma использует два canvas: один для основного контента (WebGL), второй для UI-оверлея (2D canvas API) - курсоры других пользователей, selection handles, комментарии. Разделение позволяет обновлять cursor positions в 60fps без ре-рендера сцены.

Текст рендерится через собственный движок, а не через DOM. Причина: CSS text rendering не даёт pixel-perfect совпадения между разными ОС. Figma требует чтобы дизайн выглядел одинаково у всех участников сессии. Для этого нужен полный контроль над шейпингом глифов.

  • DOM / SVG rendering — Простой API, но медленный при 1000+ элементов. Браузер не знает о layout приложения - оптимизирует вслепую.
  • WebGL + WASM (Figma) — Полный контроль над GPU pipeline. Batching draw calls. Нет GC-пауз. Требует написать свой text/bezier renderer.

Зачем Figma держит два отдельных canvas-слоя (WebGL + 2D canvas API)?

Протокол синхронизации: OT в реальном времени

Центральная проблема multiplayer: два пользователя одновременно редактируют один объект. User A двигает прямоугольник вправо на 50px. User B одновременно двигает его вниз на 30px. Какой результат корректен? Оба одновременно - это **конкурентные операции**.

Figma использует **Operational Transformation (OT)**. Каждое действие пользователя - это операция с типом, target-узлом и параметрами. Операции коммутируются через сервер. Сервер - единственный арбитр порядка. Он присваивает каждой операции глобальный sequence number и транслирует всем клиентам.

Figma применяет optimistic updates: клиент немедленно применяет свою операцию локально, не дожидаясь ответа сервера. Если сервер отклоняет (conflict) - клиент откатывает и применяет серверную версию. На практике конфликты редки: пользователи обычно работают в разных частях канваса.

Для undo Figma не хранит историю состояний (это было бы гигабайты). Каждая операция содержит **inverse operation** - операцию, которая отменяет её эффект. Undo = применить inverse в обратном порядке. Это называется **command pattern** с reversibility.

Конкурентные операции: как OT их разрешает

User A (seqNo 1): MOVE node #42, dx=+50, dy=0 User B (seqNo 2): MOVE node #42, dx=0, dy=+30 Сервер видит op1 первым -> broadcast seqNo=1 Клиент B уже применил свой op локально (dx=0, dy=+30) При получении seqNo=1 B трансформирует: результат dx=+50, dy=+30 Оба клиента приходят к одному состоянию.

Почему Figma хранит inverse operation внутри каждой операции вместо полных snapshots состояния?

Viewport Management: видим только то, что нужно

Документ Figma может содержать 100 страниц и 50,000 объектов. Загружать всё при открытии - это минуты. Figma загружает только **visible viewport** плюс небольшой буфер вокруг него. Остальное подгружается по мере скролла и зума.

Реализация через **quadtree spatial index**: весь канвас разбит на квадранты. При рендере traversal quadtree и включаем только узлы, пересекающие текущий viewport. При зуме out - узлы мелкие, включаем LOD (level of detail): показываем упрощённые placeholder вместо детализированных векторов.

Multiplayer viewport: сервер знает viewport каждого участника. Если User B смотрит на страницу 3, а User A редактирует страницу 1 - B не получает ops со страницы 1. Broadcast filtruется по viewport subscription. Это снижает трафик в 10-100x при большом документе.

Cursor других пользователей - отдельный поток данных. Координаты курсора отправляются через WebSocket с throttle 60ms (не per-pixel). На клиенте позиция интерполируется между пришедшими точками - плавное движение без перегрузки канала.

  • Quadtree culling - O(log N) поиск видимых узлов вместо O(N)
  • LOD - упрощённые формы при низком zoom, детали при высоком
  • Viewport subscription - сервер шлёт только ops для видимой области
  • Cursor interpolation - плавность при 60ms throttle

Figma использует peer-to-peer синхронизацию между клиентами для скорости

Вся синхронизация идёт через сервер-арбитр, P2P нет

P2P убирает центральный арбитр - появляется CAP-проблема. Без сервера невозможно гарантировать consistent total order операций. Figma жертвует latency (RTT до сервера) ради correctness. Latency компенсируется optimistic updates на клиенте.

Зачем сервер фильтрует broadcast операций по viewport каждого клиента?

Итоги

  • Single-owner per document: каждый документ живёт в RAM одного сервера - нет distributed locks, simple consistency
  • WASM + WebGL client: C++ движок в браузере даёт 60fps рендер без GC-пауз, full offline capability
  • OT с сервером-арбитром: операции трансформируются через сервер, optimistic updates на клиенте маскируют latency
  • Quadtree viewport culling: загружаем и синхронизируем только видимую область - снижает трафик и память в 10-100x

Связанные темы

Figma Multiplayer объединяет несколько фундаментальных паттернов распределённых систем

  • Operational Transformation / CRDT — Базовый алгоритм разрешения конкурентных операций в collaborative editors
  • WebSocket и Server-Sent Events — Транспортный уровень для real-time push от сервера к клиентам
  • Consistent Hashing — Маршрутизация document_id к конкретному серверу в кластере
  • Spatial Indexing (Quadtree, R-tree) — Эффективный поиск объектов в 2D пространстве для viewport culling

Вопросы для размышления

  • Figma выбрала stateful single-owner сервер. Какие trade-offs это создаёт при server failure - что происходит с документом и пользователями в сессии?
  • OT требует центрального сервера-арбитра, CRDT позволяет P2P. Почему Figma выбрала OT несмотря на то, что CRDT не требует сервера?
  • Viewport subscription снижает трафик, но создаёт edge case: User A делает изменение, User B его не получает (не в viewport), потом B зумирует туда. Как должна система обработать эту ситуацию?

Связанные уроки

  • sd-01-intro
Design: Figma Multiplayer

0

1

Войти