Expanding Single Page Apps into multiple Browser Windows — Part 2

Tobias Uhlig
ITNEXT
Published in
9 min readMay 7, 2021

--

including cross browser window lazy loaded CSS delta updates

This article covers a disruptive new approach on how we can further improve the rendering performance of web based frontends, since we will lazy load CSS exactly as needed.

Content

  1. Introduction
  2. The three different environments in neo.mjs
  3. Why is OOP “out of fashion”?
  4. Cross browser window CSS delta updates
  5. How does this work?
  6. What is coming next?
  7. Call for action!

1. Introduction

It has been a while since I have written part 1. In case you missed it, it is definitely still worth reading:

The neo.mjs project has evolved a lot. Let us take a look at the multi window covid dashboard app again:

The application worker is still the main actor. In neo.mjs we are not just moving expensive tasks into dedicated or shared workers. Instead, your applications and components really live there. We could describe main threads as “rendering workers”. Main is just 42KB of code which creates the workers, manipulates the DOM and delegates UI events.

If you focus on the app worker console, you will see that we are now lazy loading JS modules for new views as needed. We do not need to load them again when moving an existing component tree into a different window. We also do not need to create new JS instances of our views, we literally re-use the existing ones.

Prior to creating the new theming engine, the neo.mjs project was using a monolithic CSS out put, which did not do justice to the beautiful way of how the Javascript side works.

In case you are interested on how to create a custom SCSS build, using sass.render() from a file buffer, feel free to dive into my previous article.

The article also contains infos about the neo.mjs SCSS structure in general (e.g. explaining the different modes → building SCSS with either containing CSS variables or not).

With a granular CSS output in place, we can now lazy load our class based CSS files as needed:

At this point you might wonder: “Well, I can create CSS split chunks using webpack as well, so what is the big deal?”

This is correct, but wait for the multi window context :)

2. The three different environments in neo.mjs

I got the feedback that some of the basic concepts are still not clear. Before we jump into the “good stuff”, let us cover them in short to ensure we are on the same page.

In case you open the online examples
https://neomjs.github.io/pages/

you will see three different modes:

dist/production
We are using a webpack based build on the JS side. We get minified split chunks, even across different apps. The CSS output is minified as well. No source maps for both. This is the fastest version which you deploy once you are done with your development.

dist/development
In case you are using e.g. Angular, React or Vue, this is what you are used to call the development mode. It is a not minified webpack based build containing source maps on the JS side, to enable you to see where potential bugs happen in reality.

development mode
Now this is a part where neo.mjs is unique. You can run the real code as it is directly inside your browser. In case you change your code base, you get those changes right away. There is no need for builds, transpilations or hot module replacements. You can be 100% sure that there are no external factors causing bugs.

Bringing UI development back into the browser is not just a romantic idea. Browsers are ready for it. You should be too.

3. Why is OOP “out of fashion”?

We can extend classes. This allows us to add new methods, override methods, add and change class fields. This can reduce a lot of redundancy. You can use the factory design pattern to get close to it, but it can get a mess in big projects.

Adding a config system on top of the ES6 class system makes extending classes even more powerful. We can bulk update configs and reduce the amount of calls to the vdom engine.

I have experienced many high profile projects dealing with memory leaks. It happens frequently that developers do a good job at creating components, but ignore the destructing aspects. Like using a data store and not destroying it if needed.

So, what went wrong?

The real problem are declarative (template driven) libraries and frameworks. Several of them offer a Component.render() method, which allows you to define markup including custom tags matching component names. Calling render() will create JS instances for used custom tags. Calling render() again will destroy previous instances and create new ones.

Now guess what will happen in case you change the state? Right, this will call render() . A multiplier for memory leaks and in my opinion one of the key reasons functional programming got popular.

This is a very different story for programmatic approaches where the JS side is in charge. Calling Component.render() in neo.mjs will neither create nor destroy any JS instances. You are in control if and when you want to do this. This makes “re-using components (JS instances)” possible, which is what I talk about a lot when mentioning to move the component trees across different browser windows while even then keeping the same JS instances.

Your components deserve more love than a one way ticket. Used right, there is nothing wrong with OOP.

4. Cross browser window CSS delta updates

Back to the multi window covid app. In case you take a look at the different entry points:

