mcpField
C
conductor-rewrite-performance

conductor-rewrite-performance

github.com/boraoztunc開発431165 回インストール11 週間前に更新
リポジトリ

conductor-rewrite-performance について

ローカルファーストのデスクトップアプリを開発する React 開発者向けのパフォーマンス改善スキルです。描画や遅延、プロセスのメモリ管理に加え、Tauri のデバッグ方法も扱います。

主な機能

  1. 01ブラウザー経由で React をプロファイル
  2. 02ルーター参照の安定化
  3. 03ストリーミングリストの仮想化とメモ化
  4. 04同期処理をクリティカルパスから移動
  5. 05アイドル状態の子プロセスを終了

活用シーン

  1. 01ナビゲーション時の連鎖的な再描画を調査
  2. 02長いストリーミングチャットリストを高速化
  3. 03Chrome DevTools で Tauri UI を分析
  4. 04アイドル中の Agent セッションのメモリを削減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/boraoztunc/skills --skill conductor-rewrite-performance

インストール後、Claude Code で/conductor-rewrite-performanceを入力して読み込まれたか確認します。

詳細

このスキルを使う場面

  • React を使ってローカルファーストのデスクトップアプリを構築しており、パフォーマンスのボトルネックが発生している場合
  • ナビゲーションやルートの変更によって、マウント済みの複数のコンポーネントで連鎖的に再レンダリングが発生する場合
  • チャットインターフェースや長いリストでコンテンツをストリーミングしており、再レンダリングが遅くなっている場合
  • Tauri を使っていて、React のパフォーマンスをプロファイリングする必要があるものの、Chrome DevTools にアクセスできない場合
  • 重いビュー(サイドバー、ナビゲーション、チャット、ターミナル、エディター)が複数同時にマウントされており、状態の変更ごとにすべてが再レンダリングされる場合
  • システムメモリを使い切らずに、複数の重量級子プロセスを管理する必要がある場合

基本原則

  1. ローカルファーストにすることで、パフォーマンス問題の一群を根本から解消できます。 SQLite を信頼できる唯一の情報源とし、UI がネットワーク待ちにならない場合、ボトルネックはレンダリング層へ移ります。不要な再レンダリングはすべて、ユーザーが感じる最も遅い処理になります。

  2. 不安定な参照は、コンポーネントツリー全体に再レンダリングを連鎖させます。 ルーターフックがレンダリングのたびに新しいオブジェクトを返すと、値が変わっていなくても、それを読み取るすべてのコンポーネントが再レンダリングされ、その再レンダリングがすべての子コンポーネントに伝播します。

  3. ストリーミングリストを高速化するには、仮想化とメモ化を組み合わせるしかありません。 画面に表示されている項目だけをレンダリングし(500 件ではなく 15 件のメッセージ)、変更中の項目だけが再レンダリングされ、ほかの項目はメモ化されたままになるようにします。

  4. 最も速い処理は、ユーザーが待たずに済む処理です。 高コストな同期処理(Git のチェックポイントなど)をクリティカルパスから外し、最初のトークンがすぐに届くようにします。

テクニック

Tauri ブリッジをシム化して Chrome でプロファイリングする

Tauri の WebView で React のパフォーマンスをデバッグするために Safari の Web インスペクターを使えない場合は、同じクライアントを Chrome で実行し、DevTools に完全にアクセスできる開発用シムを作成します。

// Conductor's UI reaches the Rust core through Tauri's invoke() bridge.
// In a real browser there's no Tauri runtime: __TAURI_INTERNALS__ is
// undefined and every invoke() throws. So in dev we shim that single
// entry point and boot the exact same client in Chrome, where the
// Chrome profiler AND the React DevTools profiler both work.

import { invoke as tauriInvoke } from "@tauri-apps/api/core";

export function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
  // Packaged app: use the native bridge.
  if ("__TAURI_INTERNALS__" in window) return tauriInvoke<T>(cmd, args);

  // Dev in Chrome: stand in for the Rust backend. Proxy to a dev server
  // running the real commands, or return canned data for the surface
  // you're profiling.
  return fetch(`/__backend__/${cmd}`, {
    method: "POST",
    body: JSON.stringify(args ?? {}),
  }).then((r) => r.json());
}

手順:

  1. UI がネイティブ層と通信するために使用するブリッジ関数を 1 つ特定します(例:Tauri の invoke())
  2. ネイティブランタイムオブジェクト(__TAURI_INTERNALS__)の有無を確認します
  3. 本番環境では、実際のブリッジを呼び出します
  4. 開発環境では、ローカルサーバーにプロキシするか、モックデータを返します
  5. Chrome でクライアントを起動し、React DevTools Profiler を使ってボトルネックを特定します
react-router を TanStack Router に置き換えて参照を安定させる

ナビゲーションによって新しいパラメーターや検索用の参照が作られ、再レンダリングが連鎖する場合は、構造共有と安定した参照を提供するルーターに切り替えます。

react-router を使う場合:

// Before with react-router

import { useSearchParams } from "react-router-dom";

function WorkspaceView() {
  const [searchParams] = useSearchParams();

  // useSearchParams() returns a NEW URLSearchParams every render,
  // and this parsed object is a new reference every render too.
  const filters = {
    agent: searchParams.get("agent"),
    status: searchParams.get("status"),
  };

  useEffect(() => {
    refetchAgents(filters);
  }, [filters]); // new object each render → fires on EVERY render

  return <AgentList filters={filters} />; // child re-renders every time
}

