Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

Support memoization of component functions during render phase #33

Open
slorber opened this issue Apr 28, 2015 · 16 comments
Open

Support memoization of component functions during render phase #33

slorber opened this issue Apr 28, 2015 · 16 comments

Comments

@slorber
Copy link

slorber commented Apr 28, 2015

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.

@slorber
Copy link
Author

slorber commented Apr 28, 2015

Possibly related: facebook/react#1654

@nfroidure
Copy link

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>;
    }
});

@slorber
Copy link
Author

slorber commented Apr 28, 2015

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

@nfroidure
Copy link

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.

@slorber
Copy link
Author

slorber commented Apr 28, 2015

@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 :)

@nfroidure
Copy link

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>;
    }
});

@slorber
Copy link
Author

slorber commented Apr 28, 2015

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

@nfroidure
Copy link

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 ;).

@slorber
Copy link
Author

slorber commented Apr 28, 2015

@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>;
    }
});

@topgun743
Copy link

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

@slorber
Copy link
Author

slorber commented Sep 22, 2015

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.

@HermanBovens
Copy link

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?

@RealAdamSinger
Copy link

RealAdamSinger commented Jan 22, 2018

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.

@brigand
Copy link

brigand commented Jan 23, 2018

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],
};

@RealAdamSinger
Copy link

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

@TrySound
Copy link

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants