Skip to content

DOM Syncing Instructions

Brad Peabody edited this page Jul 28, 2019 · 2 revisions

DOM Syncing

One of the key pieces of functionality that Vugu needs to provide is that of "DOM syncing". And by this is meant taking a graph of VGNode elements that have been generated by the various components and making the browser's DOM match it.

js.Value is convenient but slow and leaks memory

As it stands right now, WebAssembly cannot access the browser's DOM directly. You can however reference it indirectly using a Value from syscall/js. While convenient, there are two main drawbacks to this approach: 1) Calling from Go wasm code into the browser is slow (more specifics below); and 2) these references are not (to my knowledge) garbage collected. And UI applications will definitely produce DOM garbage. So basically while it's convenient programmatically to have a bunch of js.Value instances around and be doing js.Value.Call all over the place for DOM manipulation - it's slow and it's gonna run out of memory and crash. Perhaps in the future this situation will improve. But if we want things to work well today, we need a different approach.

Some informal benchmarking gave me the following numbers: A. Making a call from Go to a JS function that concatenates a few characters to the string and passes it back: 25us per call. B. Making a call from Go to a JS function that creates a DOM element and sets its innerHTML to the value of the string passed: 42us per call.

Ouch. This means that the overhead of calling from Go to JS is almost twice as long as creating, filling and attaching a DOM element. I think it's clear that if Vugu is going to compete on performance, we need to significantly reduce the number of calls between Go/WASM. And while we're at it, we should do it in a way that doesn't use js.Value and eat up references.

Let's Make an Instruction Buffer

While I am normally the first one to eschew obfuscation, I believe the situation described above warrants a more efficient, albeit also more complicated, solution.

Using js.TypedArrayOf, we can make a reusable buffer that exists in Go's memory space but can also be accessed directly from JS. It's a single js.Value that can store a bunch of data and we can just reuse it. This solves our memory leak issue. We just need to figure out how to describe what needs to be synced and put it into this buffer.

And with a large buffer, the idea would be to encode a bunch of instructions in there that describe what needs to be done to sync the DOM (e.g. "there should be a first child under element with hash ID X that is a DIV, if not make it so", and "set the attributes on the last mentioned DOM node to this set", and so on). And then we only need to call from Go into JS once for a whole set of data. Instead of making thousands of calls from Go into JS during a sync, it could be only a few. This of course requires some JS code on the other side to implement this, but it's probably a good tradeoff if it's going to result in something that is both stable and performant, and can do it without waiting for new Wasm features.

This also has an interesting side effect of forcing the DOM syncing operations to be well defined and at least to some degree thought through. Since we need to express them in some sort of in-memory binary format, it forces more thought to be given to it, which I view as a good thing.