The best way to work with the DOM

While I was writing on Solid , I had the opportunity to estimate the number of attempts to optimize performance on graphs (benchmarks).

DOM is a front-end bottleneck development. Different solutions can lead to similar results. New libraries appear every week, incorporating the best of the previous ones, to make the perfect combination. After some time, my eyes begin to run away from such a variety of solutions.

Do not misunderstand me. I love to see new ideas come to life, but they all have flaws, and sometimes entail a loss in convenience or productivity. I understand the consequences of such a decision, but I want to share my observations.

What is the fastest way to work with the DOM:

  • Virtual dom
  • Tagged Template Literals
  • Fine-grained observables

Comparison


JS Frameworks Benchmark is the best open source project for comparing the performance of JavaScript UI frameworks. It is better to run tests locally instead of using official results. Results may vary by machine. Since I'm testing on a weak machine, performance will noticeably drop.

I will take the best approaches in rendering a DOM tree to illustrate the redundancy of certain solutions. I will do this using the ability of Solid to support different rendering options in order to impose costs as the variables change and compare this with similar results from other frameworks. Let's take a look at them.

Varieties of Solid:

  • solid - version of the framework with ES2015 proxy setter on top of change-tracking functions built into cloned DOM node templates. This is achieved by precompiling JSX templates (Code)
  • solid-signals - this version is the same as the previous one, but raw Signals are used instead of proxies. This complicates the use of the library, but in the end we get a smaller bundle and better performance (Code)
  • solid-lit - this version does not use JSX precompilation in runtime (Code)
  • solid-h - This version uses HyperScript to create `document.createElement` on the fly. The rest uses the same implementation as Solid (Code)

Other libraries:

  • domc β€” -, DOM DSL (domain specific language) HTML, index.html (Code)
  • surplus β€” JSX `document.createElement`. Solid (Code)
  • ivi β€” Inferno, DOM. HyperScript Helpers-esque (Code)
  • lit-html β€” , c . Tagged Template Literals DOM- (Code)
  • inferno is the fastest of React clones and one of the fastest Virtual DOM libraries. Uses special JSX directives for best performance (Code)

Some of your favorite frameworks may not be here, but this list displays optimized versions of all the techniques that you will see in the most popular frameworks. You can look at it as an indicator to evaluate the maximum capabilities of your library. If you are interested in comparing the performance of popular frameworks, I recommend this chart.

For comparison, I would like to add Web Assembly. Unfortunately, at the time of this writing, WASM records were a vanilla implementation without high-level abstractions. (Later they added wasm-bindgen to the framework - approx. Translator)

results


HyperScript (inferno, ivi, solid-h)


HyperScript displays markup as a composition of functions (like h or React.createElement). For instance:

h('div', {id: 'my-element'}, [
  h('span', 'Hello'),
  h('span', 'John')
])

Virtual DOM frameworks have this property. Even if they use JSX or other DSL template engines - under the hood they are still converted to element-by-element render methods. This is used to construct a virtual DOM tree for each render cycle. But, as shown here, the rendered functions can be used to create a reactive dependency graph, as in the case of Solid.



As you can see, libraries with a virtual DOM are much faster. Solid loses in performance due to excessive creation of a reactive graph. Notice the difference in benchmarks # 1, # 2, # 7, # 8, # 9.



The memory is less convincing. Inferno and this version of Solid show roughly the same result. While ivi uses more memory.
, Solid, , VDOM. Solid , DOM, DOM. Solid JSX DOM . , . Solid fine grained evaluation . - .
β€”
Reactive update tracking frameworks show better results when updating rows. This graph would explain the popularity of VDOM in recent years. Suffice it to say that if you use HyperScript with this update, then you better switch to Virtual DOM.

String templates (domc, lit-html, solid-lit)


Each library here has something in common. They are rendered based on cloning element templates, run in runtime and do not use VDOM. But they still have differences. DomC and lit-html use top-down diffing similar to Virtual DOM, while Solid uses a reactive graph. Lit-html splits templates into parts. DomC and Solid compiles the template into separate paths in runtime and updates them.



This category has the widest range of performance. DomC is the fastest, and lit-html is the slowest. Solid Lit is in the middle. DomC demonstrates that simplicity of code leads to the best performance.

