VS Code's Architecture Wasn't Planned—It Was Forged by a Decade of Escaping Constraints
An in-depth article, recommended to read when you have time to settle down.
Synchronized to personal site: Learning System Architecture from VS Code
Opening the source code of a mature project today easily creates an illusion: every boundary is seamless, as if the architect foresaw the landscape ten years later when the project began.
But that's not the architecture I want to learn.
What I really want to know is: At the moment a decision was made, what problem did the team see? Which technologies were mature at the time, and which weren't? What did they try, and why did it feel "wrong"? What did the final choice solve, and what debt did it leave behind?
This is the "Why" I truly want to pursue.
So, this article won't start from today's base, platform, and workbench directories. I want to pull the timeline back first: starting from Monaco's browser experiments, seeing how VS Code gradually moved to the desktop, extension ecosystem, remote development, and the Web, and finally how it restructured process boundaries due to Electron's security changes.
Let's state the conclusion first:
VS Code's architecture today was not built according to a blueprint drawn up ten years ago. It grew out of solving real-world problems, one after another.
The Extension Host was initially designed to prevent plugins from dragging down the editor, but later it allowed plugins to be moved to remote machines. Monaco started in the browser, an experience that later supported vscode.dev and helped the Workbench shed its direct dependency on Node.js during Electron's sandbox transformation. Electron allowed the team to quickly produce a cross-platform desktop product, but years later, the team also had to repay the security debt it brought.
Only by placing these decisions back in their original context does today's architecture truly make sense.
Why Looking Only at the Repository History Isn't Enough
When analyzing architectural evolution, Git commits are certainly important, but they mostly tell us "what happened to the code," rarely directly telling us "why the team thought this was worth doing."
VS Code has two special circumstances.
First, Code - OSS (the open-source version of VS Code) was only opened up when the Beta test version was released in November 2015. Monaco's explorations from several years earlier left no complete record in this repository. Second, capabilities like Remote Development span the main VS Code repository, remote extensions, server-side components, and product documentation. Staring at just one repository makes it easy to see only a part of the picture.
Therefore, this article uses three types of evidence:
- Team blogs and talks from the time: Understanding the problems, goals, and trade-offs the team publicly described.
- Source code snapshots of key versions: Confirming whether a certain design had actually landed in the code at that time.
- Contemporary technical ecosystem materials: Understanding why a certain solution was feasible then, or why it later had to change.
We must also be especially careful to prevent one kind of post-hoc attribution.
If a design from 2015 helped the Web version in 2021, we cannot therefore assert that the team foresaw all the needs six years in advance back in 2015. We should look at them separately: what problem it initially solved, and what later changes made it useful again.
Architectural evolution is not prophecy. Early choices constrain the paths that can be taken later, and they also leave some ready-made paths for the future.
First, Meet a Few Roles That Will Appear Repeatedly
If you haven't read the VS Code source code, the following terms are easy to mix up. Let's clarify them using a typical daily usage process.
After opening VS Code, the part in the middle that is actually responsible for displaying code, handling the cursor, and editing text is the Monaco Editor. It is more like the editing kernel of VS Code and can also be detached from VS Code and embedded in other web applications.
The file tree, search, source control, debug panel, terminal, command palette, and status bar surrounding the editor are collectively called the Workbench. It is not one specific feature but the application shell responsible for organizing these features. Simply put, Monaco is responsible for "editing code well," and the Workbench is responsible for "letting you complete a full set of development tasks."
When reading the source code, you will also encounter Workbench Contribution. It is easily confused with the extensions we usually install from the Marketplace, but they are not the same thing.
- Workbench Contribution is a way VS Code internally organizes features. Built-in capabilities like search, debugging, and the terminal register themselves into the workbench. It mainly serves VS Code's own source code and is not an interface directly used by Marketplace extensions.
- Extension refers to the extensions we usually install from the Marketplace, such as those for a specific language, debugger, or linter. It uses the public and stable Extension API and does not require modifying VS Code itself. Some language features that come with VS Code are also implemented in this extension style.
Most desktop extensions do not run directly in the UI process but are handed over to a separate Extension Host. This way, if an extension computes for too long or encounters an exception, it won't directly freeze the cursor and the entire window.
Language extensions also don't necessarily perform all syntax analysis themselves. They can hand off requests like "completions, error reporting, go-to-definition" to an independent Language Server, with the two sides communicating using the LSP (Language Server Protocol). You can understand it as: VS Code is responsible for editing and display, and the Language Server is responsible for understanding the code.
Later, the article will also mention Electron's Renderer (renderer process) and Main Process. The former is responsible for the UI and interaction within the window, while the latter is responsible for creating windows, managing the application lifecycle, and invoking some operating system capabilities. Just remembering these roles is enough for now; when we get to specific decisions later, we'll look at why the team moved the boundaries between them.
In the desktop version, you can initially remember their relationship like this:
First, Let's Lay Out the Ten-Year Timeline
If we compress these ten years into one sentence, it's roughly:
First, prove that good code editing can be done in the browser, then leverage Electron to enter the desktop; then use the Extension Host to support third-party extensions, and use LSP to allow language tools to be reused by multiple editors; finally, extend these divisions of labor to remote and the Web, and readjust process boundaries for stricter sandboxing.
Now, let's go back to each node.
Phase 1: Monaco's First Question Was Not "How to Make a Desktop Development Tool"
VS Code's starting point was not Electron, nor even a desktop application.
In the 2016 VS Code 1.0 retrospective, the team clearly stated the initial problem: as browsers began to support HTML5 (HyperText Markup Language 5) and JavaScript engines became faster, they wanted to try to see if they could make a code editor in the browser that "felt like a native application."
The focus was a code editor, not just a text box.
It needed not only to input text but also to support code completion, error prompts, and go-to-definition. Operations also had to be smooth enough that users wouldn't constantly feel they were writing code inside a web page. Later, this editor was used in OneDrive, Bing Code Search, Azure, and Internet Explorer's developer tools, eventually becoming today's Monaco Editor.
The 2023 VS Code Day official blog supplemented this history: the team first created the Monaco Editor, then built the internally-used Monaco Workbench around it. This workbench later became Visual Studio Online embedded in Azure, and finally evolved into the VS Code released in 2015.
The team's experience is also part of this timeline. The official blog mentions that Erich Gamma and Kai Maetzel, before joining Microsoft and creating VS Code, both participated in IBM Eclipse. It's easy to associate this background with Eclipse's Workbench, plugins, and extension point experience.
But restraint is still needed here: experience can explain why the team was familiar with these types of problems, but it cannot alone prove that a specific mechanism in VS Code was copied from Eclipse. The truly confirmable reasons for decisions should still come back to their public explanations regarding Monaco, extension interfaces, and product goals.
So, the first question VS Code asked was not:
How to imitate a desktop IDE using web technologies?
But rather:
Can the browser become a genuine runtime environment for development tools?
Why It Was Worth Trying at the Time
The team blog mentioned two direct backgrounds: HTML5 capabilities were growing, and JavaScript runtimes were getting faster.
There was another important change during the same period: JavaScript applications themselves were getting larger. TypeScript 0.8 appeared in 2012, and 1.0 was released in 2014. The TypeScript team's positioning for it at the time was to help developers build large-scale JavaScript applications, mentioning that TypeScript projects with over 500,000 lines already existed. TypeScript 1.0 Release Notes
By 2015, TypeScript 1.5 added standard ECMAScript 2015 (ES6) modules, tsconfig.json, and more ES6 capabilities. The official release notes even directly mentioned that VS Code, Sublime, and Atom already supported tsconfig.json. TypeScript 1.5 Release Notes
This doesn't prove that every early choice in Monaco came from TypeScript. But it at least shows that some key conditions were already in place: browsers were no longer only capable of hosting simple pages, and the JavaScript toolchain was beginning to support large applications, modular development, and editor integration.
The Team Used It Daily, So Monaco Grew from an Editor into a Workbench
To develop the Monaco Editor, the team needed a set of tools they could use every day. So, they created a local Node.js service responsible for providing files and the editor interface, and then gradually put more development workflows into it.
This action was crucial.
If they only made an editor that could be embedded in a web page, the team only needed to care about the text model, cursor, rendering, and language capabilities. But when they started using this tool to develop Monaco itself, files, project navigation, debugging, and version control also became unavoidable needs.
Thus, Monaco was no longer just an editor component. The Workbench we mentioned earlier grew around it: a workbench capable of hosting a complete development workflow.
This wasn't about drawing a complete architecture in a meeting first, then implementing it according to the diagram. How the team started using it directly changed what it eventually grew into.
The Decisions Truly Made in This Phase
| Dimension | Choice at the Time |
|---|---|
| Product Hypothesis | The browser can host a near-native code editing experience |
| Technology Bet | Build the editor and workbench with Web technologies |
| Evolution Method | The team used their own tool daily, adding capabilities based on real needs |
| Immediate Benefit | The same editor could be embedded in multiple Web products |
| Early Cost | Performance, file access, and system capabilities all required finding their own boundaries |
Looking at the separation of editor and workbench in VS Code today, it's much easier to understand: first, there was an editor that could be independently embedded in web pages, and later, a complete workbench grew around it. This is my architectural explanation based on product evolution, not a conclusion directly given by the team blog.
Phase 2: Choosing Electron in 2015, the Core Reason Was Reuse and Speed
After having the Web Workbench, the team wanted to make a local tool that could be installed on Windows, macOS, and Linux. It couldn't just edit cloud resources; it also needed to handle arbitrary code on a developer's computer.
According to the 1.0 retrospective, they believed that the most commonly used developer workflows—navigation, debugging, and Git—should also enter the product. Just putting Monaco into a window was no longer enough.
The problem then became very specific: how to bring the existing Web editor and Workbench to the desktop?
Why Electron
Electron was originally the Atom Shell developed by GitHub for Atom. Official Electron materials show its first commit happened in 2013; by the time VS Code Preview was released in 2015, it was still a very young desktop technology. Electron 10th Anniversary Retrospective
However, it happened to combine the three things the team needed:
- Chromium providing a browser rendering environment.
- Node.js providing file system, process, and local tool capabilities.
- A cross-platform shell responsible for windows and OS integration.
The reason given by the VS Code team in the 1.0 retrospective was very direct: since the product was already built on Web technologies, putting it into a native cross-platform shell like Electron was natural. The team also specifically mentioned that it only took a few months from this choice to releasing the first cross-platform Preview.
So, the team in 2015 wasn't concluding that "Electron is the ultimate architecture for desktop applications." They made a very pragmatic judgment:
We already have a mature Web Workbench, and Electron can bring it to three desktop platforms with the lowest rewrite cost.
This Pragmatism Can Be Seen in the Historical Source Code
The open-sourced 0.10.1/package.json clearly records Electron 0.34.1. The electron-browser/main.ts at the time also directly imported path, fs, and child_process in the Renderer process responsible for UI rendering.
By today's Electron security requirements, this approach is highly problematic. But back in 2015, it fully utilized Electron's most attractive capability at the time: the browser UI could directly call Node.js.
This allowed the team to quickly productize, but it also planted the debt that the later sandbox migration had to repay.
We can't simply say "the early architecture was wrong" because of this. If the team had prepared for strict sandboxing years in advance from the start, turning all native calls into asynchronous proxies, VS Code might have taken much longer to release. More importantly, the product's value hadn't been validated yet, but the team would have had to bear significant complexity first.
So, judging whether this decision was reasonable must also factor in the release timeline and the cost of experimentation.
Choosing Electron Wasn't a One-Time Choice
Choosing Electron also meant that VS Code would continuously be influenced by Chromium, Node.js, and Electron thereafter:
- Chromium's security model would affect what the Renderer could do.
- Node.js versions would affect extensions and native modules.
- Electron's process model would affect windows, the Extension Host, and IPC (Inter-Process Communication).
The tech stack is never just an implementation detail. A sufficiently low-level dependency will constrain your architecture with its own upgrade path.
Phase 3: Two Gaps in 2015 Pushed VS Code Towards the Extension Host
The VS Code Preview released in April 2015 had two fundamental gaps the team itself acknowledged: it couldn't be sufficiently extended, and it wasn't open for development.
These two things must be considered together.
If an editor cannot be extended, the team has to personally support all languages and tools; if the extension interface changes frequently, external developers won't dare to invest long-term. Casually adding a few callback functions is far from enough to form an ecosystem.
Why the Team Decided to "Build Themselves Using Their Own Extension Interface"
The VS Code team explained in the 1.0 blog that they wanted to create a set of extension APIs (Application Programming Interfaces) that were feature-rich and stable long-term. To verify that this API was truly usable, they decided to have VS Code itself implement important features through it. The core language services for JavaScript and TypeScript were extensions distributed with the product.
This was equivalent to setting a very strict rule for themselves:
If our own key features cannot be built through this API, it's not yet sufficient to hand over to third parties.
From the Preview to the Beta test version in November 2015, it was about six months. The team launched a complete extension mechanism, connected to the Visual Studio Marketplace, and simultaneously open-sourced the VS Code repository and a batch of their own extensions.
By version 1.0 in 2016, the Marketplace already had over 1000 extensions. The team consequently announced API stability and committed to backward compatibility.
This causal chain is important:
The product cannot cover all languages
↓
Must open up for extensions
↓
Open interfaces must be validated by real features
↓
Core language capabilities are also implemented as extensions
↓
API stability becomes a long-term product commitment
Why Not Let Extensions Run Directly in the UI Process
Historical source code proves that the extension mechanism in the Beta phase was already more than just "loading a piece of code at runtime." 0.10.1 contains an independent pluginHostProcess.ts. It would establish message communication with the renderer process, handle exceptions, monitor the parent process, and then start the plugin host.
At that time, it was still called Plugin Host, and it was gradually unified to Extension Host around version 1.0.
Why add an extra process? Because third-party extensions could access Node.js, scan entire projects, and even spawn child processes. If these tasks crowded the same event loop as keyboard input, cursor movement, and UI drawing, a poorly written extension could freeze the entire editor. Putting extensions into a separate process was precisely to isolate the open ecosystem from the core editing experience.
Today's Extension Host documentation still summarizes the goal as stability and performance: extensions should not slow down startup, block UI operations, or directly modify the UI, and should be loaded on demand via Activation Events.
However, the Extension Host mainly solves fault and performance isolation; it is not a complete security sandbox. Extensions can still read files, access the network, and execute programs.
This Choice Later Became Useful Again
In 2015, the team's main concern was "don't let plugins drag down the editor." But the independent host also brought a benefit that wasn't so obvious at the time: extensions didn't have to stay in the same process as the Workbench.
Four years later, the team actually moved part of the extension host to remote machines. The process boundary originally drawn to solve plugin lag thus became a ready-made foundation for remote development.
Phase 4: LSP in 2016, Unbinding Language Capabilities from a Single Editor
With the Extension API, developers could write language extensions for VS Code. But a new problem immediately appeared.
Suppose there are 10 editors and 100 languages. If each language had to implement completion, diagnostics, go-to-definition, and refactoring separately for each editor, a huge amount of work would be repeated.
More troublesome was that language tools aren't necessarily written in JavaScript. A Java language server is better suited to run in a Java runtime, and a C# language service might be built on .NET. Rewriting all language analysis logic as VS Code extensions wasn't realistic.
The Team's Decision at the Time
In June 2016, the VS Code team, together with Red Hat and Codenvy, announced the Language Server Protocol (LSP). Official Blog delineated the responsibilities of both sides: the editor is responsible for opening and modifying documents and receiving user actions; the language server is responsible for understanding a specific programming language. The two sides exchange messages via JSON-RPC, such as "user opened a file," "file content changed," "what errors are here," "where is the definition of this symbol."
In this way:
- Language servers can be implemented in any programming language.
- The same language server can serve multiple editors.
- Editors don't need to compile analyzers for every language into themselves.
At the time, Eclipse Che was developing browser and cloud development environments, and Red Hat was preparing to provide an independent Java Language Server. From this, it's clear that LSP wasn't an internal refactoring done behind closed doors by the VS Code team. Several tool and language teams all encountered the problem of duplicated development, so they jointly agreed on a communication method.
Why This Is More Important Than "Adding Another Layer of Abstraction"
Internal project interfaces usually only serve the same codebase and runtime environment. An open protocol allows both sides to use different programming languages, run in different processes, and even be developed separately by different companies.
The Extension Host solved "where does third-party code run," and LSP further solved "how can language intelligence be reused across tools." Combined, VS Code didn't need to be the owner of all language tools to provide a rich language experience.
Of course, protocols have costs. The team needs to handle protocol upgrades, message serialization, state synchronization, and backward compatibility. If the editor modifies a document, the language server must also see the same version; once the states on both sides don't match, the problem is much harder to troubleshoot than a regular function call.
So, you can't introduce a protocol just to "look decoupled." This complexity is only worthwhile when the boundary truly crosses languages, processes, or organizations.
Phase 5: Remote in 2019, Not Thought Up First, But Forced by Users
By 2019, the development environment itself had changed.
Containers, remote virtual machines, and Windows Subsystem for Linux (WSL, a capability allowing Windows users to directly use a Linux environment) were becoming increasingly common. Large codebases might require more powerful remote machines, data science tasks depended on data and GPUs that couldn't fit locally, and enterprise code might only be allowed to reside on controlled development machines.
The VS Code team wrote a very complete decision-making process in the Remote Development release blog.
They Didn't Initially Design a "Distributed VS Code"
When the team first started researching WSL support, their thinking was more localized: still use VS Code on Windows to edit files, and create a script allowing users to launch code from Bash; Node.js remote debugging would be adapted separately.
But they quickly found it "wrong."
Assume Python 2.7 is installed on Windows, and Python 3.7 and Django are installed in WSL. The local VS Code still sees the Windows runtime, dependencies, and toolchain. To get correct code completion and linting results, users had to install a development environment on both sides. This precisely defeated the purpose of using WSL or containers.
The team also saw that other solutions each had significant drawbacks:
- Remote Desktop preserved the full desktop but had high latency and wasn't always easy to configure on Linux.
- Secure Shell (SSH) plus Vim was direct enough but lost the modern editor experience.
- Local editing plus file syncing was prone to slowness, errors, and state inconsistencies.
- Pure browser tools at the time couldn't fully inherit the user's local toolchain and desktop experience.
The team's final conclusion was: VS Code must "run in two places simultaneously."
How the Architecture Changed Following the Problem
They kept UI work like windows and keyboard input locally, and placed the file system, terminal, debugger, language tools, and Workspace Extensions in the remote environment. UI Extensions like themes and keybindings stayed local; workspace extensions that needed to read code and call remote toolchains were placed remotely, following the code.
This wasn't simply adding an SSH channel. The team explicitly stated in the blog that they re-divided code boundaries and began cleaning up a long-standing assumption: that the development environment must be on the same computer running the UI.
1.34.0's AbstractRemoteAgentService already contained concepts like Remote Authority for identifying the remote end, communication channels established on demand, WebSocket connections, and disconnection reconnection.
Why the Old Extension Host Boundary Worked Here
If third-party extensions had been directly embedded in the Workbench from the start, remote development would have required not just moving extensions, but redesigning the entire extension mechanism.
Fortunately, extensions already collaborated with the UI through APIs and messages. The team now mainly needed to judge "should this extension run locally or remotely," and then extend the original inter-process communication between two machines.
This doesn't mean the team had already planned for remote development in 2019 when designing the Extension Host in 2015. We can only say: once the process boundary was clearly drawn, the team later had the condition to move part of it to another machine.
How the Technical Ecosystem Influenced the Decision
Remote development wasn't a product idea that appeared out of thin air. It was closely related to the gradual popularization of containers, WSL, cloud VMs, large repositories, and data science tasks at the time.
If the development environment had always been just a local compiler and a few thousand files, making VS Code run in two places would only have added complexity. This complexity only became worthwhile when external working methods changed.
Phase 6: Returning to the Browser in 2021, But It Was No Longer the Original Monaco
In October 2021, the VS Code team released vscode.dev, calling it the realization of a decade-long vision. vscode.dev Release Blog
This looked like returning to the starting point, but the Web version in 2021 was no longer the early Monaco Workbench.
It had several layers of capabilities behind it that didn't exist ten years ago:
- Modern browsers' File System Access API could access local files after user authorization.
- Web Workers could host some Web Extensions.
- GitHub Repositories could open remote repositories using a virtual file system.
- Codespaces could place the complete computing environment in the cloud and then connect the Web Workbench to it.
- The Remote architecture had already proven that the UI and workspace capabilities could be separated.
The Team Didn't Pretend the Web and Desktop Versions Were Exactly the Same
The vscode.dev blog honestly listed the capability boundaries: a pure browser environment has no local terminal or debugger, and cannot run extensions that depend on Node.js, OS modules, or external programs.
Therefore, the Web version didn't use a compatibility layer to pretend it had all desktop capabilities. VS Code provides different service implementations and extension hosts for different environments, and clearly tells users what cannot be done in the browser.
In 1.61.0's workbench.web.main.ts, you can see the Web entry first imports the common Workbench, then assembles browser implementations for services like search, files, credentials, updates, and lifecycle. The same version also already had webWorkerExtensionHost.ts.
This also explains the directory names like common, browser, node in the source code. common holds code that doesn't depend on a specific runtime environment, browser holds implementations usable in the browser, and node allows the use of Node.js. The principle behind them is:
"What it is responsible for" and "where it can run" must be two independent questions.
This Time, the Original Vision and Mid-Journey Accumulations Converged
Saying vscode.dev is the direct realization of the 2011 vision is incomplete.
Monaco, starting from the browser, provided the UI and editor foundation; the Extension Host allowed extensions to run elsewhere; Remote split the UI from computation; advances in browser APIs filled in the gap for local file access. Several originally separate development lines only came together to form VS Code for the Web by 2021.
So, this wasn't a straight line from 2011 directly to 2021. Several capabilities accumulated in the middle only truly pieced together once browser conditions matured.
Phase 7: After the Extension Ecosystem Succeeded, Security Issues Began Changing the Core Architecture
The stronger the extension and automation capabilities, the more code a development tool can execute on behalf of the user.
Running tasks, debugging programs, loading linters from a project, executing npm install—all could potentially run code from a freshly downloaded repository on the developer's machine. The VS Code team used to add prompts at each risk point individually, but in their 2021 Workspace Trust retrospective, they admitted this "find one, patch one" approach couldn't solve all problems.
So, the team elevated "whether the current workspace is trusted" to a global state readable by both the Workbench and the Extension API. Untrusted workspaces enter Restricted Mode, where capabilities that might execute code, like tasks and debugging, are collectively restricted.
This illustrates that the more successful a product is, the more likely it is to push new problems into the core architecture.
The early goal was to let extensions "do everything," but later, it also had to answer "when is it allowed to do so." This shows that permissions and trust aren't just settings added after the extension system is complete. The more successful the extension ecosystem, the more likely these become core concepts the platform itself must understand.
Workspace Trust still cannot eliminate all risks brought by malicious extensions, but it at least unified similar judgments originally scattered across tasks, debuggers, and individual extensions back to the platform layer.
Phase 8: Electron's Security Evolution Forced VS Code to Redraw Process Boundaries
One of Electron's most convenient initial capabilities was that the Renderer could directly use Node.js. VS Code in 2015 quickly gained file and process capabilities precisely through this.
But from a security perspective, this also meant: once malicious code in the UI broke through the browser's defenses, it could directly access the file system, start processes, and gain system privileges it shouldn't have.
Electron later gradually strengthened process isolation. Electron 20, released in 2022, went further: unless nodeIntegration was explicitly enabled or the sandbox disabled, the Renderer entered Sandbox by default. Electron 20 Release Notes
VS Code didn't wait until the last moment. The team started migrating in early 2020 and reviewed the entire process in the 2022 long article Migrating VS Code to Process Sandboxing.
Why This Wasn't Just Changing a Configuration
After enabling the sandbox, the Workbench's renderer process could no longer directly use Node.js. File access, process creation, and native module calls previously done directly by it all had to be moved to privileged processes, with results passed back via asynchronous messages.
This was equivalent to turning a large number of local function calls into cross-process protocols:
Before: Renderer → fs / child_process / native modules
After: Renderer → Async Service Proxy → IPC → Privileged Process
The team needed to rearrange a batch of important features, moving them out of the renderer process:
- Extension Host
- Integrated Terminal
- File Watching
- Full-text Search
- Task Execution
- Debugging
The terminal and file watching were moved into child processes of the Shared Process (a background process shared by multiple windows). Search, tasks, and debugging already had cross-process mechanisms from the remote development model and could continue to be reused. The Extension Host needed a new running location: one that had full Node.js, could spawn child processes, and could communicate directly with the sandboxed renderer process.
When Electron Lacked Suitable Capabilities, the Team Changed Upstream
At the time, Electron didn't have an API that fully met these requirements. The VS Code team therefore contributed UtilityProcess to Electron: an independent Node.js process created by the Electron Main Process, communicating directly with the renderer process via MessagePort. This way, high-frequency messages didn't all have to be routed through the main process.
The team didn't just wrap a layer of code inside VS Code to bypass the problem, and this is even more worth learning from.
When the foundational tech stack lacks a capability the product needs, large teams typically have three choices: work around it within their own project, maintain a private fork long-term, or contribute the missing capability upstream. VS Code chose the third path, giving Electron itself this process capability.
Why Web and Remote Once Again Lowered the Migration Cost
The sandbox migration blog explicitly mentioned that VS Code for the Web had already prompted the Workbench to clean up many Node.js dependencies; remote development had already established cross-process execution mechanisms for search, debugging, and tasks.
That is to say:
- The Monaco and Web route made the UI closer to a pure browser model.
- The Remote route allowed calls that originally could only be done locally to gradually be handled by other processes.
- The Extension Host route meant third-party code already had its own independent lifecycle.
These previously completed transformations collectively lowered the cost of the sandbox refactoring.
In 1.74.0, a large amount of code had already entered the electron-sandbox runtime environment. Comparing the Renderer in 0.10.1 that directly imported fs, one can very intuitively see the boundary changes over seven years.
What Was Repaid and What Was Gained in This Phase
The price the team paid was the asynchronization and process reorganization spanning almost all components, and the entire migration lasted from around 2020 to 2023.
What was gained wasn't just a more secure renderer process:
- Node.js capabilities were converged into more clearly defined processes.
- The main process no longer needed to forward all high-frequency messages.
- The implementation methods of the Web and desktop versions became closer.
- Renderer processes could be reused during page navigation, making reloading and switching workspaces faster.
This migration was originally driven by Electron's security requirements, but it ultimately made VS Code's own process division of labor clearer as well.
The Architecture Seen Today Is Layered from These Decisions
Now, looking again at VS Code's main boundaries, each one can find its origin along the timeline.
| Today's Structure | Initial Problem Solved | What It Later Solved |
|---|---|---|
Separation of editor and workbench |
Monaco needed to be embeddable in web pages and grow into a full tool | Monaco could be distributed independently, Workbench supports both desktop and web simultaneously |
| Extension Host | Third-party extensions must not block the core UI | Extensions can run locally, in the browser, or on remote machines |
| Extension API | The team couldn't cover all languages and tools themselves | Established the Marketplace and maintained long-term extension compatibility |
| LSP | Each language had to be re-implemented for each editor | The same language server can be reused by multiple editors |
| Unified service interfaces and remote proxies | Different runtime environments need different implementations | The same UI can connect to local, remote, and browser environments |
common/browser/node/electron-* |
Distinguishing code responsibility from the runtime capabilities it can use | Prevents Node.js code from mixing into Web and sandbox at build time |
| Main / Shared / Utility Process | Splitting global, shared, and high-privilege work | Reduces the impact scope of failures, security issues, and performance problems |
So, understanding today's VS Code cannot stop at "it uses layering, dependency injection, and multi-process."
What's truly worth seeing is why these structures appeared:
It successively encountered browser performance, desktop distribution, extension ecosystems, language reuse, remote environments, Web capabilities, and security model changes. Each time a problem was solved, some new division of labor was left in the system; later teams then decided whether to continue reusing these divisions or readjust them.
Some divisions withstood the test of subsequent needs and were used for many years; some early shortcuts later became debt and had to be redesigned.
Six Things I Truly Learned from This Evolution Line
1. Judging an Architectural Decision Requires Going Back to the Year It Happened
Letting the Renderer directly use Node.js in 2015 looks insecure today, but it allowed the team to bring an existing Web Workbench to three desktop platforms in a few months.
The high refactoring cost later doesn't mean the early choice was necessarily wrong. What needs to be compared is the benefit at the time, the visible risks at the time, and whether a more complex solution would have caused the product to miss its validation window.
2. Good Boundaries Don't Need to Foresee the Future, But They Can Reduce Future Demolition
The Extension Host didn't appear for Remote, yet Remote could leverage it to move extension execution locations.
The value of architecture isn't in accurately guessing the future, but in not having to overturn the entire system when needs actually change.
3. The More Successful a Product, the More It Exposes the Next Batch of Architectural Problems
The more extensions there are, the heavier the compatibility commitment, and the higher the supply chain and workspace execution risks; the more remote scenarios there are, the more important reconnection, latency, and version consistency become.
Architecture doesn't become "complete" because the product succeeds. Success usually just amplifies the next set of contradictions.
4. When the External Environment Changes, Previously Unworthwhile Complex Designs Can Become Worthwhile
When local development was mainstream, a distributed Workbench might have been over-engineering; when containers, WSL, cloud hosts, and large datasets became common, it started solving real pain points.
Similarly, the emergence of the File System Access API, Web Workers, and Codespaces turned a complete Web Workbench from a vision into a usable product.
5. The Foundational Technology You Depend On Will Also Reshape Your Architecture in Return
Electron allowed VS Code to quickly go cross-platform, and through its later Sandbox rules, it also pushed VS Code to remove Node.js from the Renderer.
When a product is built on Chromium, Node.js, and Electron, the upstream security, release, and process models are your own architectural constraints. Large projects must not only upgrade dependencies but also participate upstream.
6. Open Protocols Allow a Project's Internal Capabilities to Be Reused by More Tools
The Extension API mainly serves the VS Code ecosystem, while LSP spans editors, languages, and organizations.
When a capability needs to be implemented long-term by different tech stacks and different teams, a protocol is more useful than an internal project abstraction. However, it also requires the team to seriously handle versioning and compatibility issues.
How to Analyze the Architectural Evolution of an Open-Source Project in the Future
Just drawing the current architecture diagram is no longer enough. Going forward, for each key decision analyzed, I will consistently answer these seven questions:
- What was the user problem at the time? Don't use needs that appeared later to explain early choices.
- What was the technical ecosystem at the time? Which infrastructure was already available, and which was still immature?
- What did the team try? Are there blogs, Issues, or commit records explaining why a certain path was "wrong"?
- What was the actual decision? Was it re-splitting modules, splitting processes, establishing a protocol, or changing where code runs?
- What did it immediately solve? Don't replace concrete benefits with abstract terms like "extensibility."
- What debt did it leave behind? Who ultimately bears the cost of performance, compatibility, security, and complexity?
- How was it later reused or overturned? Distinguish the original purpose from post-hoc value.
For VS Code, one can also do a more specific longitudinal reading along versions:
2015 0.10.1 Electron Renderer + Plugin Host
2016 1.0 Stable Extension API + LSP
2019 1.34 Remote Agent and remote service proxy
2021 1.61 Web Workbench + Web Worker Extension Host
2022 1.74 electron-sandbox and new process boundaries
2026 main Current layering, runtime environments, and Utility Process
The focus of this reading path isn't comparing file counts, but tracking where the same piece of work moved across different versions: who originally did it, who was it handed to later; why did a direct function call originally become a cross-process message later.
Finally
Understanding VS Code's architecture cannot just look at what it has today.
It started from a browser editor, quickly entered the desktop leveraging the young Electron; used the Extension Host and stable API to build an extension ecosystem; used LSP to let language tools step out of VS Code and be reused by more editors. When containers, WSL, and remote computing gradually became common, the team split the local UI from the remote workspace. Later, VS Code returned to the browser, and also reorganized process boundaries due to changes in Electron's security requirements.
The most valuable part of this evolution line isn't proving the VS Code team got everything right every time, but letting us see: how they made judgments based on the information and technical conditions available at the time, and how they later dealt with the problems brought by old decisions.
So, when looking at an architectural choice in the future, the first question I want to ask won't be "what is this pattern called," but:
What problem appeared at the time that forced the system to this point? Why was the team willing to bear this complexity for it? Did the boundary drawn back then later help, or did it become debt that had to be repaid?
Only by answering these three questions along the timeline can one truly begin to understand the Why of system architecture.