Avoid re-creation/evaluation of VDOM subtrees
I was reading over Cycle's implementation and noticed an unexpected behavior while trying out some of the examples: it seems that components are re-created any time their parent changes, causing significant portions of the reactive computation graph to be discarded and recreated more often than seems necessary. This is easier to explain by example.
Example: TodoMVC
- Open the Cycle todomvc example example app.
- Create two new todos.
- Breakpoint the
TodoItemcomponent definition. - Make any change to any todo, and observe that all TodoItem components are discarded and re-rendered from scratch.
Conceptually, after step 2 the graph of reactive values looks something like:
todo a -----\
todos ---> app
todo b -----/Based on my understanding of the documentation, if only todo "a" changes TodoItem would not be re-evaulated for todo "b", and that portion of the graph to be reused. In practice, this isn't the case and both components are recreated and re-rendered (to virtual dom). I also expected that adding a new todo would not cause todo's "a" and "b" to be created or even re-rendered, but they are.
Generalization
More generally, this problem would seem to occur in any Cycle app that follows the documented patterns for constructing applications. In the component documentation, for example, the BMI calculator's view function is as follows:
const vtree$ = Observable.combineLatest(sources.props$, value$,
(props, value) =>
div('.labeled-slider', [
// ... abbreviated
input('.slider', {
type: 'range', min: props.min, max: props.max, value
})
])
);Because input immediately invokes the function and creates the component, there doesn't seem to be any way for an application to reuse a previous input at the same point in the graph (short of some memoization on the side).
Questions
This seems like a potentially significant performance problem for applications. If TodoItem were instead a complex component - i.e. with a large number of (indirect) subcomponents - it would be expensive to re-construct frequently. Note that React solves this problem by using descriptors - the equivalent of the BMI input call in React would be to create a description of the input component and its arguments, which would allow React to reuse the existing component if it existed based on the DOM structure and key. Cycle DOM theoretically supports this same optimization - it's supported by the underlying virtual-dom diffing algorithm - but this logic exists only in the driver, which is abstracted from the view. It isn't immediately clear how a Cycle app could be changed to take full advantage of virtual-dom's component reuse in order to avoid recreating the components as React would.
Some questions:
- Is this an intentional design decision?
- What are the benefits of always recalculating the components?
- What is the suggested pattern for avoiding this type of reevaluation of components should it become a performance problem?
I don't mean to attack Cycle by posting this issue, and I should note that this same problem can occur in React applications as well. I once debugged an issue where a complex React component was constantly re-rendering, taking 100s of milliseconds each time, all because of a callback prop being reallocated by its parent on every render. Fixing this meant that the component's shouldComponentUpdate worked again and that rendering could be skipped unless it was strictly necessary. I bring this up to point out that performance problems can occur in any architecture, and I'm wondering what the equivalent of React's shouldComponentUpdate performance hook is in Cycle.
Source: cyclejs/cyclejs