DomC sags only in section # 4 because it calculates the difference of nodes, which becomes more complicated as the depth increases. It is pretty fast, but you need to validate the results on big data.

Solid Lit is more productive than Solid HyperScript. Instant compilation in runtime removes the drawbacks of creating a reactive graph, allowing the framework to catch up with ivi, the fastest VDOM library (see the full table at the end of the article).



DomC showed good results in memory consumption. This happened due to cloning of the template elements. It is noteworthy that code generation in runtime can have minimal performance overhead compared to compilation at the build stage. Perhaps this is an unfair comparison for lit-html because the framework does not use this technique. But it's fair to say that lit-html or similar libraries like hyperHTML or lighterHTML are not the best way to implement Tagged Template Literals. And you can achieve good results even in runtime without VDOM.

Precompiled JSX (solid, solid-signals, surplus)


These libraries use JSX, which compiles into a DOM or reactive graph at the build stage. Templates can be anything, but JSX provides a clean syntax tree that enhances the developer experience.



This group has similar results, but the difference is very important. All three use the same library to manage S.js state . Using the Solid Signals example, you can see that tracking functions with cloning of template elements give greater performance. The standard implementation of Solid is overloaded using ES2015 Proxies, which worsens the result on all graphs. Surplus uses `document.createElement`, which degrades performance on tests where lines # 1, # 2, # 7, # 8 are created.



Memory consumption has similar results. In this case, proxies add more complexity than cloning template elements.

The conclusion here is that proxies degrade performance and more libraries should clone templates. On the other hand, you can consider a small loss in performance due to proxies as an investment. The Solid example has the smallest amount of code among other examples - only 66 lines, it has 13% less non-whitespace than Svelte - a library that prides itself on its minimalism.

Best in class (domc, ivi, solid-signals, vanillajs)


Now let's take the winners in each category and compare them with the brutal, effective, hand-written example in vanilla JavaScript. Each implementation represents one of the popular state tracking solutions. You can even draw an analogy between these libraries and the Big Three: Solid β†’ Vue, DomC β†’ Angular, ivi β†’ React. You will get this result if you remove everything superfluous, except for the render, and get rid of the 60-200kb code.



DomC and Solid are close in terms of performance, ivi is significantly behind, but DomC, on the whole, is faster. Its complexity compared to vanillaJS is noticeably less, but it is less effective with partial updates. Only this criterion is not indicative. Anyone who thinks that VDOM is slow or has unnecessary complications should check this out on their own.

Most libraries will never have this kind of performance.



DomC is also leading in the graph with memory. Fine-Grained Solid outperforms VDOM ivi in ​​terms of memory consumption.

Interestingly, these libraries are not much worse than vanillaJS, regardless of method. They are all very fast.

Bundle size


Finally, I would like to touch on the size of the bundle. Many real tests focus only on these metrics. Yes, the size of the bundle is important and has a direct correlation with performance, but what's the difference? I suspect code complexity is more important than size.



Conclusion


As usual, the results in such graphs are never completely convincing. The process itself and the conclusions we make are important. In this case, we see that the DOM itself is a big bottleneck when it comes to performance. So much so that there is no unambiguous technique for circumventing it.


Christoper Lambert as The Highlander

No, it's not that simple. Neither the DOM nor the VDOM is slow. But I believe that they are worth each other. I admit that the rhetoric of the VDOM Performance React led me to these thoughts. The ignorance of opinions around this topic is enraging.

The claim that VDOM is slow is due to poor awareness. Rendering VDOM and calculating the difference in state is a complication compared to not doing it. But is its absence scalable? And how to make data changes?

I see that there is an exception in every rule. In general, precompilation combined with Fine-Grained in reactive frameworks is the fastest solution. But DomC shows high performance without it. Native JS methods, such as cloning template elements with Tagged Template Literals, may be the best solution for implementing lit-html from large corporations (Google). But this is one of the slowest frameworks in this table and not even the best implementation of these technologies. Svelte is considered to be the fastest library in the community, but it could not even compete closely with the presented solutions.

If reactive programming wins, this does not mean that all reactive libraries are fast or that metrics mean everything. Despite the deep comparison in this article, I think that in reality there are fast libraries and slower ones. Even if we find super technology, we will still face its limitations.

Test results of all libraries in one table:


All Articles