Skip to main content

12 posts tagged with "Teardown"

How a piece of Vivari actually works, and what it cost to build.

View All Tags

A Nuxt dev server in a tab cost 3.46 GB, and the biggest thing we could actually shrink was the filesystem

· 10 min read

A tab running nuxt dev was using 3.46 GB. That is not a number you optimise your way out of with a hunch, and the hunch everybody has is the same one: bundlers are memory hogs, so it must be the bundler.

It was not the bundler, whose entire contribution was 22.5 MB. It was three other things, and they are not even measured in the same units: a filesystem holding 929 MB of node_modules as bytes, a persistence layer writing 4.7 GB to store 53 MB, and an editor spending 621 MB parsing one set of type definitions twice.

This post is the autopsy rather than a list of tips, because the useful part is which assumptions turned out to be wrong.

Bun runs in the tab and there is no Bun in it, so the interesting part is what refuses

· 11 min read

Bun is a single native binary written in Zig around JavaScriptCore. There is no wasm32 build of it, there is not going to be one soon, and a browser tab cannot execute a Mach-O or ELF file regardless.

So bun index.ts in a page is not Bun. It is Bun's API, implemented on top of the Node runtime these posts keep describing, and the honest version of that sentence is the whole subject here. A compatibility shim is only useful if you can tell, from inside it, which parts are real. Most of this post is about the parts that are not, and how they say so.

Node can require() an ES module now, and it refuses two things. We could not refuse either

· 15 min read

Recent Node versions will let you require() an ES module. It is a genuinely hard thing to have shipped, and it comes with two documented refusals: ERR_REQUIRE_ASYNC_MODULE if anything in the required graph uses top-level await, and ERR_REQUIRE_CYCLE_MODULE if the graph has a cycle that crosses the CommonJS boundary.

Node can refuse, because import() is always sitting there as an escape hatch. Tell the user to await it and the problem is theirs.

In a browser worker there is no escape hatch. require() is synchronous all the way down because the filesystem under it is, and a project's entry point is required by a loader that cannot return a promise to anyone. Every import becomes CommonJS at load time or the program does not start. Refusing was not on the menu.

A step debugger with no inspector to talk to, and the second SharedArrayBuffer that makes it pause

· 12 min read

A breakpoint is not a feature you write. It is a favour the engine does you. When you set one in VS Code, nothing in your program changes: V8's inspector holds the isolate, walks the real call stack, and hands back scopes it already had. Every step debugger you have used is a thin client in front of that.

A Web Worker has no inspector. There is no --inspect port to open, no inspector binding to require, no way to ask the engine to stop. So the first question here was not how to build a debug UI. It was where a pause could possibly come from.

There was never a second import pandas, and PEP 552 is why there is now

· 11 min read

On your laptop, the first import pandas of the day is slow and every one after it is fast. You have probably never thought about why. CPython compiles the package's .py files to bytecode, writes that bytecode into __pycache__, and never does it again.

Run Python inside a browser tab, where every command is its own process with its own interpreter and a freshly unpacked copy of every package, and something uncomfortable follows. There is no second time. Every import pandas is the first one.

Flask, Django and FastAPI answering real requests, with no socket underneath

· 15 min read

Every Python web framework bottoms out in the same two lines, whatever it calls them:

sock.bind((host, port))
sock.listen(backlog)

A browser tab does not have that. There is no TCP stack in a page, no file descriptor to bind, and no amount of WebAssembly changes it. So the interesting question is not whether you can run Flask's Python in a browser, because you can. It is what happens when someone types flask run.

Next.js 16 renders React Server Components in a browser tab, and the AsyncLocalStorage trap

· 8 min read

For most of this project's life, our notes said Next.js was out of reach. The reasoning looked solid: Next compiles with SWC, SWC is native Rust, there is no native code in a browser tab, therefore no Next.js. A hard wall, filed away.

That verdict was wrong, and it was wrong in the most ordinary way: we had decided something was impossible and then stopped rechecking it.

next dev --webpack now boots inside a browser tab, compiles an App Router page, renders React Server Components, and answers GET / → 200 with real HTML. No server. The kernel, the filesystem, the process model, the dev server and the React render all live in one tab.

Getting there was mostly unremarkable engineering, plus one problem that has no correct solution.

Three ways to isolate a preview, and the Cloudflare wildcard trick

· 8 min read

When an in-browser IDE runs your dev server and shows you the result, the result has to be served from somewhere. The easy answer is the origin you already have: put the preview at /preview/5173/ on the IDE's own domain, let the Service Worker route by path, ship it.

That is what we did, and it is a security problem.

Same origin means the same cookie jar, the same localStorage, the same IndexedDB, the same OPFS, the same Cache Storage, the same Service Worker scope. Preview code, which includes every npm package the project installed and anything an AI assistant just generated, sits inside the IDE's origin. It can read the editor's session state, corrupt its persistence, and call its same-origin APIs. Previews are not isolated from the IDE, and they are not isolated from each other.

Fixing this properly took three attempts, and each one ran into a different piece of web platform trivia.

Real npm, yarn and pnpm in the browser, and how each one broke the runtime

· 8 min read

Once you are running Node's real lib/ in a tab, the interesting question stops being "can we reimplement npm?" and becomes "why would we?". The package managers are just Node programs. If the runtime underneath them is honest, the real CLIs should run unmodified.

They do. npm, yarn, pnpm and corepack in Vivari are the actual published releases, vendored and executed as-is. But "should run" and "runs" are separated by every Node API each tool happens to touch, and the four of them touch almost disjoint sets. Each one, in turn, walked into a different corner of the runtime and found the wall.