reactjs / react-future

Specs & docs for potential future and experimental React APIs and JavaScript syntax.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support memoization of component functions during render phase

slorber opened this issue · comments

var Hello = React.createClass({
    getExpensiveNumber: function() {
       return ...; // expensive value computed from props and state
    },
    computeSomethingBasedOnExpensiveValue: function() {
      return this.getExpensiveNumber() + 1;
    },

    render: function() {
        return <div>Number is {this.getExpensiveNumber()}, next number is {this.computeSomethingBasedOnExpensiveValue()}</div>;
    }
});

In the following example, we can see this.getExpensiveNumber() is called twice during the render phase, which means the expensive code runs twice.

Current solutions for this execution cost problem are:

  • Compute the expensive value only once at the beginning of the render method, and pass it to all rendering functions that needs it. In complex components this can lead to boilerplate and more complex method signatures.
  • Perform the expensive computation in state, but this is not advised by React documentation as it duplicates the source of truth (and also introduce boilerplate).

Since React's render method is supposed to not have any side effect, and during the render phase neither props and state are supposed to change, another possibility is to be able to memoize the expensive render functions. During a single render phase, they are supposed to always return the exact same value (for primitives at least, it could be another object's identity...).

We could probably have a syntax like this one:

var Hello = React.createClass({
    getExpensiveNumber: React.renderMemoized(function() {
       return ...; // expensive value computed from props and state
    }),
    computeSomethingBasedOnExpensiveValue: function() {
      return this.getExpensiveNumber() + 1;
    },

    render: function() {
        return <div>Number is {this.getExpensiveNumber()}, next number is {this.computeSomethingBasedOnExpensiveValue()}</div>;
    }
});

And this will memoize the function per render phase, meaning the expensive call will only run once per render instead of twice.

Just use pure functions and composition:

var Hello = React.createClass({
    getExpensiveNumber: function() {
       return ...; // expensive value computed from props and state
    },
    computeSomethingBasedOnExpensiveValue: function(expensiveNumber) {
      return expensiveNumber + 1;
    },

    render: function() {
        var expensiveNumber = this.getExpensiveNumber();
        return <div>Number is {expensiveNumber}, next number is {this.computeSomethingBasedOnExpensiveValue(expensiveNumber)}</div>;
    }
});

@nfroidure this is exactly the same as what I described here:

Compute the expensive value only once at the beginning of the render method, and pass it to all rendering functions that needs it. In complex components this can lead to boilerplate and more complex method signatures.

As you can see, the code you provide requires more complex method signatures. On real production components this can lead to much more complex code, so I generally avoid these optimizations as they tend to make the code more complex than it needs to compared to the performance gain it involves.

Imo, that's not a bad thing. Using a context object or ES6 destructuring can avoid complex functions signatures.

Also, adding memoization features would make React slower for everyone and encourage bad design patterns while well known and performant ones already resolve the case you pointed out.

@nfroidure sorry but I can't understand your point about using context object (you mean the "hidden" context feature of React?) nor about using ES6 destructuring.

Also, yes it would have a very small overhead for anyone not using this feature, but help people using it to have better performances without introducing more complexity. With the same reasonment you could argue that shouldComponentUpdate is not worthy.

I would really like to know where you see a bad design pattern. Memoization is not at all a bad design pattern when you evolve in functional purity. React somehow also uses it because it memoizes the value returned by render according to props and state. What I suggest is just to permit local memoization of intermediate computations during that render function.

Please provide examples of the well known and performant patterns to solve this problem, because the code you provided introduce boilerplate and does not seem to use any of these patterns. And as far as I know memoization is a performant and well known pattern :)

I think, it is, until you wrap it into a lib ;). What you call a boilerplate is for me just a realization of a pattern.

Here is an example of a context object and destructuring usage:
http://es6-features.org/#ObjectMatchingParameterContext

function computeSomethingBasedOnExpensiveValue({expensiveNumber: n}) {
    return n + 1;
}

var Hello = React.createClass({
    getExpensiveNumber: function() {
       return ...; // expensive value computed from props and state
    },
    render: function() {
        var cachingContext = {
          expensiveNumber: this.getExpensiveNumber(),
          expensiveString: this.getExpensiveString()
        };
        return <div>Number is {cachingContext.expensiveNumber}, next number is {computeSomethingBasedOnExpensiveValue(cachingContext)}</div>;
    }
});

Hmmm yes I see your point and how this tricks do the job on this simple example.

