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.
Here are a few ways that a worker VDOM system may get into an inconsistent state or behave in ways that surprise the user.
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.
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.
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.
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.
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.
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.
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.
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.
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 ArrayBuffer
s that can be
transferred between the worker and the parent page without copying.
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.
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.
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
.
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.
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.