3

JavaScript Development: Making a Web Worker optional

 2 years ago
source link: https://itnext.io/javascript-development-making-a-web-worker-optional-f23a13490b28
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JavaScript Development: Making a Web Worker optional

In case you have a lot of JavaScript related logic running inside a main thread or a Web Worker, it makes perfect sense to move expensive logic into another worker. Workers run inside a different thread using their own CPU if possible.

However, there can be use cases where in some scenarios there is a lot of JS related workload on a given thread, while in other scenarios this thread is mostly idle.

If your thread is mostly idle, you don’t want to create an additional worker since the postMessage based communication would cause a delay compared to running the related business logic directly inside your target thread.

This article is going to cover a pretty generic approach on how to optionally use an extra worker while keeping the same API in place.

Content

  1. Introduction
  2. Creating a framework config for using the optional worker
  3. Creating the optional worker if needed
  4. Looking into the virtual dom worker
  5. Dynamically importing the vdom engine into the app worker
  6. Looking into vdom.Helper
  7. Do we need to adjust our remote definitions?
  8. How does the remote API work?
  9. Any side effects for not using the vdom worker?
  10. Final thoughts

1. Introduction

I just finished a PoC implementation for the neo.mjs UI framework to make the virtual DOM worker optional.

When using the vdom worker, our workers setup looks like this:

1*5-veT3mNdcZ0jJ4owiQwcQ.png?q=20
javascript-development-making-a-web-worker-optional-f23a13490b28

In case our App Worker (which is the main actor) is mostly idle, it can make sense to run the vdom engine directly inside this scope:

1*CkxpdeEBFoi2Z8ERDn1HrA.png?q=20
javascript-development-making-a-web-worker-optional-f23a13490b28

Obviously we want to keep the same API in place for both versions.

The concepts to make this happen can get applied outside of the neo.mjs scope. The only requirement is a solid abstraction layer to handle the cross workers communication. We are going to cover the remotes API (remote method access) a bit as well, since it provides a very elegant solution for the communication part.

2. Creating a framework config for using the optional worker

1*WmmUiPvZoWVEtHdZsIJ5_A.png?q=20
javascript-development-making-a-web-worker-optional-f23a13490b28

This part is trivial. We are just dropping a new config into:
src/DefaultConfig.mjs#L190

Now, we can use this config inside our neo-config.jsonfiles for each application we are going to build:

(this file will get auto-generated using npx neo-app )

3. Creating the optional worker if needed

This part is simple as well. We are adjusting the createWorkers() method inside worker.Manager :
src/worker/Manager.mjs#L174

In case our worker key (name) is “vdom” and our new config is set to false, we just skip the creation.

4. Looking into the virtual dom worker

To get an idea which logic we need to adjust, we should take a quick look into:
src/worker/VDom.mjs

This worker is only importing vdom.Helper (the vdom engine), so this is the only JS module which needs to get adjusted.

5. Dynamically importing the vdom engine into the app worker

Once our app worker gets the Neo.config object, we need to lazy load (dynamically import) our helper class:
src/worker/App.mjs#L222

This runs fine inside the dev mode (no builds / transpilations) and webpack will adjust the split chunks for the dist/development and dist/production environments for us.

[Side note] In case you are using the SharedWorkers based setup for multi window apps, you need to use the same useVdomWorker config value for all apps which run inside the same shared app worker instance.

6. Looking into vdom.Helper

This class (singleton) contains a lot of different methods:
src/vdom/Helper.mjs

The key part is inside static getConfig() :

We are only exposing the create() and update() methods to the App worker.

In case you are not yet familiar with the “remote method access” API, you definitely need more input on what this exactly means.

In case we are using the vdom worker, our vdom.Helper singleton lives within the vdom worker scope. In this case, Neo.vdom is undefined inside the app worker scope.

Using the remotes API will expose the two methods into the app worker. The framework will create the Neo.vdom namespace inside the app worker global scope. We can then directly call the exposed methods as promises. Example inside component.Base (app worker):