apps/sharedcovid, apps/sharedcovidchart, apps/sharedcovidgallery, etc.

you will notice that only the first (main) app contains multiple views and controllers. All child apps simply contain an empty Viewport.

In case we are moving a view into a different browser window, we can just use:

parentView.remove(view, false); // false keeps the instance alive
me.getMainView(appName).add(view);

It really is this easy.

Now on the CSS side for a child app, we expect to load the files for the Viewport (and parent chain) initially and once we dynamically move a view from the main app load the missing CSS files at this point.

It is a little bit tricky to show using popup windows (without adding delays), so I am using normal windows instead to show it. I also added a 3s delay to make it more clear:

The probably most impressive demo right now is using the CSS delta updates inside the cross browser window drag & drop demo.

This app needed a little adjustment: when starting a drag operation in one window, we create a drag proxy element (node) when dragging into the other window. The drag proxy is based on the dialog styling. However, we most likely have not created a dialog inside “window2” yet.

We can fix this with a one liner though:

Neo.currentWorker.insertThemeFiles(dockedWindowAppName, Neo.dialog.Base.prototype);

In case you watch the console logs (network → CSS), you will see that the dialog styles get loaded inside window2, when the proxy enters it.

On drop, the window will also load the dialog content styles (text fields). Starting a drag OP in window2 will load the real drag proxy CSS (which were not needed before). Further drag & drop OPs do not require to load files (no deltas left).

5. How does this work?

The challenging part was definitely to generate the file based CSS output. You can dive into the details in the article link at the top.

I needed to “monkey-patch” the source maps a little bit. The line numbers were not always correct, so we need to ensure that the file buffer previous to the content file ends up in one line (removing comments first, then removing line breaks). The maps also showed “stdin” instead of the file name. Easy to fix with adjusting the internal ArraySet mappings.

dev/buildScripts/buildThemes.js#L264

The more important addition to this file is generating a JSON based file structure map:

(It is minified in reality.)

Our application worker now needs to load this file (or the version without using CSS variables) once, even when using multiple windows.

Since the file could arrive after the first components need their CSS, I added a caching map inside the app worker as well.

Classes like component.Base are using an appName_ config. In case there is a change, it will trigger:

This happens in case we create a new instance of a class, but also when inserting an existing instance into a container.Base .

This class will update direct child items. So when we do insert any kind of component trees into a new app (different browser window), all child items will get the new appName resulting in loading their CSS files as well inside the new main thread (if needed).

The app worker does the following:

It does check the cssMap if the class has any CSS files. It also checks the prototype chain (e.g. button.Split can use styles of button.Base ).

Adding a file will add a flag to the map to ensure it does not get loaded more than once for a given scope (app).

This is already all we need to enable cross window CSS delta updates.

6. What is coming next?

To finish the v2.1 release, I still need to enhance workspaces which use neo.mjs as a node module, to enable theming on this level.

workspaces should contain their own resources/scss folder, so the build needs to add files generated here into the mix.

I can create a quick guide about creating an app inside a workspace, creating custom components there and styling them. Let me know in case this does sound interesting for you!

Right now I am getting some dependency warnings regarding JSDoc as well as jsdoc-x. Both projects are no longer maintained, which is a problem. They are using dependencies containing deprecated packages including security issues. We could probably just fork them to figure out if manually adjusting the dependencies does fix this. If not, we probably need to switch to something else or create a custom doc comment parsing engine. Help on this issue is appreciated.

In general I would love to create some shiny new components, but I am trying to stay focussed on the foundation instead. Meaning: pushing the core and ecosystem to make it easier for you to create your own components and apps.

7. Call for action!

The initial rendering for most example apps got significantly faster. Don’t trust my word, see it for yourself:

In case you need help getting up to speed, it is appreciated to join the neo.mjs Slack Channel:

https://join.slack.com/t/neotericjs/

Limited time on my end, but I do enjoy pointing curious devs into the right direction.

In case your company does need help creating a prototype app to figure out how neo.mjs plays out in their scenario, just ping me.

As a MIT licensed open source project, neo.mjs relies on your support. You can influence the roadmap. I listed several potential items inside the v2 release announcement:

In case you want to see new articles on specific topic, you are welcome to create tickets for it: github.com/neomjs/neo/issues

Best regards & happy coding,
Tobias

Preview Image:

--

--