MicroArchitectures
H.Ueda
Programmer
ブログ
Linearはなぜこれほど速いのか?ローカルファーストなアーキテクチャを紐解く
How's Linear so fast? A technical breakdown という記事を読み、Linearが提供するあの独特の操作感がどのような技術的背景によって成り立っているのか、実務の視点から自分なりに整理してみました。
SPAやるなら、これ位はやんないとね。現在のHTTPやJSのアップデートで性能的にSPAを選択しなくっても簡単な処理ならMPAでも十分早いんだからね。一時期、CouchDBを使った同期を考えていたけど、まだ有効な手段かな。
プロジェクト管理ツールのLinearを触ってみると、画面遷移や課題の更新が異様に速いことに驚かされます。一般的なWebアプリでは、ボタンを押してからサーバーの応答を待つまでの「間」がありますが、Linearにはそれがほとんどありません。
このスピード感は、単にコードを最適化した結果ではなく、設計思想の根幹にある「ローカルファースト」という考え方によって実現されているようです。
ネットワークを「待たない」設計
多くのWebアプリが採用しているのは、クライアント(ブラウザ)がサーバーにリクエストを送り、そのレスポンスを待ってから画面を更新するというモデルです。しかし、この方法ではどうしてもネットワークの遅延(レイテンシ)がボトルネックになります。
Linearはこの伝統的な関係を逆転させ、「ブラウザ側をメインのデータベースとして扱う」という手法を採っています。
従来モデルとLinearモデルの比較
一般的なアプリとLinearの処理の流れを比較すると、以下のようになります。
| 項目 | 従来のWebアプリ (CRUD型) | Linear (ローカルファースト型) |
|---|---|---|
| データの保持先 | サーバー側のデータベース | ブラウザ内のIndexedDB / メモリ |
| UIの更新タイミング | サーバーからの応答後 | ユーザーの操作直後(同期的) |
| ネットワークの役割 | 処理の必須ステップ | バックグラウンドでの同期 |
| オフライン動作 | 基本的に不可 | 可能 |
従来の方式では、どれだけサーバーを高速化してもネットワークの往復時間は削れません。Linearは「ネットワークリクエストをユーザーの操作から切り離す」ことで、この問題を解決していると言えます。
処理のワークフローを可視化する
実際にどのような流れでデータが処理されているのか、Mermaid図で整理してみます。
従来の一般的なWebアプリのフロー
sequenceDiagram
participant User as ユーザー
participant Browser as ブラウザ (UI)
participant Server as サーバー
participant DB as データベース
User->>Browser: 課題を更新
Browser->>Server: PATCH /api/issues/1
Server->>DB: UPDATE query
DB-->>Server: Success
Server-->>Browser: JSON response (200 OK)
Browser->>Browser: 再レンダリング (UI更新)
Browser-->>User: 更新完了を表示
この図のように、ユーザーはサーバーとDBの処理が終わるまで待機することになります。
Linearの同期フロー
sequenceDiagram
participant User as ユーザー
participant Browser as ブラウザ (In-memory/LocalDB)
participant Sync as 同期エンジン
participant Server as サーバー
User->>Browser: 課題を更新
Note over Browser: ローカル状態を即座に書き換え
Browser-->>User: 即座に反映 (UI更新)
Browser->>Sync: トランザクションをキューに追加
Sync->>Server: バックグラウンドで同期 (バッチ処理)
Server-->>Sync: 同期完了
Linearの場合、ユーザーが操作した瞬間にブラウザ内のメモリ(MobXなど)とIndexedDBが更新されます。UIはサーバーの返答を待たずに書き換わるため、体感速度が劇的に向上します。
コードで見る「同期的」な更新
記事の中で紹介されていたコード例を参考に、その違いを見てみます。
従来のWebアプリのイメージ
非同期処理(async/await)がUI更新の前に介在するため、どうしても待ち時間が発生します。
// サーバーの応答を待つ必要がある例
async function updateIssue({ issue }) {
showSpinner(); // ローディング表示
const response = await fetch(`/api/issues/${issue.id}`, {
method: "PATCH",
body: JSON.stringify({ title: issue.title }),
});
const updated = await response.json();
setIssue(updated); // ここでようやくUIが更新される
hideSpinner();
}
Linearのアプローチ
UIの更新を同期的に行い、保存処理は抽象化された「同期エンジン」に任せる形です。
// ローカルの状態を即座に変更する例
issue.title = "新しいタイトル";
issue.save();
issue.save() を呼び出した時点では、まだサーバーとの通信は終わっていないかもしれません。しかし、ブラウザ上のデータはすでに書き換わっているため、ユーザーは次の作業に移ることができます。これは、いわゆる「楽観的更新(Optimistic Update)」をシステム全体で徹底しているようなイメージでしょうか。
同期エンジンという土台
Linearの共同創業者の一人であるTuomas氏は、開発の最初期にこの「同期エンジン」を構築したと語っています。一般的なスタートアップであれば、まずは標準的なRESTやGraphQLでAPIを作る辺りから始めるのが普通ですが、彼らは「速度」を最優先事項として、アーキテクチャそのものを独自に設計しました。
この独自の同期エンジンが、以下のような役割を担っています。
- 変更のバッチ処理: 細かな更新をまとめて効率的にサーバーへ送る。
- 競合の解決: 複数のデバイスで同時に変更があった場合の整合性を保つ。
- 差分同期: 必要なデータだけをWebSocketを通じてやり取りする。
私たちが自分のプロジェクトに活かすには
Linearのようなカスタム同期エンジンをゼロから構築するのは、実際のところ非常にコストが高い作業です。ほとんどのプロジェクトでは、そこまでの作り込みは必要ないかもしれません。
しかし、Linearのような「速さ」に近づくための現実的な手段はいくつかあります。
- TanStack Query (React Query) や SWR の活用: これらには「楽観的更新」の機能が備わっており、サーバーの応答を待たずにUIを更新する仕組みを比較的簡単に導入できます。
- 状態管理の工夫: 全てのデータをサーバーから取得するのではなく、一時的な状態をクライアント側で賢く管理することで、不要なネットワーク通信を減らすことができます。
結局のところ、アプリを速く感じさせる秘訣は、「ユーザーにネットワークを意識させないこと」に尽きるのだと思います。ローディングスピナーを表示する代わりに、まずはUIを動かしてしまう。そんな「ローカルファースト」な発想を取り入れてみると、ユーザー体験はぐっと良くなるのではないでしょうか。