question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Avoid re-creation/evaluation of VDOM subtrees

See original GitHub issue

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

  1. Open the Cycle todomvc example example app.
  2. Create two new todos.
  3. Breakpoint the TodoItem component definition.
  4. 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:open
  • Created 8 years ago
  • Comments:85 (52 by maintainers)

github_iconTop GitHub Comments

11reactions
laszlokortecommented, Mar 22, 2016

@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 the attrA 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 child const child = yourObject.root.children[0] you have a reference to the child object so if the attrA 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 call myLense.get() you will get the correct value no matter how yourObject has changed. Same works for writing: you can call myLense.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 and set 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.

8reactions
HighOnDrivecommented, Mar 24, 2016

@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?

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found