19.05.26
How Browser-First Runtimes Actually Work
A practical tour of the browser primitives that make a real development loop possible inside a tab.
Browser-first runtimes are easy to describe badly. "Node.js in the browser" sounds like one large binary compiled to WebAssembly, or a thin iframe around a remote container. In practice, the useful version is more deliberate: use the browser as the first host, use its native isolation and storage primitives, and move to a server only when the workload actually needs server capabilities.
Verklet is built around that shape. It runs Node.js-style workloads and supported Python in the visitor's tab first. Web Workers isolate processes, WebAssembly handles the byte-heavy hot paths, OPFS persists project state, and an isolated preview bridge makes previews feel like normal pages without sharing your app origin.
The runtime is a set of browser services
The browser does not expose a POSIX process table, a loopback network, or a normal filesystem. A browser-first runtime has to create those interfaces from the primitives the platform does expose.
The important pieces are:
- Web Workers for process isolation.
- A coordinator worker for filesystem state, ports, process records, and runtime events.
- WebAssembly for synchronous filesystem access, archive handling, path validation, hashing, and other hot paths.
- OPFS for local persistence that survives reloads.
- Service Workers for preview routing.
- A package manager layer that can hydrate dependency layouts without asking the browser to behave like Linux.
Each piece is ordinary by itself. The product work is making them behave like one runtime.
Workers become processes
Every spawned command runs in a worker boundary. That is the difference between embedding a code editor and embedding a development environment.
When user code throws, loops, or exits, it should not freeze the page that owns the editor. The main thread should keep rendering the terminal, the file tree, and the preview frame. A worker gives each runtime process its own execution boundary, stdio stream, and lifecycle events.
That also makes parallel work possible. A tutorial can run a dev server while a build script runs. An agent can edit files, run tests, and inspect output without turning the host UI into the runtime.
The filesystem has to feel synchronous
Node packages expect fs.readFileSync, CommonJS resolution, require(),
and lots of small filesystem checks. Browser storage APIs are mostly
asynchronous. That mismatch is one of the core reasons browser runtimes
are hard.
Verklet uses a virtual filesystem owned by the runtime, with WebAssembly
on the byte-heavy paths. The goal is not to expose OPFS directly to user
code. The goal is to provide filesystem semantics that Node-compatible
tools can use, then persist snapshots to OPFS when a persistenceKey is
configured.
That snapshot model gives the host product a useful property: reload the page, mount the previous files, and continue. The runtime can degrade when OPFS is missing, but the programming model stays the same.
Previews need routing, not public ports
A browser worker cannot open a real TCP listener on localhost:5173.
But embedded development environments still need previews. If a Vite app
or a small HTTP server responds to a request, the user should see that
response in an iframe.
The browser-friendly answer is preview routing. Verklet uses a dedicated
preview origin and routes preview URLs into the runtime. The iframe loads
a normal URL on preview.verklet.com; the preview bridge turns that
request into a message to the runtime process, then returns the response.
That keeps untrusted preview code away from the application origin while still behaving like a normal browser preview rather than a remote tunnel.
Python starts in the tab too
Python can also be browser-first when the workload fits Pyodide. A
runtime can run supported python, python3, pip, and pip3 flows
without provisioning a backend for every visitor.
The boundary matters. Pyodide is excellent for supported packages,
notebooks, plots, and educational code. It is not a substitute for every
native wheel, subprocess, uv workflow, or Linux tool. That is why a
hybrid runtime needs a server path.
Server execution should be promotion, not a separate product
The useful hybrid model is not "browser runtime over here, remote container over there." It is one SDK surface with different backends.
With backend: 'auto', Verklet starts in the browser and promotes when a
workload needs server capabilities. Promotion exports the browser
filesystem snapshot, creates a managed server session, mounts the
snapshot into that workspace, and continues through the same runtime
object.
The host UI still calls runtime.fs, runtime.spawn(), and
runtime.on(...). The backend changed because the workload required it;
the application code did not split into two runtime integrations.
The browser is the default because it changes the economics
Containers are powerful, but they are an expensive default for every demo, tutorial, preview, and agent scratchpad. Browser-first execution changes the cost and latency profile:
- JavaScript and supported Python start close to the user.
- Files can persist locally without a server workspace.
- Previews run on an isolated preview origin.
- Server sessions are reserved for native tools, larger workloads, and persistence that should outlive browser storage.
That is the practical definition of a browser-first runtime. It is not a claim that the browser is the right host for everything. It is a runtime that starts with the cheapest capable host and moves only when the work demands it.