Skip to content

Instantly share code, notes, and snippets.

@lawnsea
Last active September 29, 2020 10:19
Show Gist options
  • Save lawnsea/be39347a68a3ae71219c to your computer and use it in GitHub Desktop.
Save lawnsea/be39347a68a3ae71219c to your computer and use it in GitHub Desktop.
Hitching in a Web Worker

Goal

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.

Solution

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 MutationRecords to a Web Worker. Specifically:

  1. For each mutation:
    • increment the generation of the mutated element and its ancestors
    • serialize the MutationRecord and generation id
  2. 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 class Ci on the mutated element
    • otherwise, remove some class Ci from the mutated element

The mutation observer is responsible for serializing the changes to the VDOM and sending them to the parent page:

  1. For each mutation
  • serialize the MutationRecord and the mutated element's generation id
  1. 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:

  1. Stop listening to mutations
  2. 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
  3. Start listening to mutations again

Correctness

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.

Performance

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.

@muodov
Copy link

muodov commented Sep 29, 2020

I'm curious, did it actually work as expected eventually? :)

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