vbarbarosh
vbarbarosh

Reputation: 3702

Is there a way in React to update a DOM without constructing Virtual Tree?

Imagine a list with 100 items. There is a component which displays all of them. Several times per second one (random) item should be moved into a new position. The React native approach would be to rerender whole component by constructing Virtual Tree, then diff it with previous copy, then to patch DOM. The problem is that making a Virtual Tree and creating a diff takes time (about 50ms in my case).

Is there a way in React to skip this creation of Virtual Tree and calculate diff? Like the following: shouldComponentUpdate will return false; then manually one Node will be removed from DOM, and inserted into another position.

Update

Concerning to this video there is a worst case scenario in React. When you update just one item in a 100 of them. The question is how to update DOM the fastest possible way (without diffing 100 items)?

This is the demo for the issue, and this is the code.

Needle in haystack problem

Upvotes: 0

Views: 675

Answers (3)

damianmr
damianmr

Reputation: 2531

An approach that I have used in one particular case (is not something that could be applied to every case) is to just set some position to every node and then sets its position with CSS.

Something like:

<div class="parent">
  <div data-position="3"></div>
  <div data-position="1"></div>
  <div data-position="2"></div>
</div>

Although the position attribute changes based on some sorting order, the DOM position of the node remains the same. The problem is that you need to set the visual position of every node with CSS like this:

.parent {position: relative}
.parent div {
  position: absolute;
  height: <something>px;
  width: 100%;
}

.parent div[data-position="1"] { top: 0; }
.parent div[data-position="2"] { top: <something>px; }
.parent div[data-position="3"] { top: <something * 2>px; }

This saves the browser from performing DOM recalculation, although it needs to recalculate the render tree. But if you change the position of elements several times per second, only one recalculation is made (because the browser will try to apply batch updates as long as you are not performing any re-layout).

The CSS 'top' property can be generated either by using a tool like Less/Sass, or with plain JS or can even be generated by the render method in your component, setting the style of each node on each pass: its is only having an pre-set height thats gets multiplied by the index you are iterating.

Sorry if this is not the kind of answer you were expecting, but I used this approach and worked fine for me for hundred of elements.

Upvotes: 1

Brigand
Brigand

Reputation: 86220

One technique is to not recreate nodes that already exist, i.e. caching. Here's a demo with 1,000 nodes and every second there's a random item removed from the array, and an item added at a random position. The update takes 10ms on my laptop with the development version of react.


I say this a lot, but I have a large monitor and I can only see 70 of these items at a time and it's too many. The best answer here is to not render 1,000 items from a performance and ux perspective.


Say you have an array of objects with an id property, and some other stuff.

class Foo {
  constructor(){
    this._nodeCache = {};
  }

  renderItem(item){
    return <div key={item.id}>{item.text}</div>;
  }

  renderOrGetFromCache(id, item){
    if (this._nodeCache[id]) {
      return this._nodeCache[id];
    }
    this._nodeCache[id] = this.renderItem(item);
    return this._nodeCache[id];
  }

  render(){
    return (
      <div>
          {this.props.items.map((item) => this.renderOrGetFromCache(item.id, item))}
      </div>
    );
  }
}

So how does this differ from what you were previously doing?

Before the reconciliation looked like:

  • render()
    • create a bunch of objects
  • for each child
    • compare the key and component type
    • diff the attributes
    • recurse into children
  • perform updates

Now it looks like:

  • render()
    • create objects we haven't used before
  • for each child
    • compare the key and component type
    • is this the same virtual node (=== check)?
      • if so, we're done
      • otherwise normal reconciliation from above

You're still constructing the virtual dom, but we're talking about fractions of a millisecond to render(), and a very fast reconciliation, without even a shallow equals.


One more thing to note is the signature of this function:

renderOrGetFromCache(id, item)

This first parameter takes the place of our shouldComponentUpdate, meaning it needs to uniquely identify a value. If you happen to change the .text property of the object, but the id is the same, it won't update. To fix this, you can change what you pass to renderOrGetFromCache. This way, it'll do the normal diffing of the item rendered because the key is the same, but all other nodes will still be an === check.


The main trade off here is you need to handle clean up manually. As is, this code will leak memory. If an item is no longer rendered, or has changed, it needs to be removed from the _nodeCache. How you do this depends on how your data changes over time, and because this is a performance question, there's no universal 'best' answer.

Upvotes: 0

andykenward
andykenward

Reputation: 954

You would have to just access the DOM in componentDidMount.

But as you are having performance issues. I would try making your children render function "pure". You can use the PureRenderMixin , but as mixins may not be staying around you can

import shallowEqual from 'react/lib/shallowEqual';

then in your shouldComponentUpdate function do

shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }

Also look into using Immutable-JS for holding your 100 items as a Map as it will minimise the need to copy or cache your data.

Upvotes: 1

Related Questions