However I still think on more complex components this pattern will increase complexity and you would have to pass your caching context to many component instances. Also, you have to precompute the caching context at the beginning of the render method. The advantage of memoization is that the expensive computation is done lazily on demand and avoid doing the computation if it is finally not required by the rendering code. Sure you can also create the caching context conditionnally according to props, but this involves another layer of complexity compared to my proposition

Lazy computing could be achieved with proxys / getters/setters.

To avoid extra code you also could isolate all the caching context related code into a separated module in order to reuse it in several components.

Since, expensive computing done synchronously is very rare (and should be avoided), i think adding that kind of functionality to React is not a good idea.

On the other hand, that's just my opinion, and i'm not using React intensively right now ;).

@nfroidure I'm not sure using proxys getters setters and a separate module for a little performance gain would keep things simple enough.

I'm not trying to run expensive and synchronous machine learning algorithms during the render phase of my React components.

I just point out that there are cases where the same component method that computes something can be called multiple times during a single render phase. Sometimes this method returns an object, an array, a helper or anything. This produces extra computation, memory allocation and garbage collection.

This is a minor optimization detail at a component level (as we are not performing extremely complex computations) but additionned together these optimizations could make sense. That's why the way to trigger these optimizations should rather be very simple, like shouldComponentUpdate is.

@andreypopp deleted his answer but suggested using https://github.com/andreypopp/memoize-decorator
While this will memoize the component method globally (and not for a render phase), using decorators is probably a nice solution like:

var Hello = React.createClass({

    @renderMemoized
    getExpensiveNumber: function() {
       return ...; // expensive value computed from props and state
    },

    @renderMemoized
    computeSomethingBasedOnExpensiveValue: function() {
      return this.getExpensiveNumber() + 1;
    },

    render: function() {
        return <div>Number is {this.getExpensiveNumber()}, next number is {this.computeSomethingBasedOnExpensiveValue()}</div>;
    }
});

What if we use loadash for this?
https://lodash.com/docs#memoize

I don't think React plans to depend on a third party library at any point. Memoization is not complicated to implement btw, and in this case it should be well-integrated with component lifecycle methods.

Compute the expensive value only once at the beginning of the render method, and pass it to all rendering functions that needs it. In complex components this can lead to boilerplate and more complex method signatures.

Is there any objection to using a simple instance variable to store the result of the computation, to avoid having to pass it around?

Where to handle expensive calculations is a really good question and has large implications on the structure of applications. I've been struggling with this for a while and have tried many non-ideal solutions.

For instance if we have a Component with an expensive filter function to pass data down to a child component, it is a problem to me that the expensive function gets run every time some other prop change occurs.

I would really like to avoid using componentWillRecieveProps and state for memoization (it works but seems wrong and gets messy), and pulling this logic upwards doesn't always work and moves away functionality that you may want the aforementioned component to manage.

I would really love to just handle this at the last possible moment => in the render function of the component that requires the calculation for its desired functionality.
Its lame that when a calculation is expensive I have to be like "oh well this can't go here anymore ¯\(ツ)/¯" and abandon a consistent component structure to manage this some other way.

I've been searching for an opinion from @gaearon on this (because I believe his comments have very much experience and thought put into them) but couldn't find anything. Hopefully he can find the time to shed some light here.

Seeing as this has been left open for years, I imagine there is an answer out there.

You can implement it in render with a HOC using memoized selectors. Example of usage:

import memoHoc from 'hypothetical-package';

const C = (props) => (
  <div>
    foo: {props.foo((x, y) => x + y)}
    bar: {props.bar((x, z) => x + z)}
  </div>
);

const memos = {
  foo: [props => props.x, props => props.y],
  bar: [props => props.x, props => props.z],
};

export default memoHoc(memos)(C);

In this example, when props.y changes, the function passed to props.foo will run again. Same for props.z changing triggering the props.bar argument. When props.x changes, then both of the functions will run again.

This is just shifting around the arguments in the reselect api, but couldn't be implemented with reselect. Also not sure how you'd make it work if you called props.foo twice in one render with a different function argument.

To extend its versatility, you could instead define the full or partial selectors in render. Here there's another argument computed in render for whatever reason, where it recomputes the selector value when props.x, props.y, or valueOfExtra change.

props.foo(valueOfExtra, (x, y, extra) => x + y + extra)
const memos = {
  foo: [props => props.x, props => props.y],
};

@brigand very cool. I just tried this out and I like it a lot.
In my test I was able to contain all the memoization in the HOC and pass the calculated props to my WrappedComponent which really cleaned up the component as an added bonus.

Also, guys, take at this RFC which introduces new lifecycle for your task. Will come in v16.3
https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md