I was surprised by how responsive the Codex desktop app feels, so I traced its process tree and inspected the packaged application.
The short answer: the main UI really is Electron. It is just a notably well-architected Electron app.
These are independent observations from one locally installed build, not official OpenAI documentation. Details may change.
The normal app interface—sidebar, conversations, composer, settings, panels—is a React application rendered in an Electron BrowserWindow
.
The packaged entry point contains a standard React root, and the live app runs the expected Chromium renderer, GPU, network, and storage processes. The primary window is created with:
- Context isolation enabled
- Node integration disabled
- A hidden-inset macOS title bar
- Native vibrancy and traffic-light positioning
So it is not a native SwiftUI/AppKit interface containing a small web panel.
It is still hybrid around the edges. AppKit provides the actual macOS window, title-bar controls, menus, context menus, Dock integration, permissions, and window behavior. Specialized overlays also have native-composition support. But the main product UI is web-rendered.
The inspected build used roughly:
- Electron 42 / Chromium 150 / Node.js 24
- React 19
- TypeScript and Vite/Rolldown
- Tailwind and Radix UI
- TanStack Query
- A scoped atom/signal state layer
- A separate Rust
codex app-server
A long conversation does not leave every historical turn mounted in the DOM.
Codex has a custom conversation virtualizer that:
- Keeps estimated and measured heights for each turn
- Uses
ResizeObserver
to update measurements - Finds the visible range with binary search
- Renders only the visible turns plus a tiny overscan
- Preserves the scroll anchor when streaming content changes height
- Restores measurements and scroll position per conversation
It also uses browser primitives such as:
content-visibility: auto;
contain-intrinsic-size: auto 240px;
This is probably the biggest reason large threads remain smooth. Streaming a token does not involve an enormous historical DOM tree.
The renderer uses a scoped atom/signal-style store rather than treating the entire application as one React state tree.
Components subscribe to specific derived values, with atom families and scoped caches for things such as individual threads. The store integrates with React through useSyncExternalStore
.
TanStack Query is wrapped in a reader-aware query abstraction. Queries can be activated according to whether something visible is actually reading them, avoiding unnecessary fetches and updates for hidden surfaces.
The practical result: updating one thread status or streaming one response does not wake up the entire sidebar and application shell.
The production bundle contains React Compiler output. Components have generated memo slots that retain callbacks, objects, and JSX until their actual dependencies change.
That provides broad automatic memoization without requiring developers to manually place useMemo
and useCallback
everywhere. Important row components are additionally wrapped in React.memo
.
This would not rescue a bad architecture by itself, but it compounds the benefits of fine-grained state and virtualization.
The process split looks approximately like this:
React renderer
├─ visible UI
├─ virtualized conversation
└─ fine-grained state/query subscriptions
│
▼
Electron main process
├─ window and native integration
├─ request scheduling
├─ Node worker_threads
├─ PTY and SQLite native modules
└─ IPC bridge
│
▼
Rust codex app-server
├─ agent orchestration
├─ tools and processes
└─ async I/O
Git work, process snapshots, terminal management, database access, and agent orchestration do not run on the React renderer’s event loop.
There are additional Web Workers and WASM modules for specialized work such as PDFs, documents, and spreadsheets.
Requests to the Rust service do not enter a simple FIFO queue.
The Electron main process separates work into categories resembling:
- Critical: start, stop, steer, interrupt, approve
- Interactive: direct UI actions
- Background: hydration, lists, metadata, prefetching
The scheduler limits concurrency, reserves space for urgent work, separates background lanes, coalesces equivalent reads, expires stale requests, and includes fairness so background work is not permanently starved.
This prevents repository hydration or metadata refreshes from delaying something the user just clicked.
The preload bridge is tiny and exposes a narrow API rather than Node itself.
Large values use an ordered, chunked message protocol with acknowledgements. Objects and long strings are assembled incrementally instead of arriving as one huge structured-clone operation.
The preload also caches bootstrap values such as the initial shared-state snapshot, theme, and sidebar data. That removes repeated IPC during startup and avoids visual flashes before React mounts.
The app is not small—the main packaged renderer bundle is substantial.
Instead, it loads a local shell immediately and progressively imports deeper functionality. Conversation routes are prefetched early, while repository, Git, account, and optional feature data are enabled in later stages.
This gives the user a usable surface before every subsystem has finished warming up.
If I wanted another Electron app to feel similarly responsive, I would focus on:
- Keeping filesystem, Git, database, and process work out of the renderer.
- Virtualizing the largest scrolling surface by semantic units.
- Using selectors or atoms instead of broad global-state subscriptions.
- Giving user-triggered backend work explicit priority.
- Measuring and bounding IPC payloads.
- the shell first and prefetching probable next actions.
- Coalescing resize/layout work with
requestAnimationFrame
. - Measuring input-to-paint latency across every process boundary.
The takeaway is not that Electron secretly became native. It is that Chromium can be very fast when the application carefully controls how much work reaches the renderer between a user gesture and the next frame.