The Conductor Rewrite: What They Changed to Make It Fast
Comments
The Conductor Rewrite: What They Changed to Make It Fast I recently saw a post on X from Charlie Holtz saying, “We’ve rebuilt Conductor from scratch to make it twice as fast,” and it immediately caught my attention. I needed to know exactly what they did to improve the performance. Fast forward a couple weeks, I had the chance to sit down with Jackson de Campos, one of the founders of Conductor. He walked me through the technical decisions behind the rewrite, the stack powering it, and some of the early learnings from building the company, including the fact that they barely knew React when they first started Conductor! First, build for yourself As a founder myself, I know how important the early days are. After speaking with Jackson, it's clear the team at Conductor got the fundamentals right. Most importantly, they built the product for themselves. They live with the same workflows, frustrations, and edge cases as their users, which makes every pain point impossible to ignore. As they put it on their website: "We built Conductor using Conductor." You can see it in how fast they move. Their changelog updates constantly, each one full of fixes and refinements that only come from using the product all day. That cadence is the real payoff of dogfooding: when you feel a problem yourself, it stops being a backlog ticket and becomes something you fix this afternoon. The initial version of Conductor was already pointed in the right direction, and they hit product-market fit quickly, but they knew performance was becoming a bottleneck. The key features, chat, worktrees, and code viewing, were frustratingly slow to use. That's when the rewrite was born: a deep dive into performance aimed at every pain point they were feeling across every surface of the app. The story of Conductor's tech stack is consistent before and after the rewrite. The fundamentals remain the same: a local-first React application wrapped in Tauri. However, they made several key changes that unlocked amazing performance gains. Before diving into what they did, here's a look into their current tech stack to better understand what Conductor is working with: Frontend React 19 + react-dom (UI runtime) TypeScript (frontend language) Vite (build; per-package content-hashed chunks) @tanstack/react-router (type-safe routing; stable refs) TanStack Query (primary data layer) Zustand (in-memory state stores) react-virtuoso (chat + list virtualization) @tanstack/react-virtual (secondary virtualization surface) Tiptap + ProseMirror (rich-text composer) Shiki (code highlighting) xterm.js + anser (terminal renderer; ANSI parsing) react-hook-form + Zod (forms + validation) Radix UI primitives (popovers, menus, dialogs) Floating UI (popover positioning) cmdk (command palette) sonner (toasts) vaul (drawer) react-resizable-panels (split panes) lucide-react (icons) @dnd-kit (drag and drop) Tailwind CSS + tailwind-merge (styling; no CSS-in-JS runtime) marked + remark/rehype (markdown rendering) react-markdown (markdown -> React) fuzzysort / fuse.js (client-side fuzzy search) Data + native shell SQLite (local source of truth) Tauri 2.6.2 (Rust shell + native WebKit webview) Rust core (spawns + supervises agent processes) Bundled agent CLIs (Claude Code agents run as child processes) Bun (runtime for the agent processes; was Node) What they got right from the start Before getting into the rewrite, it's worth pointing out the choices that made Conductor fast from day one. The whole app runs locally. SQLite is the source of truth for workspaces, chat history, checkpoints, and settings. None of that lives in a remote server, so the UI never waits on the network. This is actually a stronger version of the local-first architecture I wrote about in my Linear piece. Linear caches a remote Postgres in IndexedDB and reconciles in the background. Conductor doesn't have a remote database to cache from. The local store is the database. As I hammered in the Linear article, the more network requests you can eliminate, the faster the app feels. Next, they chose Tauri over Electron as the native shell. The Tauri vs Electron debate is usually framed as a performance contest, but in practice the deciding factor is architecture fit. OpenCode, for example, moved its desktop app off Tauri and onto Electron, not because Tauri was slow, but because their TypeScript client-server design fit Node's runtime better once they dropped their Bun dependencies. Conductor's architecture pulls the other way: a Rust core spawning agent CLIs, with the UI in a very performant native WebKit webview (Safari on macOS). For them, Tauri's approach is the better choice: smaller bundle, faster cold start, and snappier UI rendering. The benefit of a local-first app is that you eliminate an entire category of performance problems: the ones caused by the network (yes, there it is again). But performance is relative. Remove the biggest bottleneck and the next one comes into focus. With the network gone, every unnecessary re-render, every janky scroll, every dropped frame is suddenly the slowest thing the user feels. The friction moves up into the UI itself. So before they could make anything faster, they had to find exactly where that friction was hiding. Measure twice, cut once Before they could make Conductor faster, they had to see where the time was actually going. That turned out to be harder than it sounds, because of where Conductor runs. This is the tradeoff with Tauri. Electron ships its own copy of Chromium, so you inherit Chrome's entire toolchain for free: the performance profiler, the memory tools, and crucially the React DevTools extension. Tauri doesn't bundle a browser. It renders your UI in the operating system's webview, which on macOS is WKWebView, the same engine as Safari. That's what keeps the bundle small and the cold start fast (exactly why Conductor chose it), but it means the only profiler you get out of the box is Safari's Web Inspector. And Safari's Web Inspector is not enough for a React performance problem. It has a JavaScript profiler, but it profiles JavaScript, not React. It can tell you a function ran for 12ms; it can't tell you that WorkspaceView re-rendered 400 times because a prop reference changed on every navigation. For that you need the React DevTools profiler, which records renders at the component level and shows you what re-rendered and why. And the React DevTools browser extension can't load inside a WKWebView at all. So the one tool that would have pointed straight at their bottleneck was the one tool they couldn't run. That left two options: build a custom profiler, or get the React client into an environment where the real React DevTools already works. They went with the second, and the trick is that Conductor's frontend is just a Vite single-page app. Nothing about rendering a chat message or a file tree needs the native shell. The webview only matters when the UI talks to the Rust core, which it does through Tauri's invoke() bridge. So if you can stand in for that one bridge, the exact same client runs in plain Chrome. // 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 (cmd: string, args?: Record ): Promise { // Packaged app: use the native bridge. if ("__TAURI_INTERNALS__" in window) return tauriInvoke (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()); } With the bridge shimmed, the production React client boots in Chrome, and now they have the full Chrome performance profiler plus the React DevTools profiler pointed at the real app. That's how the bottlenecks stopped being a guess. The profiler made it obvious the slow part wasn't the data layer at all. It was React, re-rendering far more than it needed to. The rewrite performance improvements Remember, the bottleneck never disappears, it just moves. The first one they hit was re-rendering. Because of the way Conductor's UI is designed they have multiple views mounted at once such as the sidebar, nav, chat, terminal, and editor. With React Router, every navigation produces fresh references for params/search, so every component reading them re-renders even when nothing actuall changed, and those re-renders cascade through the entire app The key highlight from Conductor's changelog was, "Creating tabs, switching workspaces, and rendering files are all 50% faster", and this was largely due to their migration from react-router to @tanstack/react-router . But why exactly did this have such a big impact? Remember, Conductor has multiple heavy views mounted at once so when the user would navigate using react-router a bunch of unstable references would cause them all to either re-render or re-run unneeded effects. // 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 ; // child re-renders every time } The only way to make those references stable is to hand-roll memoization everywhere: const agent = searchParams.get("agent") const status = searchParams.get("status") const filters = useMemo( () => ({ agent
Comments
No comments yet. Start the discussion.