動画で読む
まず結論 — Linear の速さは「ネットワークを待たない」設計の積み重ね
Linear のフロントエンドが速いのは、特定の魔法のライブラリのおかげではありません。「ユーザー操作のたびにサーバーを待つ」という Web の前提を、ローカルファースト同期エンジンで丸ごと外しているからです。performance.dev が公開した技術分解(How is Linear so fast?)を読みながら、私はこの一点に尽きると思いました。
速さの内訳は大きく3層に分かれます。データをブラウザ内に置く同期エンジン、UI を即座に更新する楽観的更新と粒度の細かい再描画、そして初回ロードを縮める徹底したバンドル最適化です。順に運用者目線で読んでいきます。
ローカルファースト同期エンジン — DB をブラウザに置く
Linear の核心は、データベースの実体をブラウザの IndexedDB(ブラウザ内蔵の永続ストレージ)に置いていることです。サーバーが正本でクライアントがそのキャッシュ、という普通の構図を反転させています。
動きはこうです。ユーザーが Issue を編集すると、変更はまずローカルに適用され、そのあと非同期でサーバーに push される。サーバーは受け取った差分(delta)を WebSocket で他のクライアントへ broadcast する。つまり画面の反応にネットワークの往復が挟まりません。私が普段触っている Rails + Sidekiq + Redis の構成だと、書き込みは「リクエスト→DB→レスポンス」が当然の直列で、その往復こそがユーザーの待ち時間です。Linear はその往復を体感の外へ追い出しています。
もうひとつ効いているのが遅延ハイドレーションです。同期エンジンはデータを JavaScript のバンドルと同じようにチャンク分割し、Issue や Comment のような重いテーブルは必要になってから読み込みます。おかげで Issue が1万件あるワークスペースでも、100件のワークスペースとほぼ同じ速さで起動します。データ量に対して起動時間がほぼ一定、というのは運用していて一番ありがたい性質です。
楽観的更新と「読んだ場所だけ」再描画する仕組み
速さの2層目は、楽観的更新(optimistic update、サーバー確認を待たず先に UI を変える)と、再描画の粒度の細かさです。スピナーを見せずに即座に結果を出すのは、この2つの合わせ技です。
楽観的更新そのものは目新しくありません。差を生むのは再描画の粒度のほうです。Linear は MobX の observable をプロパティ単位で使っていて、ある Issue の1フィールドが変わると、そのフィールドを読んでいるコンポーネントだけが再描画されます。50件の Issue がリアルタイムに更新されても、リスト全体ではなく該当する50セルだけが描き直される。これは地味ですが大きい話です。私も以前、共同編集っぽい画面を作って、他人の更新が来るたびにリスト全体が再描画されて指がカクついた経験があります。原因はまさにここで、状態の購読が粗すぎたせいでした。観測する単位を細かくするだけで、同時編集の体感は別物になります。
アニメーションへの線引きも徹底しています。Linear が動かすのは transform と opacity という合成(composite)だけで完結するプロパティに限られ、レイアウトを再計算させるプロパティはアニメーションに使いません。さらに遷移時間は 100〜250ms と業界標準よりかなり短く、出現は一瞬・消えるのは150ms、と非対称にしている。速さは計測値だけでなく、こうした「待たされた感を出さない」演出の設計にも宿っています。
初回ロードを縮める — バンドルとブートの最適化
3層目は初回ロードです。Linear は約21MB のミニファイ済み JavaScript を数百のルート単位チャンクに分割し、それでも初回表示を速く保っています。量で殴っているのに速い、という矛盾を設計で解いています。
効いている工夫を並べると、まずモダンブラウザだけを対象にしてレガシー対応と polyfill を捨て、出荷コードを50%削減・圧縮後で30%小型化しています。次に index.html で重要チャンクを modulepreload で並列先読みし、本来直列だった読み込みの滝(waterfall)を1回の並列バッチに畳んでいる。ログイン後は Service Worker が約1,200個のハッシュ付きアセットを precache し、2回目以降のナビゲーションを即時かつオフライン可能にします。
私がいちばん面白いと思ったのは認証の扱いです。Linear はセッションを検証してから描画するのではなく、localStorage に ApplicationStore があればそのまま即座に描画し、セッションの検証は WebSocket のハンドシェイクなどで後追いする。いわゆる happy path(正常系を仮定する)戦略です。普通なら「未ログインだったらどうする」を先に考えてしまうところを、Linear は「ほとんどのケースはログイン済みなのだから、まず描いてしまえ」と割り切っている。正直に言うと、自分のコードレビューならこの順序は一度突き返しそうです。でも体感速度のためにあえて楽観に倒す、という判断の筋は通っています。
運用者として真似できること、真似できないこと
最後に、自分のプロダクトへ持ち帰れる線を引きます。同期エンジン丸ごとの移植は現実的ではありませんが、思想の一部は今日から使えます。
真似できるのは、再描画の粒度を細かくすること、合成プロパティだけでアニメーションすること、初回 HTML にアプリシェルとブートに要る情報を埋め込んでおくこと。このあたりは既存スタックのままでも効きます。何を速くすべきかは勘ではなく計測から決めるべきで、その姿勢は自動化の対象は測定で決めるで書いた話とそのまま地続きです。一方で21MB を捌くチャンク分割やビルドの作り込みは、ツールチェーン側の体力に依存します。バンドラやツールチェーンの選定がプロダクトの速度を左右する点はVoidZero と Vite ツールチェーンの独立性で触れたとおりで、Linear の速さもこの土台の上に立っています。
よくある質問
- Linear が速いのは何が一番効いているのですか?
- ローカルファースト同期エンジンです。データベースの実体をブラウザの IndexedDB に置き、変更をまずローカルへ適用してから非同期でサーバーへ送るため、ユーザー操作にネットワークの往復が挟まりません。これが体感速度の土台になっています。
- Issue が大量にあっても遅くならないのはなぜですか?
- 同期エンジンがデータをチャンク分割し、Issue や Comment のような重いテーブルを必要時にだけ遅延ハイドレーションするためです。これにより1万件のワークスペースでも100件とほぼ同じ速さで起動します。
- 自分のプロダクトで真似できる部分はありますか?
- 再描画の粒度を細かくする、合成プロパティ(transform / opacity)だけでアニメーションする、初回 HTML にアプリシェルとブート情報を埋め込む、の3つは既存スタックのままでも効きます。同期エンジン全体の移植は現実的ではありません。