We would like to observe changes to the DOM and, for each changed element el
, call one or more functions f1, f2, ..., fn
, passing el
as an argument to each. For each function, if it returns true, add a CSS class C
to el
, and otherwise remove class C
from el
.
Register a mutation observer on the root element of the DOM. The mutation observer is responsible for tracking the "generation" of all elements in the DOM and for marshalling MutationRecord
s to a Web Worker. Specifically:
- For each mutation:
- increment the generation of the mutated element and its ancestors
- serialize the MutationRecord and generation id
- postMessage the serialized mutations to the worker
Next, in the worker, instantiate a virtual DOM and register a mutation observer on its root. Then register a postMessage listener that is responsible for calling all functions in [F]
, serializing the results, and sending them back to the parent. Specifically:
For each mutation:
- find the mutated element in the VDOM (or add it if it doesn't exist)
- For each function
f1, f2, ..., fn
, call it, passing the mutated element- if a function
fi
returns true, set some classCi
on the mutated element - otherwise, remove some class
Ci
from the mutated element
- if a function
The mutation observer is responsible for serializing the changes to the VDOM and sending them to the parent page:
- For each mutation
- serialize the MutationRecord and the mutated element's generation id
- postMessage the serialized mutations to the parent page
Finally, register a postMessage listener in the parent that is responsible for applying the serialized changes from the worker. Specifically:
- Stop listening to mutations
- For each mutation:
- compare the generational id of the mutation with that of the mutated element in the real DOM
- if the generations are the same, apply the mutation
- otherwise, drop the mutation
- compare the generational id of the mutation with that of the mutated element in the real DOM
- Start listening to mutations again
The generation tracking is intended to deal with data races. Specifically, we should not apply updates from the worker that were the result of a change to an older version of the mutated element. So, we drop the update because we know that the change that resulted in the most recent version of the element is in the pipeline.
In many cases, this may be fast enough as is. Two optimizations that seem likely to help are to (1) batch mutations and only send the resulting diff and (2) encode the mutations in an ArrayBuffer
and transfer it between the worker and parent page. For (1), I am envisioning a VDOM on both sides - parent and worker. For (2), careful benchmarking is necessary to be sure that the cost of the structured clone is in fact higher than the cost to create a binary encoding of the mutations.
I'm curious, did it actually work as expected eventually? :)