关于 conductor-rewrite-performance
面向使用 React 构建本地优先桌面应用的开发者,提供性能优化方法,涵盖渲染、延迟和进程内存管理,并包括 Tauri 调试方案。
主要功能
- 01通过浏览器桥接分析 React
- 02稳定路由引用
- 03虚拟化并记忆化流式列表
- 04将同步任务移出关键路径
- 05关闭闲置子进程
适用场景
- 01排查导航引发的级联重渲染
- 02加快长篇流式聊天列表
- 03用 Chrome DevTools 分析 Tauri 界面
- 04减少闲置 Agent 会话的内存占用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ boraoztunc/ skills --skill conductor-rewrite-performance安装后在 Claude Code 中输入/conductor-rewrite-performance确认已加载。
详细介绍
何时使用此技能
- 你正在使用 React 构建本地优先的桌面应用,并遇到了性能瓶颈
- 导航或路由变更会导致多个已挂载组件级联重新渲染
- 你有一个会流式输出内容的聊天界面或长列表,重新渲染变得迟缓
- 你正在使用 Tauri,需要分析 React 性能,但无法使用 Chrome DevTools
- 多个繁重视图(侧边栏、导航栏、聊天、终端、编辑器)同时挂载,且都会在状态变化时重新渲染
- 你需要管理多个重量级子进程,同时避免耗尽系统内存
核心原则
-
本地优先可以消除一整类性能问题。 当 SQLite 是数据源,且界面无需等待网络时,瓶颈就会转移到渲染层——每一次不必要的重新渲染,都会成为用户感受到的最慢环节。
-
不稳定的引用会导致整个组件树级联重新渲染。 当路由器 Hook 每次渲染都返回新的对象时,即使值没有变化,所有读取这些对象的组件也会重新渲染,而这些重新渲染还会传播到所有子组件。
-
虚拟化加记忆化是让流式列表保持流畅的唯一方法。 只渲染屏幕上可见的内容(15 条消息而不是 500 条),并确保只有当前正在变化的条目会重新渲染,其余条目保持记忆化状态。
-
最快的操作,是用户无需等待的操作。 将耗时的同步工作(例如 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());
}
步骤:
- 找出界面用于与原生层通信的唯一桥接函数(例如 Tauri 的
invoke()) - 检查原生运行时对象(
__TAURI_INTERNALS__)是否存在 - 在生产环境中,通过该桥接函数调用真实接口
- 在开发环境中,代理到本地服务或返回模拟数据
- 在 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!
}
步骤:
- 使用
createFileRoute定义路由,并添加validateSearch函数来解析搜索参数并为其指定类型 - 将
useSearchParams()替换为Route.useSearch(),以获得稳定的引用 - 移除对解析后的参数手动添加的
useMemo包装——结构共享会处理这些工作 - 使用 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} />}
/>
);
}
步骤:
- 使用
React.memo包装每个消息组件,使其仅在 props 变化时重新渲染 - 将
.map()循环替换为react-virtuoso的VirtuosoMessageList(通用列表可使用Virtuoso) - 将完整的消息数组作为
data传入,并通过itemContent渲染每个条目 - 让 Virtuoso 处理滚动锚定、流式输出期间贴底以及动态高度测量
- 验证只有正在流式输出的消息会重新渲染,其余消息保持记忆化状态
将耗时的同步工作移出关键路径
当阻塞操作(如 git 检查点)处于用户输入和首个响应令牌之间时,将其移到后台执行。
模式:
- 之前: 用户按下回车 → 同步执行
git add -A→ 发送提示词 → 收到首个令牌 - 之后: 用户按下回车 → 立即发送提示词 → 收到首个令牌 → 在后台运行检查点
步骤:
- 找出关键路径上的同步操作(例如,文件快照、数据库写入)
- 在面向用户的操作完成后,异步执行这些操作
- 确保后台任务能在需要下一个检查点之前完成
- 如果恢复会话依赖检查点,请传入
--resume <uuid>标志,以便会话能够从磁盘重启
管理多个重量级子进程的内存
当应用启动多个长期运行的进程(例如,Agent 会话)时,关闭空闲进程,并在需要时恢复。
步骤:
- 跟踪每个子进程最近一次活动的时间戳
- 超过一段时间无活动后(例如,5 分钟),终止进程并回收内存
- 使用唯一标识符(例如,
--resume <uuid>)将会话状态持久化到磁盘 - 用户返回该工作区时,使用恢复标志启动新进程
- 在活动监视器 / 任务管理器中确认空闲工作区不会占用内存
反模式
❌ 不要手动将每个不稳定引用都包装在 useMemo 中——通过改用提供稳定引用的库(如 TanStack Router 的结构共享)来解决根本原因。
❌ 不要渲染所有列表项并依赖 CSS overflow: scroll——使用虚拟化,让 DOM 中只存在可见项。
❌ 不要在关键路径上通过同步 git 操作或文件 I/O 阻塞 UI 线程——将它们移至后台任务。
❌ 不要让重量级子进程无限累积——实现空闲关闭和按需恢复。
❌ 不要因为 Webview 不支持 Chrome DevTools 就跳过性能分析——模拟原生桥接,并在 Chrome 中运行同一个客户端进行开发。
❌ 不要假设本地优先就意味着默认快速——网络延迟消除后,渲染瓶颈会成为新的限制因素。