June 1, 2026

The Conductor Rewrite:
What They Changed to Make It Fast

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 ships constantly, named releases every few days, each one full of fixes and refinements that only come from using the thing 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. The general principle still holds: the more network requests you can eliminate, the faster the app feels.

Querying Conductor's local files showing conductor.db in the list

Next, they chose Tauri over Electron as the native shell. The Tauri-versus-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. For them, Tauri's approach is the better choice: smaller bundle, faster cold start, and snappier UI rendering.

Tauri's bootup speed combined with a local first approach is incredibly fast

The benefit of a local-first app is that you eliminate an entire category of performance problems: the ones caused by the network (and trust me, those are the worst!). 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<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());
}

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.

A flag stored in local storage that enables or disables react profiler for proper measurement

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 logically changed, and those re-renders cascade through an already-heavy tree.

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 <AgentList filters={filters} />; // 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, status }),
  [agent, status] // tedious, error-prone
);

But that's tedious and error-prone. If you've used useMemo a lot in React you know you inevitably run into issues and new edge cases you didn't consider. The real fix is to fix the underlying issue and in this case it was the migration to @tanstack/react-router and the stable refs it provides.

// 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!
}

With that change, navigating around the app no longer triggers a full re-render. Switching workspaces or changing a filter used to cascade through every mounted pane (the sidebar, nav, chat, terminal, and editor), and now it doesn't. Those stable references from TanStack Router fixed one of Conductor's biggest performance bottlenecks.

Stable refs from tanstack mean only the updated components gets re-mounted

The simple fix that made their most important feature fast

Conductor's core experience is the chat. It's where you prompt an agent, watch it think, and read its output, and you might have three or four of these running in parallel across different workspaces. If the chat is slow, the app is slow.

In my experience building real-time UIs, chat is one of the hardest things to make fast in a React app, and it's worth understanding why. Three forces work against you at once:

The simpler version of chat UI is something like messages.map(m => <Message {...m} />), re-rendered whenever a token arrives. This does an enormous amount of redundant work. The 200 messages above the one that's streaming haven't changed, but React doesn't know that, so it reconciles all of them anyway.

How do you fix it? Only render, and re-render, what actually changed. To help you understand here's a before and after:

// 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} />}
    />
  );
}

First, virtualization: only render the messages on screen. Conductor uses react-virtuoso, specifically VirtuosoMessageList, the variant built for exactly their usecase. It renders the handful of messages in the viewport plus a small buffer and swaps them as you scroll. A 500-message session has maybe 15 message components in the DOM at any moment, not 500.

Second, stable references (again!) and memoization: wrap each message in React.memo with a stable key so a token landing in the streaming message re-renders that one message and leaves the other 499 untouched.

VirtuosoMessageList handles the parts that make chat specifically miserable: it sticks to the bottom as new messages arrive (unless you've scrolled up to read history), keeps your scroll position anchored when older messages load in at the top, and re-measures the streaming message smoothly as it grows. You get all of that instead of hand-rolling scroll math.

The result is a chat that stays smooth at hundreds of messages with output streaming live, the single most important thing for a tool you stare at all day.

Running four agents without melting your Mac

Everything so far has been about rendering. Conductor has a harder problem most apps never face: it runs several Claude Code agents as separate processes at once, each holding a model session, file watchers, and memory. Since the rewrite those processes run on Bun instead of Node, a lighter runtime that also cut 150MB from the bundle.

Creating new workspaces like it's nothing

That still leaves ten open workspaces as ten live processes. So when an agent process has been sitting idle, Conductor shuts it down and reclaims its memory. It is safe because every session launches with a --resume <uuid> flag, visible in the running process, so a killed agent loses nothing: the session lives on disk by uuid and resumes when you return. When your app spawns heavyweight subprocesses, memory management is a feature, not an afterthought.

Getting out of the way of the first token

When you hit enter, the only thing that should stand between you and the first token is the model itself. Any local work in that window is latency you've added, and Conductor had a slow step hiding there. Every agent run began by taking a checkpoint, synchronously, on the critical path: checkpointer.sh snapshots the working tree with git add -A, which walks the entire repo to stage it. On a big project that walk isn't free, and it sat right between you and the response.

The fix was to move the start checkpoint off the critical path: fire the snapshot in the background and let the agent respond immediately. You still get the rollback point, it just no longer sits between you and the first token. The node to bun switch helped here too: when an agent process has to spin up or resume from idle, a faster-starting runtime shaves time off that path as well. The fastest operation is the one the user never waits on.

What Conductor changed to make it fast

What I love about Conductor's approach is almost the opposite of what I loved about Linear's. Linear stayed fast by keeping things simple and avoiding the modern stack. Conductor leaned all the way into it: TanStack Query, Router, Virtuoso, Zustand, Tiptap. But the discipline underneath is the same. When they hit a re-rendering problem, they didn't paper over it with useMemo in fifty components. They reached for the library that solved the actual problem and let it do the work. Picking the right tool for each bottleneck instead of fighting the wrong one is its own kind of simplicity.

The shape of it is roughly this. The app is local-first, with SQLite as the source of truth, so the UI never waits on the network. It's wrapped in Tauri rather than Electron, native WebKit instead of bundled Chromium, for a smaller bundle and a faster cold start. When they couldn't profile that webview with Safari's tools, they wired Chrome's devtools in themselves to find the bottlenecks. Moving from react-router to TanStack Router gave them stable references through structural sharing, so navigating no longer cascades re-renders through every mounted pane. The chat runs on react-virtuoso, rendering fifteen messages instead of five hundred and re-rendering only the one that's streaming. Idle agent processes get shut down and resumed on demand, and the whole thing runs on Bun instead of Node. The start checkpoint moved off the critical path, so the agent responds the instant you hit enter.

And maybe that's the real lesson. When Jackson and the team started Conductor, they barely knew React. The hard part of building something fast was never the libraries; you can learn TanStack Router in a weekend. The hard part is living inside your own product long enough to feel every dropped frame, every cascaded re-render, every checkpoint that makes you wait, and caring enough to chase each one down. That's the part you can't install from npm.

If you haven't, I'd recommend giving Conductor a try to see it all in action.

Hope you picked up a thing or two. Digging into how a team makes their product fast is my favorite kind of rabbit hole, and Conductor's was a great one.


A huge shoutout to small team at Conductor! They use Conductor to build Conductor and experience every pain point first-hand. It was a huge pleasure sitting down with Jackson and getting the inside information on their performance pain points and how they addressed them.

If you have any feedback, suggestions, or want to connect you can find me on X.

Dennis Brotzky