From main.ts to First Paint: VS Code's Multi-Process Startup
Prerequisites
- ›Article 1: Architecture and Layering
- ›Basic understanding of Electron's multi-process model (main vs renderer)
From main.ts to First Paint: VS Code's Multi-Process Startup
Every time you launch Visual Studio Code, a carefully orchestrated startup sequence crosses at least three OS processes and initializes hundreds of services — yet the editor appears in under a second on modern hardware. This article traces that sequence from the very first line of src/main.ts through to the rendered workbench shell, explaining how Visual Studio Code balances correctness with startup speed.
The Electron Main Process Boot
Everything begins at src/main.ts, the Electron main process entry point. Before Electron's app.ready event even fires, this file performs critical synchronous setup:
- Performance marks —
perf.mark('code/didStartMain')starts the clock immediately. - Portable mode —
configurePortable(product)checks for a portable installation. - CLI argument parsing —
parseCLIArgs()uses minimist for early flag detection. - Sandbox configuration — Enables Chromium's process sandbox unless explicitly disabled.
- User data path —
getUserDataPath()resolves the profile directory beforeapp.ready. - Protocol registration —
vscode-webview://andvscode-file://custom schemes are registered with appropriate security privileges. - Crash reporter — Configured synchronously so crashes during startup are captured.
sequenceDiagram
participant OS as Operating System
participant Main as src/main.ts
participant Electron as Electron Runtime
participant CodeMain as CodeMain
OS->>Main: Launch process
Main->>Main: Parse CLI args
Main->>Main: Configure sandbox
Main->>Main: Set userData path
Main->>Main: Register protocols
Main->>Main: Configure crash reporter
Main->>Electron: Register app.ready listener
Electron->>Main: app.ready event
Main->>Main: Resolve NLS configuration
Main->>Main: bootstrapESM()
Main->>CodeMain: import('./vs/code/electron-main/main.js')
The key insight is that src/main.ts does as much synchronous work as possible before app.ready, because Electron blocks on this event. Once ready, the onReady() function resolves NLS (localization) configuration in parallel with creating the code cache directory, then calls startup() which bootstraps ESM and dynamically imports the real entry point: src/vs/code/electron-main/main.ts.
CodeMain: Single-Instance Lock and Initial Services
The CodeMain class is where the main process really takes shape. Its startup() method follows a deliberate sequence:
src/vs/code/electron-main/main.ts#L97-L160
Step 1: Create initial services. createServices() (line 162) builds the bootstrap service collection — Product, Environment, Logger, Log, Files, State, UserDataProfiles, Policy, Configuration, Lifecycle, and a few others. These are the minimum set needed to determine if we're the primary instance.
Step 2: Initialize services. Directories are created (mkdir -p style), state is loaded, configuration is read from disk.
Step 3: Claim the single-instance lock. claimInstance() attempts to start an IPC server on a named pipe. If it succeeds, we're the primary instance. If it fails with EADDRINUSE, another instance is running — we connect to it as a client, forward our CLI arguments, and exit.
flowchart TD
A["CodeMain.startup()"] --> B["createServices()<br/>Bootstrap ~15 services"]
B --> C["initServices()<br/>mkdir, load state, init config"]
C --> D["claimInstance()<br/>Try IPC server bind"]
D -->|Success: first instance| E["Create CodeApplication"]
D -->|EADDRINUSE| F["Connect as IPC client"]
F --> G["Forward args to primary instance"]
G --> H["Exit"]
E --> I["CodeApplication.startup()"]
This single-instance pattern is critical. Without it, two VS Code instances would fight over state files, extension hosts, and user data. The IPC forwarding means that opening a file from the terminal always routes to the existing window.
Tip: If VS Code won't start and you see "Another instance is running" errors, the stale IPC pipe is usually in
$XDG_RUNTIME_DIRon Linux or~/Library/Application Support/on macOS. Deleting the socket file resolves it.
CodeApplication: The Main Process Singleton
Once we've claimed the instance lock, CodeApplication takes over. This is the main process singleton — created via DI with about a dozen injected services:
src/vs/code/electron-main/app.ts#L159-L172
The constructor immediately configures Electron session security (CSP, permission handlers, certificate validation) and registers IPC listeners. Then startup() builds the full main-process service graph:
src/vs/code/electron-main/app.ts#L552-L604
The startup() method:
- Sets the Win32 app user model ID (for taskbar grouping)
- Creates the Electron IPC server for renderer↔main communication
- Resolves machine telemetry IDs
- Sets up the Shared Process — a hidden Electron renderer that hosts services shared across windows (extension management, telemetry batching, search indexing)
- Calls
initServices()to create the expanded service graph (telemetry, storage, workspaces, windows management, update service, terminal pty host, and many more) - Opens or restores windows via
WindowsMainService
Renderer-Side Bootstrap: DesktopMain and Workbench
Each VS Code window runs in an Electron renderer process. The bootstrap happens in DesktopMain:
sequenceDiagram
participant Main as Main Process
participant Renderer as Renderer Process
participant DM as DesktopMain
participant WB as Workbench
Main->>Renderer: Create BrowserWindow
Renderer->>DM: new DesktopMain(config)
DM->>DM: reviveUris()
DM->>DM: initServices() in parallel with DOM ready
DM->>DM: Create ServiceCollection
Note over DM: MainProcessService, Product, Environment,<br/>Logger, Policy, SharedProcess, Files,<br/>Remote, Storage, Configuration...
DM->>WB: new Workbench(document.body, services)
DM->>WB: workbench.startup()
WB->>WB: initServices() — collect all registerSingleton() descriptors
WB->>WB: initLayout()
WB->>WB: Registry.start() — workbench contributions
WB->>WB: renderWorkbench() — create UI parts
WB->>WB: restore() — restore editors, views
The open() method at desktop.main.ts#L114-L142 parallelizes service initialization with DOM readiness — a smart optimization since both are I/O-bound. It explicitly applies the zoom level before creating the Workbench to prevent visual flicker.
Notice the prominent comments in initServices(): "NOTE: Please do NOT register services here. Use registerSingleton() from workbench.common.main.ts". This is the barrel file system from Article 1 in action. DesktopMain only registers services that need direct access to the main process IPC channel. Everything else comes from the globally-registered singletons.
The Process Zoo: Six Roles Explained
VS Code isn't just "main + renderer." There are six distinct process roles:
graph TD
MAIN["<b>Main Process</b><br/>Electron main<br/>Window management, OS integration,<br/>lifecycle, IPC hub"]
RENDERER["<b>Renderer Process</b><br/>Electron renderer (per window)<br/>Workbench UI, editor, panels"]
SHARED["<b>Shared Process</b><br/>Hidden Electron renderer<br/>Extension management, search indexing,<br/>telemetry batching"]
UTILITY["<b>Utility Processes</b><br/>Node.js child processes<br/>PTY host (terminal), file watcher"]
EXTHOST["<b>Extension Host</b><br/>Node.js / Web Worker<br/>Runs extension code in isolation"]
SERVER["<b>Remote Server</b><br/>Headless Node.js<br/>SSH/container/WSL backend"]
MAIN --> RENDERER
MAIN --> SHARED
MAIN --> UTILITY
RENDERER --> EXTHOST
SERVER --> EXTHOST
style MAIN fill:#e3f2fd
style RENDERER fill:#e8f5e9
style SHARED fill:#fff3e0
style UTILITY fill:#f3e5f5
style EXTHOST fill:#fce4ec
style SERVER fill:#e0f2f1
- Main process — The Electron "brain." Manages windows, handles OS-level integration (menus, dock, file associations), coordinates IPC between all other processes.
- Renderer process — One per VS Code window. Runs the full workbench UI. Sandboxed with no direct Node.js access (communicates with main via IPC).
- Shared process — A headless renderer that caches and shares work across windows: extension gallery queries, telemetry aggregation, settings sync.
- Utility processes — Electron's
UtilityProcessAPI for CPU/IO-intensive work. The PTY host (terminal backend) is the primary example — it manages all pseudoterminal instances. - Extension host — Runs third-party extension code. Isolated from the renderer. Can be a local Node.js process, a web worker, or a remote process on another machine.
- Remote server — The headless backend for remote development (SSH, containers, WSL). Hosts extension hosts and file system access on the remote machine.
Alternative Entries: CLI and Remote Server
Not every VS Code startup involves Electron. The CLI entry at src/cli.ts is a lightweight 26-line script. It resolves NLS, bootstraps ESM, sets VSCODE_CLI=1, and imports the CLI handler — used for operations like code --install-extension and code --diff.
The remote server at src/server-main.ts is more substantial. It parses server-specific CLI args, optionally prompts for license acceptance, creates an HTTP server, and lazily loads the remote extension host agent. The key design decision: the HTTP server starts listening before the full VS Code server module loads. This means the port is claimed immediately, and the first actual request triggers the expensive initialization:
flowchart LR
A["server-main.ts"] --> B["Parse args"]
B --> C["Create HTTP server"]
C --> D["server.listen()"]
D --> E["First request arrives"]
E --> F["Lazy load server.main.js"]
F --> G["Create extension host agent"]
This lazy loading pattern means the server appears responsive to connection tools (like the Remote-SSH extension) even while it's still loading hundreds of services in the background.
What's Next
We've seen what gets instantiated during startup, but we glossed over how hundreds of services get wired together without manual constructor calls. The next article dives into VS Code's custom dependency injection system — the createDecorator/InstantiationService pattern that makes all of this service graph construction possible.