This is probably the last post about these improvements. At the last post, I showed how I threw every performance optimization I tried away (combine-leafs, other stuff, etc) just to throw it all away because React didn’t like what I did.
So, after a lot of time, I decided to do the horrible, insane thing that I did a lot more than I wanted – rewrite the rendering process of Chlorine to not use Reagent. The reason was actually quite simple: I wanted things to be simpler to implement, and honestly, Reagent was getting in my way. Like, A LOT.
Before I continue explaining the performance improvements that I am doing, I have to say something – in the first version of the rendering code (the one that I was trying to optimize at my last posts) I had a huge beginner’s luck – the actual rendering time is amazing for most cases, and while it is slow for some very specific situations, it’s actually not that bad; but there were also some weird trade-offs that I had to do and I cannot use them anymore.
The reason being, Chlorine started to become very strong in the customization code and “interactive renderers”. If you’re not aware, you can configure Chlorine to add custom commands and other amazing stuff, just by opening up a config file, writing some ClojureScript code, and you’re good to go. Unfortunately, also at the first versions of Chlorine, sometimes you would evaluate a code – let’s say a list of a thousand elements – and you get back only 10 or 20 elements. Worse yet, you would not get back a “List”, but an object called an “Incomplete List”. Which was not ideal, and basically made the whole “customize your editor” difficult to say the least.
Reagent
And then there’s Reagent. Reagent can use a specific type of atom
to render stuff but it’s slow. To optimize, you want to use what they call a cursor
– but cursors doesn’t work with lists or tagged the literals and other stuff. To fix this, I implemented a “custom cursor” in my performance improvements-and let me tell you, it’s horrible. I basically had a lot of intermediate, custom states that I had to be aware, and I could not “deref” them otherwise it would trigger a re-rendering of the parent; so, it basically complicates all code that have to handle state, and to make things worse, it meant that changing something that was wrong or that was buggy in the rendering process was a nightmare.
And when I had to worry about HTML stuff being rendered on top of React, and all the complications that happen by trying to do so. But the worst part of all this is that, even with all this complicated code, fine-crafted stuff, performance tricks, and code complexity I was still seeing the UI being rendered in the wrong way – either by updating more stuff than I wanted, or replacing parents with children meaning that I was losing UI fragments altogether… so I decided to give up React and Reagent, and do things as “vanilla” as possible, manipulating HTML Elements by hand.
DOM API
So what I did was simple – to avoid having to handle DOM elements, I implemented a “Hiccup to DOM” (not to HTML string – to DOM. For example, [:div]
is the same as document.createElement('div')
). This code was basically very small, it doesn’t handle all the edge cases (but again, Reagent also doesn’t). But the good part is that I could control the issues that could happen, capture exceptions, render errors correctly, and so on. Obviously I lose some of the Reagent’s power, but that might not be such a big issue considering that I’m still using Hiccup elements, and the code was actually simpler and more predictable (meaning – if I want to change some specific part of the UI, I can just… change the specific part. I don’t actually need to create cursors and keep track of when they are deref
erred.
So what about performance? Well… not that amazing, to be honest. The first version of Chlorine, with the old Regent code, took 620ms of script with 850ms of actual rendering. The new code uses 645ms of script, with 800ms of rendering – not a big improvement, but that’s just the first render. The first “click”, to open stuff, is taking 240ms/345ms (script/rendering) on the new code, and 330ms/570ms at the old one – about 20% better. But where it actually shines is at the third click, when I open an inner key – the new code takes 17ms/91ms, where the old one took 110ms/120ms!
As you can see, at the last example the actual performance hit was on “scripting” code – and that is true. Rendering is not that different – what the new code renders at the screen is basically the same as the old code, but to calculate that style I had to create intermediate states, render them, and then update these intermediate states so that it would recalculate the whole parent and then repaint at the screen just what changed. In the new code… I just create the “child” (or the “open” element, if you want) and .appendChild
the new element.
And the best part? It just does that for the first interaction! To make it work, I basically did a (delay (...))
, meaning that the first time I need some element, it’ll generate the DOM and the second, third, fourth time, it’ll just use whatever already exists! For example, the second example (240ms/345ms), when I click a second time to close and open the elements, the new “open” takes just 6ms/345ms (unfortunately, the “rendering” time doesn’t change – Pulsar, or the browser, still needs to paint things the same as before).
Now, we’re already at the point where we’re faster than the old code, less complicated, and we’re not using Reagent or React. So, what more can be done? Glad you asked.
Back to the beginnings
Combining similar elements, the thing I did at the first implementation, didn’t work because React don’t like me messing up with the elements. Basically, for React (and Reagent) a function is an element, so when I did something like [Text something...]
, that Text
was the “fragment” that would be recalculated, and repainted, when the state changed. Unfortunately, that also meant that I could not do things like (let [element (Text something...)
and then mess up with element
, combining stuff, etc, because that would “demote” Text
and make the whole parent the “fragment” to be recalculated.
But now, I’m not using Reagent anymore! Which means, these limitations don’t exist anymore. I tried a naïve first implementation of node combination (basically, combining every element that renders stuff in a column into a single element) and while it didn’t cut the script time, it did cut the rendering time – instead of the 800ms, it took 600ms (which makes sense – rendering divs over divs over divs is slower than rendering a single div). I could make it even lower, but there are other techniques I could try – for example, to render sub-elements after some timeout of, let’s say, 100ms (so that the UI won’t freeze), or “cut” rendering after some levels (for example, render just the first 50 elements, and as far as 3 levels deep).
These techniques could work on Reagent too, but again – it’s hard to get it right, because of cursors and sub-states and other stuff. Basically, to render things lazily, or in background, either we need support from React (and then we have to conform to React’s rules) or we need to very carefully craft our Reagent code around states in a way that makes it lazy, but don’t cause re-renders or infinite loops (harder than it seems, especially because sometimes, infinite loops are not that visible: for the UI, things are the same – an element on the screen – and there’s nothing visually wrong, except that the editor’s process is consuming 100% of my CPU).
Another approach that could work is to use clojure.walk
‘s functions and make some code to consume my hiccup thing, and recursively “flatten” it to a single div
if possible. That could cut rendering time a lot but it might complicate too much the code and honestly, the techniques to cut rendering after some levels, or move things to background will work better (because they’ll also cut the script time which is something that I can’t escape with any combination techniques).
So, finally, to wrap it up – these techniques work in ClojureScript because we use hiccup – a data structure – to represent HTML. They won’t work in pure Javascript because, for some reason, the language decided to invent new syntax (JSX) instead of using what the language offers you.
This is the power of “data first” languages. And I hope other languages can learn from it.