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. 03用 Chrome DevTools 分析 Tauri 界面
  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 是数据源,且界面无需等待网络时,瓶颈就会转移到渲染层——每一次不必要的重新渲染,都会成为用户感受到的最慢环节。

  2. 不稳定的引用会导致整个组件树级联重新渲染。 当路由器 Hook 每次渲染都返回新的对象时,即使值没有变化,所有读取这些对象的组件也会重新渲染,而这些重新渲染还会传播到所有子组件。

  3. 虚拟化加记忆化是让流式列表保持流畅的唯一方法。 只渲染屏幕上可见的内容(15 条消息而不是 500 条),并确保只有当前正在变化的条目会重新渲染,其余条目保持记忆化状态。

  4. 最快的操作,是用户无需等待的操作。 将耗时的同步工作(例如 git 检查点)移出关键路径,让第一个 token 立即到达。

技巧

调整 Tauri 桥接层,以便在 Chrome 中分析性能

如果无法使用 Safari 的 Web Inspector 调试 Tauri WebView 中的 React 性能,可以创建一个开发环境垫片,让同一个客户端在 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. 找出界面用于与原生层通信的唯一桥接函数(例如 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 验证导航时只有实际发生变化的组件会重新渲染
对流式聊天列表进行虚拟化和记忆化

如果包含数百条消息的聊天界面会在每个 token 到达时重新渲染,请结合虚拟化(只渲染可见条目)和记忆化(跳过未变化的条目)。

之前:

// 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 检查点)处于用户输入和首个响应令牌之间时,将其移到后台执行。

模式:

  • 之前: 用户按下回车 → 同步执行 git add -A → 发送提示词 → 收到首个令牌
  • 之后: 用户按下回车 → 立即发送提示词 → 收到首个令牌 → 在后台运行检查点

步骤:

  1. 找出关键路径上的同步操作(例如,文件快照、数据库写入)
  2. 在面向用户的操作完成后,异步执行这些操作
  3. 确保后台任务能在需要下一个检查点之前完成
  4. 如果恢复会话依赖检查点,请传入 --resume <uuid> 标志,以便会话能够从磁盘重启
管理多个重量级子进程的内存

当应用启动多个长期运行的进程(例如,Agent 会话)时,关闭空闲进程,并在需要时恢复。

步骤:

  1. 跟踪每个子进程最近一次活动的时间戳
  2. 超过一段时间无活动后(例如,5 分钟),终止进程并回收内存
  3. 使用唯一标识符(例如,--resume <uuid>)将会话状态持久化到磁盘
  4. 用户返回该工作区时,使用恢复标志启动新进程
  5. 在活动监视器 / 任务管理器中确认空闲工作区不会占用内存

反模式

❌ 不要手动将每个不稳定引用都包装在 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

评论

支持 Markdown