TanStack Router を使う場合:

// After with tanstack router

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/workspace")({
  // parse + validate search once, fully typed
  validateSearch: (s): { agent?: string; status?: string } => ({
    agent: s.agent as string | undefined,
    status: s.status as string | undefined,
  }),
});

function WorkspaceView() {
  // structural sharing: SAME reference unless agent/status actually change
  const filters = Route.useSearch();

  useEffect(() => {
    refetchAgents(filters);
  }, [filters]); // fires only when a value really changes

  return <AgentList filters={filters} />; // no re-render!
}

手順:

  1. createFileRoute でルートを定義し、検索パラメーターを解析して型付けする validateSearch 関数を追加します
  2. useSearchParams() を Route.useSearch() に置き換えて、安定した参照を取得します
  3. 解析済みパラメーターを囲む手動の useMemo ラッパーを削除します。構造共有によって処理されます
  4. React DevTools Profiler で、実際に変更があったコンポーネントだけがナビゲーション時に再レンダリングされることを確認します
ストリーミングチャットリストを仮想化し、メモ化する

数百件のメッセージがあるチャット UI で、トークンごとに再レンダリングが発生する場合は、仮想化(表示されている項目のみをレンダリング)とメモ化(変更のない項目をスキップ)を組み合わせます。

変更前:

// Before: the simple approach where each message/token rerenders everything

function Chat({ messages }) {
  return (
    <div>
      {messages.map((m) => (
        <Message key={m.id} message={m} /> // all N re-render on each token
      ))}
    </div>
  );
}

変更後:

// After: virtualize the list + memoize each row

const Message = React.memo(function Message({ message }) {
  return <MarkdownContent text={message.content} />; 
});

function Chat({ messages }) {
  // VirtuosoMessageList is a component from Virtuoso
  return (
    <VirtuosoMessageList
      data={messages}
      itemContent={(_, m) => <Message message={m} />}
    />
  );
}

手順:

  1. 各メッセージコンポーネントを React.memo でラップし、props が変わったときだけ再レンダリングされるようにします
  2. .map() ループを react-virtuoso の VirtuosoMessageList(または汎用リスト用の Virtuoso)に置き換えます
  3. メッセージ配列全体を data として渡し、itemContent で各項目をレンダリングします
  4. スクロール位置のアンカリング、ストリーミング中の最下部への追従、動的な高さの計測を Virtuoso に任せます
  5. ストリーミング中のメッセージだけが再レンダリングされ、ほかのメッセージはメモ化されたままになることを確認します
クリティカルパス上の時間のかかる同期処理をバックグラウンドに移す

ユーザー入力から最初のレスポンストークンまでの間に、ブロッキング処理(git のチェックポイントなど)がある場合は、バックグラウンドに移します。

パターン:

  • 変更前: ユーザーが Enter キーを押す → 同期的に git add -A を実行 → プロンプトを送信 → 最初のトークンが届く
  • 変更後: ユーザーが Enter キーを押す → すぐにプロンプトを送信 → 最初のトークンが届く → バックグラウンドでチェックポイントを実行

手順:

  1. クリティカルパス上の同期処理(ファイルのスナップショット、データベースへの書き込みなど)を特定します
  2. ユーザー向けのアクションが完了した後、非同期で処理を実行します
  3. 次のチェックポイントが必要になる前に、バックグラウンドタスクが完了することを確認します
  4. 再開時にチェックポイントが必要な場合は、セッションをディスクから再開できるよう --resume <uuid> フラグを渡します
複数の負荷が大きい子プロセスのメモリを管理する

アプリで複数の長時間実行プロセス(エージェントのセッションなど)を起動する場合は、アイドル状態のプロセスを終了し、必要に応じて再開します。

手順:

  1. 各子プロセスの最終アクティビティのタイムスタンプを記録します
  2. 一定時間(例:5 分)操作がなければ、プロセスを終了してメモリを解放します
  3. セッションの状態を一意の識別子(例:--resume <uuid>)とともにディスクに保存します
  4. ユーザーがそのワークスペースに戻ったら、再開フラグを指定して新しいプロセスを起動します
  5. Activity Monitor / Task Manager で、アイドル状態のワークスペースがメモリを保持していないことを確認します

アンチパターン

❌ 不安定な参照をすべて手動で useMemo で囲まないでください—TanStack Router の構造的共有など、安定した参照を提供するライブラリに切り替えて、根本原因を解消します。

❌ リスト項目をすべてレンダリングして、CSS の overflow: scroll に頼らないでください—仮想化して、表示されている項目だけが DOM に存在するようにします。

❌ クリティカルパス上で同期的な git 操作やファイル I/O を行い、UI スレッドをブロックしないでください—バックグラウンドタスクに移します。

❌ 負荷が大きい子プロセスを際限なく蓄積させないでください—アイドル時の終了と、必要に応じた再開を実装します。

❌ webview が Chrome DevTools に対応していないからといって、プロファイリングを省略しないでください—ネイティブブリッジをシムで置き換え、開発時には Chrome で同じクライアントを実行します。

❌ ローカルファーストならデフォルトで高速だと思い込まないでください—ネットワーク遅延がなくなると、レンダリングのボトルネックが新たな制約になります。

出典

The Conductor Rewrite: What They Changed to Make It Fast

コメント