Avoid re-creation/evaluation of VDOM subtrees
See original GitHub issueI 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
TodoItem
component 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.
Issue Analytics
- State:
- Created 8 years ago
- Comments:85 (52 by maintainers)
Top GitHub Comments
@niieani For a more in depth explaination you can take a look at this: https://www.youtube.com/watch?v=efv0SQNde5Q
A lens is basically a path to a nested property of an object. Say you have an object:
{root: { children: [ {attrA: "bar", attrB: "foo"} ] }}
Now you could read theattrA
value via:const myVal = yourObject.root.children[0].attrA
But now if the object get’s changed your myVal will not update. If instead you just read the childconst child = yourObject.root.children[0]
you have a reference to the child object so if theattrA
get’s changed your object will have the new attrA value as well. But if somebody replaces the whole child you are still out of luck.Now a lens represents that path
yourObject.root.children[0].attrA
without actually reading the value so you can read the value whenever you need it an get the most recent one. eg:const myLense = lense("yourObject.root.children[0].attrA", yourObject)
and then whenever you callmyLense.get()
you will get the correct value no matter howyourObject
has changed. Same works for writing: you can callmyLense.set("barbar")
and the object will be updated correctly.Now the real advantage is that you can pass somebody a lens without telling where it points. Eg I can pass you a
myCoolLense
object an you can read it and write to it without know what will actually happen. It could be referring to just one value at the root of my object or it could be referring to a value very deep nested inside my object hierarchy. All I tell you is that it’s a lens containing a string. And you can construct lenses out of lenses. So If I construct a lens pointing to a user object inside my storage you can take that lens, knowing it “contains” a user and build a new lens that only point’s to the user’s name and pass that new lens on to somebody else - just telling her that it’s the user’s name. She can now go andset
that’s lens’ value. That change will propagate back to you user object and to my storage.Now if you combine that with observables: Applying a lens to an observable containing an object will give you can observable that contains only the object’s value the lens refers to.
@Staltz Never mind “unsafe” and “inconvenient” it’s retarded. Cyclist want to work with real data not cheap low end versions of stores. Come on already, get with the times here! Lets start considering the bigger picture which is comprised of serious data backends for today’s and tomorrows awesome apps.
There must be good input and output for such data including streaming and also robust in client access, transmission and integration. The growth of Cycle will simply stay at a snails pace until data and state flow freely within Cycle. Not having this effects the construction of components and wiring them up in every way, how is that for a side-effect of blocking progress!
Maybe your just not aware of how your tunnel vision does not line up with the very real importance of data? At one point you said you were not a backend guy. Anyways, on every point where it concerns integrating Cycle apps together with data your thinking far to small for the real enterprise world. Will Cycle always be a toy?