- What is neo.mjs?
- Enhancing the createApp program
- Development mode (Chrome v83+)
- Dist modes → Chrome
- Dist modes → Firefox
- What about Safari?
- Can we move Component Trees from one Window to another?
- Can we further scale the architecture if needed?
- What is coming next?
- v1.2.5 under the hood
- Recap: What are remote methods?
- SharedWorkers on mobile?
- Online Examples
- Final thoughts
To me, this one feels like a milestone in the history of UI development.
A quick recap:
Chrome v83 enables JS module support for SharedWorkers → Starting a new era for multi Browser…
After Chrome added the full support for SharedWorkers, I was thrilled to get a first PoC ready as fast as possible. This version was using hacks and inside a separate branch, so it needed to get fully supported in a more clean way.
With this release, we can now decide if we do want to use SharedWorkers or stick to normal ones for each App.
The exciting part is, that there is also support for the webpack based dist versions in place now. Meaning: you can not only use it with Chrome v83+, but also bundled for production with older Versions of Chrome and even with Firefox.
In case you read the previous article, feel free to skip section 2 (redundant).
Make sure to read the highlight of this article: section 8.
2. What is neo.mjs?
neo.mjs is a webworkers driven UI framework I am working on. The first public release was on November 23, 2019.
The entire code base as well as all demo Apps are using the MIT license.
The webworkers driven UI framework (Beta version). Contribute to neomjs/neo development by creating an account on…
In short: neo.mjs is using 4 threads by default:
- Main (top level)
The main thread only creates the 3 workers, delegates virtualised UI events to the App worker and manipulates the real DOM (applying delta updates).
You can enhance Main using main thread addons, e.g. in case you want to work with external libraries like AmCharts or MapboxGL, which have to run inside the main thread.
Most parts of the framework as well as the Apps you build with it run inside the App worker.
This leaves the main thread mostly idle, leading to a blazing fast rendering performance.
To really get the idea, let us take a quick look at the Covid Dashboard Demo App without using SharedWorkers first:
MapboxGL is spawning some workers on its own. While workers can not access the DOM (window and window.document are undefined inside the worker scope), they can however directly access Canvas. So this is a good use case.
If you look at the console you will see the App, Data & Vdom workers.
Looking closer into the App worker:
Looking closer into the App worker (console), you will see that the Demo App is living there. This is the real code (JS modules & classes) without using source maps. You can work on your code base outside of nodejs. You don’t need any JS builds or transpilations to directly run it inside the browser.
3. Enhancing the createApp program
In case you are creating a new App, you can now pick the config:
Quick look at the help options:
node ./buildScripts/createApp.js --help
-u is the new one.
npm run create-app
In case you don’t want to use command line options, you can use the inquirer UI instead.
The option also got added for:
4. Development mode (Chrome v83+)
You can find the new online demo here:
Open another Browser Tab and enter:
SharedWorkers are not shown inside the default console, so opening them inside a new Tab or Window feels mandatory for debugging.
Click on “inspect” for the App worker:
Now this is the real deal: a SharedWorker using JS modules. This is the unchanged code, no source-maps, no builds or transpilations. It just runs inside the Browser as it should be. You don’t need to develop UIs inside nodejs.
Looking at our App:
Looking at our App: compared to the version above, the App, Data & VDom workers are no longer listed inside the left Panel of the dev tools.
The main thread is only importing very little parts of the neo.mjs framework to be super lightweight. As before, the App related JS modules live inside the App worker realm.
JS modules are in place here as well, so debugging is a fun experience: change the code, reload your browser; like in the good old days.
5. Dist modes → Chrome
The dist modes (development & production) are the highlights of this release. You will stick to the real dev mode for development and once your App is ready you can just deploy it to a wider Range of browsers (Chrome, Edge, Firefox).
is using source-maps and is not minified, while
is not using source-maps and minified.
Lets us take a look at dist/development:
You can spot the difference right away: webpack is creating replacements for the non existing import statements.
You will also notice that the main thread is including more than 1 file:
- main.js: the real main thread
- src/main/addon: addons get dynamically imported and you can pick which ones to use for each App separately.
We only get one file inside the App worker: a combination of our App related code base and the App worker code itself.
You can see the red folders & yellow files as the result of the source map.
5. Dist modes → Firefox
Getting this one to work was a bit tricky. I love the result though. A lot!
Pretty much the same as before, let us pick production this time:
Open another Browser Tab, enter:
Click on “Inspect” for the App worker again:
Well, as mentioned dist/production is minified and without source-maps :)
Quick look at dist/development:
Pretty much the same as in Chrome and reasonable for debugging, since you only need to work on the differences in Chrome & FF.
7. What about Safari?
From what I found on the Web, the Webkit team started working on SharedWorkers and then dropped it on purpose.
There is a new open ticket:
149850 - Reinstate support for SharedWorkers
Bug 149850: Reinstate support for SharedWorkers
In case you care about it (you really should!), please make sure to add some weight there. The more devs vote for this, the higher the chance that this will happen. I just added a comment as well.
It would make me sad to see Safari turning into the IE6 of the modern Web.
neo.mjs is now smart enough to check, if a Browser does support SharedWorkers or not. So you can open the Covid demo for shared workers using Safari, but it will just fall back to use non shared workers instead.
8. Can we move Component Trees from one Window to another?
Now this is my favourite part.
Since all Apps for all Browser Windows live within the shared App worker, they can directly communicate to each other. We don’t even need postMessages.
The shared App worker is using just one IdGenerator, so all Components for all Apps have unique Component IDs.
Since we are using one shared Vdom worker as well, this does ensure that all DOM nodes across all Windows have unique IDs too.
This makes it completely trivial to just move Components or entire Component Trees around as you like.
Even better: DOM events are de-coupled right from the start, since their handler functions live inside the App worker. So, if you move e.g. a Button from one Window to another, the click handler will still work.
9. Can we further scale the architecture if needed?
neo.mjs is all about scaling UIs, so the answer is yes.
In case the VDom worker should become a bottleneck at some point, we could just add a second one and then use odd IDs for the first one and even IDs for the second one. You get the idea. Easy, right?
We could think about multiple App workers as well, but this would make things a lot more complicated. Better move expensive tasks somewhere else (e.g. the Data worker is still at an early stage).
Right now, most parts (only the ones which you need for your App(s)) of the framework already run inside the App worker, so the main thread(s) are pretty lightweight. At this point 37KB for dist/production on the JS side, excluding addons. We could further decrease this if needed.
10. What is coming next?
I am going to further enhance the support for SharedWorkers next. The missing piece is to store connections (ports) inside the App worker and map those to Apps (main threads => Windows).
Once this is in place, I want to enhance the SharedWorkers based Covid App to use multiple Windows.
- Open Window 1 => the App is like before
- Open Window 2 => move the Charts Panel from Window 1 into 2 and make it fullscreen
- Open Window 3 => remove the Helix Tab and add it into Window 3, full screen
- Open Window 4=> remove the Gallery Tab and add it into Window 4, full screen
Now imagine what happens in case we select Countries in Window1?
If this is only half as good as I think it should be, we can expect a beautiful animation firework.
This should be the next release and a new Blog Post.
I also want to finish the Tutorial Part 2 on how to create the Covid App, to give you a better chance to get up to speed.
One epic item is to support dynamic imports for the app realm. While this works out of the box for the development mode, it will be a lot of work to make this happen inside the webpack based dist env. Think about split-chunks.
11. v1.2.5 under the hood
I know, many of you guys reading this are JS pros :)
So I bet that a lot of you have one question in mind at this point:
“Where is the code?”
There we go:
[core.Base is worth a look anyway (hint!).]
When working on the dist versions (especially FF), I realised that creating the worker instances and onConnect() had a bigger delay than inside the development mode.
So, what happened was that class instances containing remote methods wanted to register them to other threads before the port was connected.
Meaning: post.postMessage() was not available yet.
It was an easy fix though: inside SharedWorkers, there is a new connected event as well as an isConnected config. Each method call that would trigger to early is just getting delayed until the connection is established.
12. Recap: What are remote methods?
Rich Waters added the remote methods API at a very early stage already (back in 2015 I think).
The idea is to make the cross worker communication as easy as possible.
We have the method getBoundingClientRect(), which lives inside the main thread. Scroll up to line 40:
We expose this one to the App worker.
Inside the App worker, Neo.main would be undefined (since it does not import modules from the main thread).
All Component classes live inside the App realm and we did expose getBoundingClientRect() to it.
As a result, neo.mjs will create the namespace Neo.main.DomAccess and you can just call the method directly. The only difference: this one is a Promise, so you need to use then() to get the return value.
Under the hood: postMessage() from App to Main, get the JSON based version of a DOM rect, send it back to Main.
13 .SharedWorkers on mobile?
I picked “13” for this on purpose.
Not there yet.
Now you might ask: Wait, why would I want multiple Windows on my tiny phone?
The answer might be inspirational: Think about a native shell with multiple WebViews.
Now you should think: Hmm, iOS does only support Safari and all other Browsers you install there are “Fake”, since they do use Webkit under the hood anyway.
[Hint: Scroll back up to section 7 and add some weight to the Webkit Bugzilla Ticket.]
14. Online Examples
Before I share the link:
There is a red line saying “not optimised for mobile yet”.
Meaning: Do not look at them with your phone yet. Seriously, just don’t!
I think the non shared worker setup can rock on mobile as well, but I have still not found the time to dive into it. On the roadmap though.
15. Final thoughts
I hope that at this point you got an idea about the scope of neo.mjs as well as the potential moving forward.
neo.mjs is an Open Source project (the entire code base as well as all examples are using the MIT license).
Meaning: you can use it for free.
It will stay like this.
However, the project is in need for more contributors as well as sponsors.
A lot(!) more items & ideas are on the roadmap.
If you want to contribute to a lovely Open Source project, this would be highly appreciated.
In case the project has or will have business value for your company: signing up as a sponsor can allow me to put more time into it, resulting in a faster delivery time for new things.
Famous last words: Inside the worker scope, window as well as window.document do not exist. In German, we call this “Kindersicherung”.
Best regards & happy coding,