Neo.vdom.Helper.update(opts).then(data => {/*...*/});

Calling the exposed method as a Promise will internally send a postMessage from the app to the vdom worker, trigger update() with the passed opts parameter and send the return value back to the app worker, which resolves the Promise.

To keep our API in sync, we need to adjust the return values of our create() and update() methods a little bit.

Basically we have two options here:

Option one is to change all calls of this method inside our code base (is there a vdom worker in place? → call it as a promise, otherwise call it directly). This would break our API.

Option two is to change the return value:

return Neo.config.useVdomWorker ? node : Promise.resolve(node);

This approach already keeps the API in sync for both modes.

After applying the same change to the update() method, we are done.

7. Do we need to adjust our remote definitions?

You might wonder if there is a problem with the remote definition, in case we are running vdom.Helper inside the app worker scope.

1*0rJ0rzZHV9NXPP-MiCdq0A.png?q=20
javascript-development-making-a-web-worker-optional-f23a13490b28

After all, we are exposing methods to the scope we are already in :)

core.Base will check for the same scope and ignores registering methods in this case: src/core/Base.mjs#L357

So the answer is “no, we are fine”.

8. How does the remote API work?

In case you want to apply this concept to non neo.mjs based environments, you are welcome to use the code base as needed (after all, the entire project is MIT licensed).

If you are using neo already, you just need to know how to work with the API.

In both cases I strongly recommend to take a dive into the code base (just 160 lines).

worker.mixin.RemoteMethodAccess does get included into worker.Manager (main threads), as well as worker.Base .

9. Any side effects for not using the vdom worker?

Actually there are some. Rich Waters implemented this communication logic back in 2015 → long before i added MessageChannels into the mix.

The vdom worker communication runs like:

App → Main → VDom → Main → App

After the vdom worker creates deltas using the update() method or the initial vnode gets returned via create() , these message will drop the changes on their way back to the App worker into the main thread they are passing through.

The idea is to save an additional message chain:

App → Main → App

Obviously there is no postMessage chain at all in case we are running the vdom engine directly inside the app worker scope.

I adjusted component.Base.render() to trigger this.mount() as well as updateVdom() to trigger Neo.applyDeltas() in case we are not using the vdom worker.

Even though we are instantly resolving Promises inside the new non vdom worker setup, Promises are still async:

Meaning: In case you are inside a class method and trigger an update like:

this.vdom = vdom;

The new state (vnode) is not available inside this method. You can obviously use setTimeout() with 1ms afterwards to be safe.

In case you would trigger another vdom engine update before the new vnode is in place, this can cause trouble.

There is a fail safe inside component.Base to prevent new engine calls while an update is still running. The accumulated updates will happen after the previous update in this case.

However, engine calls do not lock the parent component tree for updates. This might be a good addition (feel free to open a ticket).

10. Final thoughts

The “application worker being the main actor” paradigm can significantly increase the performance of your frontend related code base.

After more than 12,000 commits inside the ecosystem, the benefits of using neo are more than I could possibly list here.

I can just repeat myself and strongly recommend to take a deep dive into the code base:

In case you would like to see the concept of optional workers getting applied to the neo data worker as well, you are welcome to open a ticket.

I tested the non vdom worker setup with the calendar app and this one worked really well.

The covid app renders fine too, but there are some side effects (e.g. table selections not getting visually applied).

Since the demo apps do not trigger a lot of JS related logic, not using the vdom worker feels a tick faster.

We could do some benchmarking to get “feels” into more precise numbers. Your help would be greatly appreciated!

I am definitely looking for more feedback on this topic in general:

Is the setup of running the virtual dom engine inside the app worker a topic of interest and something we should invest more time into to really polish it?

The roadmap is still intense, so pushing this topic further strongly relies on your input.

You are very welcome to join the Slack Channel:

Best regards & happy coding,
Tobias

PS: Here is the complete change log to make the vdom worker optional:

56 additions, 27 deletions.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK