The Elephant in the Bedroom

A net cost analysis of React awesomeness (or any “UI as a function of state” for that matter).

François De Serres
PALOIT
6 min readJun 18, 2018

--

UI is not a function of state. Maybe it is so within the scope of micro-components, but not at the application level.

Virtual DOMs provide virtual UI components. They are invisible. When the internal state of a component changes, it reacts. A component reacts by applying changes to its actual representation (the visible stuff, eg. the real DOM). These changes come in three flavours: mount (appear), render (update), unmount (disappear). The one cool thing about virtual DOM components is that I only need to define how they mount, and the VDOM logic will take care of unmount and render.

Wait, you mean “you only need to define render” don’t you?

Nope, and thanks. Updating the DOM according to arbitrary state changes is really tough to get right. Therefore, I’m going to define how my UI component appears, and the VDOM will be smart enough to figure out how to make it disappear and, even better, how to update the native DOM with an optimal changeset. Now my code looks (and behaves) as if the UI was unmounted-mounted again for every update. And that is the genuine optimisation: straightforward UI code. Forget about the fallacious “<hyped-VDOM> renders much faster than <previously-hyped-DOM-stack>”. A VDOM is just slightly smarter than most of us in the way it applies changes to the DOM. Yet, it needs to figure out the ideal changeset on behalf of the lazy bastard.

There’s enough literature available about VDOMs diffing algorithms, suffice to say the changeset doesn’t come for free. First, in order to do a useful comparison you ought to have two distinct things, so you’ll need two buffers. Next, you want to be able to relate items in the new version to those from the old version, hopefully finding genuine changes versus mere moves. Third, you can’t do much with just the differences, you need to come up with a set of instructions to transform the first thing into the second (an editscript), which you’ll want to transpile into a batch of optimal, ordered DOM instructions. Our browser’s JS engine revved up well before anything is actually hitting the DOM.

Thank heavens, some brilliant bloke discovered that if a new virtual component is equivalent to an old one, diffing them yields an empty editscript. How do you know that two UI components are equivalent? “Um… diff them?” says Bozo. “Trivial!”, shouts Smarter, “if render is a pure function of state, and new state is equivalent to old state, then rendering new state is equivalent to rendering old state, therefore diffing their results gives an empty editscript, hence if I can prove that old state and new state are equivalent I don’t even need to bother diffing, I’m done.” And there we are, optimising.

We’re lucky, though, our beloved programming language was born a LISP, we have closures so we can memoi… wait, it’s OOP time, let’s write a class inherited from Component that stores its last rendered state, and override its SCU method so render is only called if new props lead to a state that’s not equivalent to the old one. Easy. Just have to figure out what to bind where to this, and what to memorise in which local state, so it’s all instance data that I can easily access and compare. Wait, easily compare? Checking for equivalence between JS objects is not a given! Shallow compare should do, yeah? Mostly. Ok got it somehow, now it’s telling me something changed: let’s render! Oh, child components also need to run maaaany fetches and comparisons, down until they nail what changed for them… maybe I should factor? Um… the resulting code is slimmer, but the very same comparisons are still running all over my components SCU methods. Every time the root state changes, a huge batch of comparisons happen, explicitly or not: if I’m using some smart observable store enhancement, I won’t code that, but it’s still happening.

Wait, I heard about those immutable data structure things, they provide damn fast (and reliable) equality checks. Let’s optimise… just need to convert back and forth between objects and immutable maps. Few lines of code, loads of CPU and RAM, who cares. Unfortunately, most of the props objects I pass to my components are actually derived values, meaning they are not directly found in (immutable, easily compared) root state, but rather calculated from it. Bye bye quick pointer comparisons.

“Hey”, says Smarter, “that’s what Smart Components(tm) are for”. They do the heavy lifting (no, Lifting the State(tm) is something else) so my Dumb Components(tm) can simply render whenever the Smart guy upstairs says to do so. Now my optimisations can all go in distinct places: selectors in Smart components. Fantastically, they’ll trigger Dumb rendering with a new set of props, only when their own input state changes. A bit like memoized functions, ya know. Yes, they still need to fetch their input from root state and do the derivation jobs, like… compute the very same stuff on every single change in root state.

Credit: https://simpleprogrammer.com/dumb-components-smart-refactoring/

At the end of the day we have very much impr… moved all of our diffing logic: instead of efficiently comparing virtual components at one point, in one place, we’re now comparing gathered/derived bits of new versus old state, mostly by hand, and all over the place. Each UI component is now merely a function of its own, internal state. Big deal.

If we mean the entire UI, and the complete app state, then UI is not a function of state, else we’d be actually redrawing the whole screen on each keystroke. Either:
1) UI is the delta computed from the result of applying a function to a derived state, and its previous return value, or
2) UI is a function of the many deltas computed between two derived states.

Im my experience, and with my opinionated view on performance, solution 1 beats solution 2: simplicity and clarity always win in my book. I’m more of a Bozo, maybe, but it looks to me like solution 2 consumes at least as much resources and time as #1: the diffing ought to happen somewhere, there’s no way around it. Here’s a net cost calculation: your app has to package much more logic with solution 2, written by you or not. Whichever renders faster is highly subjected to context and appreciation, but recall that it’s not enough to know something has changed, or even to know what. At the end of the day, we want an optimal editscript for a DOM tree, we can’t do without the VDOM, so it’s an easy choice: throw away your state diffing.

Now, if we had an editscript that transforms our old state to new state, then we could directly transpile it into a DOM editscript. The world could probably do without the dissipated heat of those gazillions diffs per minute?

I’m not suggesting that we all should switch our stack for a diffing VSTATE of some sort, and I do love React. But if you’re building complex, rich web applications (like we do at PALO IT), you probably already put a significant effort in managing your client side state in a transactional fashion. What would it take to be able to spit an editscript of some sort for every transaction dispatched to your store? The store we use on one of our flagship projects does it for free, it’s called DataScript, and was inspired by Datomic. It gives you a fine grained set of adds and retracts that your transaction generated, and you can actually query this set as if it were a database, including joins between the set of changes and the original data. Check it out, it’s solid stuff!

Thanks for reading, I hope you’ll be spending less time moving diffs around, so you can spare time to try a DataLog backed store.

About the author

Francois De Serres is the Head of Digital Technology at PALO IT Singapore. Passionate about new technologies, and people, François has recruited and guided large development teams. He has also set up and led complex IT programs using both waterfall and agile, and constantly succeeds in helping stakeholders achieve their objectives for more than 20 years.

--

--