The browser is a marvel of engineering, but let's be honest: the DOM is slow. When you're trying to render 100,000 independent moving particles that also need to act as interactive UI elements, standard HTML/CSS falls apart. The garbage collector panics, the main thread locks up, and the user's laptop fan spins up like a jet engine.
I decided I didn't want to use the DOM anymore. I wanted to build a UI entirely inside a WebGL canvas, bypassing the layout engine entirely. This is the story of how I lost three weeks of my life to matrix multiplication.
"At 3 AM, I realized I was writing my own text-wrapping algorithm in GLSL. That's when I knew I had gone too far."
— Commit message, Day 14
The Architecture
To make this work, I had to essentially rebuild a minimalist version of the browser's rendering engine. I created a hierarchical scene graph where every "div" was a PlaneGeometry, and text was rendered using Signed Distance Fields (SDF) to keep it crisp at any zoom level.
Event delegation was another nightmare. Since there's only one actual DOM element (the canvas), I had to cast a ray from the mouse position through the camera frustum on every pointer move, intersecting it against bounding boxes mapped to the "virtual" DOM elements.
I used a spatial hash grid to optimize the raycasting, which kept the frame rate locked at 60fps even with 10k interactive nodes. You can read more about that approach in this excellent breakdown of spatial partitioning.
export function intersectVirtualDOM(
pointer: Vector2,
camera: Camera,
sceneGraph: VirtualNode[]
) {
// Normalized Device Coordinates
_raycaster.setFromCamera(pointer, camera);
// The brutal reality of O(N) checking
const hits = [];
for (let i = 0; i < sceneGraph.length; i++) {
const node = sceneGraph[i];
if (node.isInteractive && checkBounds(node, _raycaster.ray)) {
hits.push(node);
}
}
return hits.sort((a, b) => b.zIndex - a.zIndex)[0];
}
The Breaking Point
It all worked beautifully until I tried to implement accessibility. Trying to map a complex WebGL scene graph to an invisible, screen-reader-friendly DOM tree in real-time proved to be a sync nightmare. The state drift between the visual representation and the a11y layer was unacceptable.
Ultimately, I abandoned the project. It remains a fascinating monument to over-engineering. The repo is public if you want to poke at the corpse.