Skip to content

Instantly share code, notes, and snippets.

@lawnsea
Last active January 2, 2021 08:26
Show Gist options
  • Save lawnsea/b72630e0fdaf818fb758 to your computer and use it in GitHub Desktop.
Save lawnsea/b72630e0fdaf818fb758 to your computer and use it in GitHub Desktop.
A discussion of the challenges I anticipate we will face creating a widely useful system based on a virtual DOM running in a web worker

I have been watching the current discussions about running a virtual DOM in a web worker with a great deal of interest. In 2011, I built a research project, Treehouse (USENIX Talk (DOMTRIS demo at 20:25), paper), which ran a hacked-up version of jsdom in a worker. My goal was fine-grained containment of untrusted scripts, while still providing access to browser APIs that existing code expected.

Treehouse achieved a small amount of influence in academic circles, but it had problems and was ultimately unsuccessful. Virtual DOMs were not a widespread or well-understood idea at the time, so the advantages of running one in a worker were difficult to communicate.

I'm very excited that the idea has found new traction. I think it promises huge performance and security wins. With that said, it's quite a bit trickier to get right than it appears on the surface. In particular, building a worker VDOM system that will work for a large set of web content is a much harder engineering problem than building one that works for a single app.

Challenges

Correctness

Here are a few ways that a worker VDOM system may get into an inconsistent state or behave in ways that surprise the user.

Synchronous interfaces and side effects

An important challenge is that some browser APIs are synchronous, but the postMessage interface is asynchronous. Fortunately, the virtual DOM abstracts away the largest synchronous API surface: the DOM. However, some tricky synchronous bits remain.

Synchronous APIs

Other than the DOM itself, synchronous APIs are fortunately relatively rare. Those that are present can usually be virtualized. Others, like window.prompt cannot.

These can probably be safely ignored.

Event capture and cancellation

There was a good twitter discussion about this. The crux is that event cancellation is synchronous in the parent page, but the application's event handlers should run in the worker, and will thus not have an opportunity to cancel the event.

The proposed solution is to cancel all events at the root of the virtual DOM. This will likely work, though I fear it will have tricky semantics for things like clicks on links.

Difficult and hidden state

There is a surprising amount of state in the browser that is hidden or difficult to monitor for changes. This state will need to be synchronized to the worker.

Input values

Changes to the value of form inputs do not trigger DOM attribute changes (though they do fire input events in modern browsers). So, how does the worker's virtual DOM get updated with the latest input values? Dispatching only input events the app listens for is insufficient, as the app may examine the value of an input that it is not listening to.

I believe it will be necessary to stream input events for every input element that is in the worker's VDOM. It may be necessary to stream key events as well.

Computed values

This is a tough one. How will the worker's VDOM know what to return if the app ask for something like an element's style.color? Similarly, what will it return if the app asks for an element's scrollHeight?

I believe this will require apps to use some new asynchronous interface instead of the usual DOM APIs.

Concurrency

This is perhaps the hardest challenge. When I designed Treehouse, I thought that I avoided the problem of handling concurrent access to VDOM nodes by requiring that a particular DOM node appear in at most one VDOM. Unfortunately, James Mickens pointed out in Pivot that I missed an additional writer: the user.

It's possible for the user to interact with a DOM that is out of sync with the VDOM in the worker. How the app would handle a UI event in this situation is unclear. I emailed with Brian Ford about this, in regards to Angular 2's plans to support worker apps. He argued that limiting the capabilities of the app mitigates this concern. In the more general case, something like Operational Transforms may work.

Performance

Running an app against a VDOM in a worker offers the usual performance advantages of a virtual DOM plus the compelling liveness win of getting the application logic off of the UI thread. However, there are some performance challenges to overcome.

High volume events

Some categories of events, such as mouse movements, can be fired at high volumes, which may degrade the throughput of the postMessage channel by interfering with higher-priority messages.

It may be possible to throttle, debounce, and/or batch these events in many circumstances. Note, however, that doing so will often result in higher latency.

Latency-sensitive events

User experience can be extremely sensitive to latency when handling certain UI events. Dragging is a great example, as it also requires the app to listen to mouse movement events, thus requiring subscription to a high-volume stream of events.

One possible mitigation is to install handlers for things like mouse movement events in the parent page itself, in the spirit of kernel-mode device drivers. This will make it harder to provide performance and security guarantees.

Another solution is to optimize the throughput and latency of the postMessage channel. Two costs that can be optimized are memory copying and serialization of messages. It may be faster to serialize VDOM patches and events directly into a binary format, perhaps via a ring buffer of ArrayBuffers that can be transferred between the worker and the parent page without copying.

Containment

While running an app in a worker provides exciting security benefits, our objective here is better performance and responsiveness, so I will leave containment in the sense of security for another discussion.

However, it's important to note that certain APIs may unintentionally affect the parent page. For example any non-inline styles the app injects into the DOM may have global effects.

Developer experience

The performance and responsiveness gains realized by running a VDOM in a worker come at a substantial cost in terms of developer convenience. Since web workers have not seen substantial uptake outside of certain specialized applications, developer tooling is somewhat spotty.

Debugging

Things have certainly improved since I built Treehouse in the fall of 2011, when source-level debugging of workers was not supported by any browser (IE10 was the first). Chrome, IE, and Edge(?) support source-level debugging of workers. AFAICT, Firefox and Safari do not. Safari also lacks support for the console API, though that can be polyfilled via postMessage.

DOM inspection

Developers are accustomed to inspecting the state of the DOM via the developer tools. No such equivalent exists for inspecting a VDOM in a worker.

Profiling

I have not investigated what capabilities currently exist for profiling the performance of code in a worker, but I would be surprised if they are not limited when compared to those available for profiling the UI thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment