React: React Virtual DOM performance

Created on 1 Oct 2015  Â·  12Comments  Â·  Source: facebook/react

I've found a website comparing the performance of the Virtual DOM implementations in different libraries like uix, cito.js, bobril, deku and others.

Here's a screenshot with the benchmarks for these libraries (the lower the score, the better the performance):
vdom-benchmarks

Seems that React has the highest score (lowest performance) and InfernoJS has the lowest score (highest performance).

The benchmarks can be run here http://vdom-benchmark.github.io/vdom-benchmark/

Is there a way for React to use InfernoJS' vDOM implementation and increase its performance?

Most helpful comment

This benchmark is a tool for library authors. If you don't know about internals of different implementation, there are zero value in this numbers. Especially overall time, I really don't like "overall time", but some library authors are using it, so they can easily track regressions.
And this benchmark is testing only how fast is children reconciliation algorithm with simple nodes (without any attributes, styles or events). There are also some problems with this benchmark, for example, dom operations on dirty nodes are way much faster in Chrome, if we add raf between iterations, the difference between libraries will be way much smaller, because dom ops will be significantly slower ( http://jsfiddle.net/67jz79n2/3/ ).
Also, InfernoJS is using caching, so it creates html nodes faster than it is possible with simple for loop, I am not sure that this forms of caching is good for real use cases, but it will certainly win all microbenchmarks.

From my experience, React performance is good enough for almost all use cases in dynamic SPA.

p.s. I am the author of this benchmark.

All 12 comments

This benchmark is a tool for library authors. If you don't know about internals of different implementation, there are zero value in this numbers. Especially overall time, I really don't like "overall time", but some library authors are using it, so they can easily track regressions.
And this benchmark is testing only how fast is children reconciliation algorithm with simple nodes (without any attributes, styles or events). There are also some problems with this benchmark, for example, dom operations on dirty nodes are way much faster in Chrome, if we add raf between iterations, the difference between libraries will be way much smaller, because dom ops will be significantly slower ( http://jsfiddle.net/67jz79n2/3/ ).
Also, InfernoJS is using caching, so it creates html nodes faster than it is possible with simple for loop, I am not sure that this forms of caching is good for real use cases, but it will certainly win all microbenchmarks.

From my experience, React performance is good enough for almost all use cases in dynamic SPA.

p.s. I am the author of this benchmark.

@cosminnicula React's virtual dom diffing does things like guaranteeing that if a node with a particular type-keyPath-pair appears in both the first and second render, the DOM nodes will be reused (thus preserving imperatively added children, etc). This is a serious expense that inferno doesn't incur because they are using simple templating and can just throw away nodes if it's too inconvenient/expensive to calculate an absolutely minimal diff. This is just one of many key differences.

One might argue that the performance differences exceed the value of the additional features in React, but that's a different argument. It's hard to know one way or the other. In practice, React is pretty fast for most real-world applications, and we think the features we support are valuable.

This is why we only use perf tests for regression testing, and not to compare frameworks. I have a post here in https://github.com/facebook/react/issues/4974#issuecomment-143924915 which touches on the topic in detail.

Myth busted :) Thanks!

This is a serious expense that inferno doesn't incur because they are using simple templating and can just throw away nodes if it's too inconvenient/expensive to calculate an absolutely minimal diff. This is just one of many key differences.

Can you explain what you mean by this? Inferno shouldn't be throwing away any keyed nodes. The reason why Inferno is so fast is because it leverages on the concept of virtual fragments (chunks of virtual elements) and value diffing, rather than vdom diffing. This also allows Inferno to recycle DOM nodes (it doesn't pre-cache nodes or do anything fancy), as they're associated to virtual fragments. Inferno is still under development, but it definitely doesn't throw away nodes in cases that I know of.

Recycled DOM and cloning nodes is not new, many other frameworks/libraries use this concept extensively to improve performance (Angular 2 does for example).

Note: I'm the author of Inferno.

@trueadm Here is what I mean:

Inferno.render(
  t7`<div><div id='widget' /><div>foo</div></div>`,
  document.getElementById("app")
)

