Lithium UI (web UI Framework) Internals

It's been a while since I started developing Lithium UI - a web UI framework. I've been working on it since Dec 2013.

I developed Lithium UI for myself and not for the public (even though it's on GitHub with an MIT license :D). Yet, this article will touch on various other frameworks, their internals and how LUI compares to them.

Before I continue, note that this article is not for anyone who is new to client-side JS frameworks. I assume the reader has sufficient experience with at least one framework and has compared their framework of choice with other alternatives.
So let's begin. Firstly..

What is Lithium UI?

LUI is a UI framework for organizing the view part of a web application. It does not include routers or AJAX or other HTML5 features.
This framework is mostly useful for single page apps (eg: WYSIWYG website builder, web mail client) [1].

LUI tries to solve the same problems which other frameworks try to solve 1. Code organization and 2. Making updates to the view easier.

Next I'll move on to explain, how some of the internals of LUI are different from other frameworks and the value they give.

Components

Tighter coupling where it makes sense

We know that the "code" for a component consists of at least two languages/markup: One JS and the other HTML. However from a logical perspective and code organization perspective they are just dependent on each other and should be treated as one.

LUI takes a (not very common) approach of almost always assuming that if a component exists, then the view for it exists as well (as DOM nodes). i.e. LUI renders a component's view in-memory (in a DocumentFragment) on construction.
This in-memory render early in the component's life-cycle reduces several internal checks. For example, ExtJS 3 components would be littered with checks on whether the component has been rendered or not (almost every component had a check; and often on multiple methods). With LUI, one can be certain after construction, that component.el (i.e. root element of component) will exist.
Also one can add event listeners right after calling the constructor. There is no need to wait till the DOM nodes are attached to the document.

I don't know of any framework that thinks that way. A component is useless without a view; and the view can't be created without the component's states/props. They are inherently, conceptually coupled. Then what is the harm in an in-memory render on construction?

Communication

It is a very common need for a parent component to request it's child component to do/execute some action. With LUI, communication from parent to child is as easy as calling a method on the child.
I have to criticize popular frameworks for not seeing the simplicity of this approach. Angular needs pub-sub events to communicate from parent-to-child component (controller), which I see as unnecessary. And React sees this approach as uncommon (why?) and instead encourages to use a prop, then check for changes to the prop in componentDidUpdate() event. It is more complicated than it needs to be. Calling a method is much more straight-forward, easier to debug and is perfectly valid for parent-to-child communication.

For communicating across component heirarchies, use a publisher-subscriber event system (Li.Publisher/Redux/Flux/whatever) to avoid tight coupling.

Data binding & Change detection

Goals

LUI's data binding & change detection is a trade-off between four seemingly conflicting goals:

  1. Maximize convenience.
  2. Maximize performance.
  3. Achieve #1 and #2, but also ensure the developer knows about performance side-effects.
  4. Ensure changes done to the view are predictable.

The How

It was difficult to take an approach that maximizes the above goals. The most challenging trade-offs are in the change detection.

There is an interesting article on change detection at teropa.info. LUI takes the approach of making developers add "proxies" for each state that can potentially change the view. This is inspired from KnockoutJS's ko.observable(). Also KnockoutJS has a templating syntax which LUI imitates (for the most part).

Advantages

Performance: Performance-wise, the approach of using a proxy is close to vanilla JS. Updating the UI is an O(1) time-complexity operation.
(There is however one special case where LUI resorts to array diffing for change detection, which is an O(N) time-complexity operation).

"How O(1)?"

LUI has a template syntax where one can bind JS expressions to any element for specific DOM operations.
That combined with proxies/"observables", LUI can auto-detect all the observables used by each JS expression.
When an observable changes state, LUI knows exactly which DOM elements to update and how to update. So if an observable is used on a single element, then the operation is O(1).
If developer has used the same observable on multiple elements then the operation is O(constant) = O(1).


Predictibility: The high performance enables us to synchronously update the view. This indeed makes it easy for one to visualize, how the view will look like when a particular line of code executes. It much more predictable as compared to React or Angular's approach where updates to the view are asynchronous.
Async rendering has the bad side-effect of making things like text field focus (or text range selection) harder since it can get affected by an update from the framework on the next render tick.

Secondly, unlike KO, LUI only does one-way binding. i.e. changes to states updates the view but user interactions won't automatically change states. State change need to be handled by the developer/application logic.
Some may be turned off by having to do more work. But in my experience this isn't bad at all, as the developer gets full control over user interactions.
As a result, debugging user actions becomes much easier, since the developer needs to debug only their own code and doesn't need to check whether the library has changed any states anywhere.

Convenience?

Some may not like the inconvenience of making special "observables" for each state they use in the template. It may be possible for me to remove the need to explicitly declare observables by using ES6 proxies. However, I am of the opinion that properties that change the UI should stand-out from other properties. i.e. the fact one has to do this.property(newValue) rather than this.property = newValue, makes it clear to the developer that property is special - updating it too many times could affect performance etc. The framework would be hiding that fact by using ES6 proxies.
Also note that enforcing some restriction on changing states isn't new in the framework world. Even newer frameworks like React enforces developers to use setState(), rather than changing the state directly. Angular has no such restriction though.

React users may also not like a templating syntax either. Having the render defined through JS seems to be more convenient for some.
This is certainly a debatable topic near React users, but in my experience the view only changes in few predictable ways. So a templating library, inspite of not allowing extremely arbitrary changes to markup, doesn't feel limiting at all for the use-cases we want to achieve in typical applications.
Also to my eyes, templates (written with templating libraries) are cleaner than most React render() methods I have seen.

Server-side rendering

LUI is able to run on node.js with help of jsdom. This is sufficient to run test suites. However LUI won't be efficient on serving 100s/1000s of requests with server-side rendered HTML, since creating DOM nodes is slow. Updates to already created DOM nodes using Observable()s is very fast (in fact almost as fast as compiled JS templates). However loops (i.e. foreach binding) and subsequent updates would cause several DOM nodes to be removed and added, making the approach slow.

If one only wan't to run KnockoutJS templates, then Htmlizer would help. Htmlizer is fast since it doesn't use jsdom. However Htmlizer cannot run LUI components and hence the application needs to be architectured in a way that can run with LUI and Htmlizer. In other words, running the client-side code "as-is" would be inefficient.
Therefore the current approach of LUI makes it only useful for SPAs that don't need server-side rendering. I am looking into reusing DOM nodes when using foreach binding, which may make it more efficient for server-side rendering.
Clearly React (and Ember?) is more fitting for server-side rendering.

Parting note

There is benefit in understanding how frameworks work internally. It helps us use them more effectively, and may also help us improve them.
And there is also benefit in identifying general patterns used along with frameworks. It can make us better programmers.

Updated on Oct 21, 2016
blog comments powered by Disqus