document.getElementById('widget').innerHTML = "this should be retained";

Inferno.render(
  t7`<div><div id='widget' /><div>bar</div></div>`,
  document.getElementById("app")
)

As of today, Inferno does not retain the widget. Retaining the widget is important because it allows people to mount non-react stuff into a react render tree, which is critical for interoperability with other libraries. But even if that were fixed, it's not the only difference.

What you've done is very cool and I encourage you to pursue those ideas (or better yet, please do contribute them to the React core!), but it's certainly not a drop-in replacement.

leverages on the concept of virtual fragments (chunks of virtual elements) and value diffing, rather than vdom diffing

@trueadm I don't know what this means. Can you help me understand?

@spicyj He is hoisting the jsx up out of the render function, and using triple equals equality on the elements rather than walking the vdom. Sort of similar to Ember's glimmer stuff.

@spicyj not really about hoisting JSX or diffing JSX (although similar to Glimmer). In Inferno, pseudo JSX (t7 in this case, although I'm changing to an Open JSX form in the near future) of something like this:

<div><span>{ value }</span></div>

Gets parsed in precompile tile into a fragment, which looks like this:

let fragment = {
  dom: null,
  key: null,
  templateValue: value,
  templateType: Enum.TEXT,
  templateElement: Span Node,
  template: someFunction
}

This fragment is the only thing that is "diffed" on each render(). It simply checks through the templateValue (or templateValues if there are more than one) where each value represents an expression in the original JSX-like input. If there is a difference at === between expressions, it then does an update on that value depending on the Enum type.

You're probably wondering what fills in the templateType and templateElement. Well on the "first" render, each fragment gets constructed from its template function. The template function is also generated in advance/pre-compile time and is basically a vdom representation of how that fragment may look and fills in a templateType for each expression in the templateValue that is passed. This way, it can easily determine how to update the DOM in the most efficient manner possible.

If a fragment is updated with a fragment that has a different template function, the first fragment is either recycled or replaced. If the fragments have keys, they are handled and sorted just like a vdom implementation would work.

Essentially each t7 call is a fragment, and the same would apply for JSX, where the root return operation is the root of the fragment.

The key is in the JSX/t7 implementation on how it translates something like <div><Component /></div> into something operations that are far more optimal for an imperative DOM API without making the experience hard to debug and by making the developer API/sugar syntax nice and a pleasure to use (thus the JSX direction). There's also an alternative declarative/functional way of creating fragments and templates. If you're interested in seeing some of the later code and seeing how some ideas might feed back into React – I would recommend checking out this branch: https://github.com/trueadm/inferno/tree/new-build

In React,

<div><span>{value}</span></div>

becomes essentially

{
  type: 'div',
  props: {
    children: {type: 'span', props: {children: value}}
  }
}

and React compares these objects. Isn't that the same thing?

Somewhat similar, but very different approach.

Given you have a somewhat large vdom tree, you'll be creating an awful lot of objects to simply diff and chuck away. Plus you have to traverse an non-optimal object.

I've built Inferno to basically take advantage of monomorphism and improve V8 performance by simply creating a single object that might represents tens of hundreds of vdom objects. The big difference is that I create these objects once via the template and I only compare the expressions that were passed into the fragment. You gain performance benefits in React by hoisting the elements, but the are also downsides to that approach too (I'd be happy to take into Slack or somewhere to better discuss this all in depth if you'd like).

My intention was to make Inferno compatible and familiar to people using React, thus the API and lifecycle events – I just took an approach that was tailored towards performance on the DOM (something that I've got a keen internet in for mobile web technologies). So, this is no dig at your work with React, If you guys hadn't come out with React and JSX, I doubt there'd be anywhere near as much interest in this stuff. So kudos to you there :)

Got it. Thank you.

The example provided by @jimfb (https://github.com/facebook/react/issues/5024#issuecomment-144770135), is that accurate? Does it stand in the current version of Inferno?

Was this page helpful?
0 / 5 - 0 ratings