omniscientjs / omniscient

A library providing an abstraction for React components that allows for fast top-down rendering embracing immutable data for js

Home Page:http://omniscientjs.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Simplify `shouldComponentUpdate`

Gozala opened this issue · comments

While I've being working on #64 I have being refactoring shouldComponentUpdate in order to have a predicate that could be used to compare current & past input of the function.

Current implementation seems little over-engineered, but likely I don't have enough history of the code base to asses all constraints it needs to deal with, so I though I'll start a thread here to propose simplifications and learn why they can or can not be made. I'll start with few facts:

  1. Data structures from Immutable.js do have comparison operator .equal built into them & there for they could be compared simply by expect.equals(actual). Also note that two immutable data structures may not satisfy this check https://github.com/omniscientjs/omniscient/blob/master/shouldupdate.js#L182 while it would expect.equals(actual) here is an example:

    Immutable.List.of(1, 2) == Immutable.List.of(1, 2) // => false
    Immutable.List.of(1, 2).equals(Immutable.List.of(1, 2)) // => true
  2. Cursors at (least from Immutable.js) also do implement .equal operator on them & similar to comparing immutable data structures it will return false in cases where use of .equal would have returned true.

  3. The only other thing that sholudComponentUpdate needs does is ignores special properties like statics and children (I was actually wondering why it is ignoring children). Which got me thinking that
    the whole thing could be simplified & speeded up greatly if we did something slightly different for statics, which I'm gonna cover later.

Given all that, I would imagine shouldComponentUpdate could be simplified to:

"use strict";

var isEqual = require('lodash.isequal');

function isComparable(value) {
  return value && typeof(value.equals) === 'function';
}

function isEqualComparable(expect, actual) {
  return isComparable(expect) ? expect.equals(actual) :
         isComparable(actual) ? false :
         void 0;
}


function shouldComponentUpdate(nextProps, nextState) {
  return !isEqual(nextProps, this.props, isEqualComparable) ||
         !isEqual(nextState, this.state, isEqualComparable);
}

Now you may thing that will not cover statics, but I think that would be easy to fix by statics internally into something like:

function AlwaysEqual(value) {
   this.value = value
}
AlwaysEqual.prototype.equals = function(value) {
   return true;
}

And instead of doing this https://github.com/omniscientjs/omniscient/blob/master/component.js#L197-L201 we could just do:

// If statics is a node (due to it being optional)
// don't attach the node to the statics prop
if (!!statics && !props.statics && !_isNode(statics)) {
   _props[component.shouldNotUpdate] = AlwaysEqual(statics);
} else {
  _props[component.shouldNotUpdate] = AlwaysEqual(statics);
}

Which could be then unboxed before invoking a render, which basically will change this line
https://github.com/omniscientjs/omniscient/blob/master/component.js#L145 to:

return options.render.call(this, input, this.props[component.shouldNotUpdate]);

This not only would improve performance of shouldComponentUpdate & simplify it but would also open door for great new opportunities, here is a short list:

  1. Will lead a natural path for statics validation proposed in #75, only thing that will be required is to make
    update boxing type form AlwaysEqual to something smarter that will return false in case transparent rewiring could not be done.

  2. In addition it would solve problem again stated in #75 of how do we let third parties rewire their custom communication channels. With this it's going to be easy the would just need to have their own boxing type that will have to implement .equals to make a call weather rewiring is possible and [component.rebase] to redirect communication to a new target. I think it will look very elegant here are few examples:

    View({cursor, output: Output({remove, change}) })

    Note that key of the prop no longer matters it can be whatever, in this case Output will be something along these lines:

    function Output(channels) {
      if (this instanceof Output) {
       if (channels.pipe) {
         throw TypeError('pipe is reserved key and can not be used please use different name')
       }
       if (channels.equals) {
         throw TypeError('equals is reserved key and can not be used please use different name')
       }
    
       Object.assign(this, channels);
      } else {
        return new Output(channels);
      }
    }
    Output.prototype.equals = function(other) {
       // If all are channels we can re direct to them.
       return lodash.isEqual(this, other, function(a, b) {
          return isChannel(b);
       });
    };
    Output.prototype[component.rebase] = function(target) {
      Object.keys(target).forEach(function() {
        this[key].pipe(target[key]);
      }, this);
    };

    In fact this can be taken even a step forward if channel, signal, event emitter or else would opt-in and implement these two methods they would not even need to be boxed and could be passed along as regular props:

    View({cursor, outputChannel})

    In case we want to go that route we should probably change from just .equals to something less generic I'd say this:

    var isEqual = require('lodash.isequal');
    
    function isRebaseable(value) {
      return value && value[isRebaseableOnto]
    }
    
    function isComparable(value) {
      return value && typeof(value.equals) === 'function';
    }
    
    function isEqualComparable(expect, actual) {
      return isComparable(expect) ? expect.equals(actual) :
             isComparable(actual) ? false :
             void 0;
    }
    
    function isRebaseableOntoOrEqual(expect, actual) {
      return isRebaseable(expect) ? expect[isRebaseableOnto](actual) :
                isEqualComparable(expect, actual);
    }
    
    
    function shouldComponentUpdate(nextProps, nextState) {
      return !isEqual(nextProps, this.props, isRebaseableOntoOrEqual) ||
             !isEqual(nextState, this.state, isEqualComparable);
    }
  3. As you could already have noticed in previous item this way we no longer need any special key in the props instead users can just wrap stuff they want to pass but do not affect sholudComponentUpdate and be done.

.equals operation for Immutable collections is still fairly expensive; we should still take advantage of === to compare Immutable objects.

This should be preserved:

omniscient/shouldupdate.js

Lines 175 to 187 in d2b4d3c

if (_isCursor(current) && _isCursor(next)) {
return _isEqualCursor(current, next);
}
if (_isCursor(current) || _isCursor(next)) {
return false;
}
if (_isImmutable(current) && _isImmutable(next)) {
return current === next;
}
if (_isImmutable(current) || _isImmutable(next)) {
return false;
}
return void 0;


I pretty much agree that shouldComponentUpdate can be simplified by delegating stuff like hasDifferentKeys or key length check to lodash.isEquals.

.equals operation for Immutable collections is still fairly expensive; we should still take advantage of === to compare Immutable objects.

I do believe lodash.isEqual will actually do === and only invoke custom comparator if that isn't true, so unless I'm mistaken about that it won't be necessary.

@Gozala lodash.isEqual would invoke the custom comparator first for any two input. Comparison would only be delegated back to lodash.isEqual when custom comparator returns void 0.

This should be preserved:

omniscient/shouldupdate.js

Lines 175 to 187 in d2b4d3c

if (_isCursor(current) && _isCursor(next)) {
return _isEqualCursor(current, next);
}
if (_isCursor(current) || _isCursor(next)) {
return false;
}
if (_isImmutable(current) && _isImmutable(next)) {
return current === next;
}
if (_isImmutable(current) || _isImmutable(next)) {
return false;
}
return void 0;

Can you please elaborate ? If values are identical loadash will return true without going through any of that if these are immutable and or cursor that aren't equal (as otherwise it would have short circuited already) we should probably let implementer of data type decide if values are equal and both immutable and cursors do have .equals for that. From what I can tell Immutable.js also takes shortcuts whenever it can: https://github.com/facebook/immutable-js/blob/master/src/utils/deepEqual.js

@Gozala lodash.isEqual would invoke the custom comparator first for any two input. Comparison would only be delegated back to lodash.isEqual when custom comparator returns void 0.

It seems odd, I can't imagine why would customiser would return false if a === b, but you are absolutely right: https://github.com/lodash/lodash/blob/master/lodash.js#L8147-L8154

So yeah we can update both isEqualComparable and isRebaseableOntoOrEqual to these:

function isEqualComparable(expect, actual) {
  return expect == actual ? true :
         isComparable(expect) ? expect.equals(actual) :
         isComparable(actual) ? false :
         void 0;
}

function isRebaseableOntoOrEqual(expect, actual) {
  return expect == actual ? true : 
         isRebaseable(expect) ? expect[isRebaseableOnto](actual) :
         isEqualComparable(expect, actual);
}

It's preferable to not compare by values (i.e. deep equal or Object.is semantics), and only compare by reference (which is usually cheaper in the long-run).


(Saw that you figured out lodash.isEqual, posting demo for posterity)

const
_ = require('lodash'),
Immutable = require('immutable');

const map1 = Immutable.Map({a: 42});
const map2 = map1;
const map3 = Immutable.Map({a: 42});

console.log(map1 === map3); // => false

const comp = function(x,y) {

    const x_iter = Immutable.Iterable.isIterable(x);
    const y_iter = Immutable.Iterable.isIterable(y);

    if(x_iter && y_iter) {
        return (x === y);
    }

    if(x_iter || y_iter) {
        return false
    }

    return void 0;
}

console.log(_.isEqual(map1, map2, comp)); // => true
console.log(_.isEqual(map1, map3, comp)); // => false

console.log(map1.equals(map2)); // => true
console.log(map1.equals(map3)); // => true

Ok based on comments from @dashed I'm updating proposed simplification to following:

var isEqual = require('lodash.isequal');

function isRebaseable(value) {
  return value && value[isRebaseableOnto]
}

function isComparable(value) {
  return value && typeof(value.equals) === 'function';
}

function isEqualComparable(expect, actual) {
  return expect == actual ? true :
         isComparable(expect) ? expect.equals(actual) :
         isComparable(actual) ? false :
         void 0;
}

function isRebaseableOntoOrEqual(expect, actual) {
  return expect == actual ? true :
         isRebaseable(expect) ? expect[isRebaseableOnto](actual) :
         isEqualComparable(expect, actual);
}

function shouldComponentUpdate(nextProps, nextState) {
  var isEqualProps = nextProps == this.props ||
                     isEqual(nextProps, this.props, isRebaseableOntoOrEqual);

  var isEqualState = isEqualProps &&
                     nextState == this.state ||
                     isEqual(nextState, this.state, isEqualComparable);

  return !isEqualState;
}

This should be preserved:

omniscient/shouldupdate.js

Lines 175 to 187 in d2b4d3c

if (_isCursor(current) && _isCursor(next)) {
return _isEqualCursor(current, next);
}
if (_isCursor(current) || _isCursor(next)) {
return false;
}
if (_isImmutable(current) && _isImmutable(next)) {
return current === next;
}
if (_isImmutable(current) || _isImmutable(next)) {
return false;
}
return void 0;

@dashed would latest version cover this or am I still missing something ?

@Gozala Latest version unfortunately still doesn't cover it :(


This is how I would envision the refactor.

I'm not a big fan of nested ternary operators. So I made a generalized or higher-order function that's left-associative. This would make it easy to see and sort the precedence order of various custom comparison checks. I added voidFn as the last function in the comparison check to explicitly delegate comparison back to _.isEqual as opposed to relying on the last comparison function to return void 0.

All comparison check functions after referenceCheck have the invariant prev !== next, so this makes them more simpler, and they can return void 0 as well.

function or(...args) {
    return function(...fnArgs) {
        for (let _len = args.length, _idx = 0; _idx < _len; _idx++) {
            const fn = args[_idx];
            const ret = fn.apply(fn, fnArgs);
            if(ret || (_idx === (_len - 1))) {
                // NOTE:
                // It's imperative to preserve the value of ret (e.g. don't do this !!ret)
                // _.isEqual relies on this explicit value.
                return ret;
            }
        }
        return false;
    }
}

function referenceCheck(prev, next) {
    return (prev === next);
}

const IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
function isImmutable(maybeImmutable) {
  return !!(maybeImmutable && maybeImmutable[IS_ITERABLE_SENTINEL]);
}

function immutableCheck(prev, next) {

    if (isImmutable(prev) || isImmutable(next)) {
        return false;
    }
}

function isCursor(potential) {
    return potential && typeof potential.deref === 'function';
}

function unCursor(cursor) {
  if (!cursor || !cursor.deref) return cursor;
  return cursor.deref();
}

function cursorCheck(prev, next) {

    const isPrevCursor = isCursor(prev);
    const isNextCursor = isCursor(next);
    if (isPrevCursor && isNextCursor) {
        return unCursor(prev) === unCursor(next);
    }

    if(isPrevCursor || isNextCursor) {
        return false;
    }
}

function isRebaseable(value) {
    return value && value[isRebaseableOnto];
}

function isRebaseableOnto(expect, actual) {

    if(isRebaseable(expect)) {
        return expect[isRebaseableOnto](actual);
    }

    return false;
}

function isComparable(value) {
    return value && typeof(value.equals) === 'function';
}

function isEqualComparable(expect, actual) {

    if(isComparable(expect)) {
        return expect.equals(actual);
    }

    if(isComparable(actual)) {
        return false;
    }
}

function voidFn() {
    return void 0;
}

// construct precedence order of comparison check
const fundamentalCheck = or(referenceCheck, immutableCheck, cursorCheck);
const propsCheck = or(fundamentalCheck, isRebaseableOnto, isEqualComparable, voidFn);
const stateCheck = or(fundamentalCheck, isEqualComparable, voidFn);

function compare(nextProps, nextState) {
    return !isEqual(nextProps, this.props, propsCheck) ||
        !isEqual(nextState, this.state, stateCheck);
}

@Gozala Latest version unfortunately still doesn't cover it :(

To clarify. I'm not really comfortable delegating to .equals for comparison of cursors and immutable objects. IMO, it's no better than delegating to _.isEqual.

I'm not a big fan of nested ternary operators. So I made a generalized or higher-order function that's left-associative. This would make it easy to see and sort the precedence order of various custom comparison checks. I added voidFn as the last function in the comparison check to explicitly delegate comparison back to _.isEqual as opposed to relying on the last comparison function to return void 0.

All comparison check functions after referenceCheck have the invariant prev !== next, so this makes them more simpler, and they can return void 0 as well.

I <3 functional programming and high order functions, but I would much rather avoid them in this case since sholudComponentUpdate is the most frequently called function (at least in the code) I've being working on, so getting it as fast as possible would be my preference, doing that many function calls has cost that I'd rather avoid paying in such a hot code path.

To clarify. I'm not really comfortable delegating to .equals for comparison of cursors and immutable objects. IMO, it's no better than delegating to _.isEqual.

May I know the reason why you feel uncomfortable doing it ? Without knowing reasons it seems awfully odd that you are more comfortable depending on internal implementation details like x['@@__IMMUTABLE_ITERABLE__@@'] than using public API of that data structure designed to do an equality check.

Also please note that immutable data structures have hashing built in which means that subsequent comparisons will be a lot faster than traversing data structure which is what _.isEqual does (although it's not the case in your implementation).

Hah. My proposal is completely flawed. Here's my revised proposal.

I ditched or higher-order function in favour of something that would return the first non-undefined value of any function. If a function is the last to be evaluated, it'll return any value it returns.

It's pretty much the chain version of the customiser function semantics as defined for lodash.isEqual.

/**
 * Given N functions that all take n arguments, return a function X.
 * Function X takes n arguments, and evaluates N functions in the order they're given.
 * If any function returns a non-undefined value, function X returns that value.
 * If function X reached the last given function, it'll return any value it returns.
 */
function firstValue(...args) {
    return function(...fnArgs) {
        for (let _len = args.length, _idx = 0; _idx < _len; _idx++) {
            const fn = args[_idx];
            const ret = fn.apply(fn, fnArgs);

            if(ret !== void 0 || _idx === (_len - 1))
                // NOTE:
                // It's imperative to preserve the value of ret (e.g. don't do this !!ret)
                // _.isEqual relies on this explicit value.
                return ret;
        }
    }
}

function referenceCheck(prev, next) {
    return ((prev === next) || void 0);
}

const IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
function isImmutable(maybeImmutable) {
  return !!(maybeImmutable && maybeImmutable[IS_ITERABLE_SENTINEL]);
}

// redundant
function immutableCheck(prev, next) {
    if (isImmutable(prev) || isImmutable(next)) {
        return false;
    }
}

function isCursor(potential) {
    return potential && typeof potential.deref === 'function';
}

function unCursor(cursor) {
  if (!cursor || !cursor.deref) return cursor;
  return cursor.deref();
}

function cursorCheck(prev, next) {
    if (isCursor(prev) && isCursor(next)
        && (unCursor(prev) === unCursor(next))) {
        return true;
    }

    if(isCursor(prev) || isCursor(next)) {
        return false;
    }
}

function isRebaseable(value) {
    return value && value[isRebaseableOnto];
}

function rebaseableOntoCheck(expect, actual) {
    if(isRebaseable(expect) && expect[isRebaseableOnto](actual)) {
        return true;
    }

    if(isRebaseable(actual)) {
        return false;
    }
}

function isComparable(value) {
    return value && typeof(value.equals) === 'function';
}

function comparableCheck(expect, actual) {
    if((isComparable(expect) && expect.equals(actual))) {
        return true;
    }

    if(isComparable(actual)) {
        return false;
    }
}

function voidFn() {
    return void 0;
}

// construct precedence order of comparison check
const fundamentalCheck = firstValue(referenceCheck, immutableCheck, cursorCheck);
const propsCheck = firstValue(fundamentalCheck, rebaseableOntoCheck, comparableCheck, voidFn);
const stateCheck = firstValue(fundamentalCheck, comparableCheck, voidFn);

function compare(nextProps, nextState) {
    return !isEqual(nextProps, this.props, propsCheck) ||
        !isEqual(nextState, this.state, stateCheck);
}

I <3 functional programming and high order functions, but I would much rather avoid them in this case since sholudComponentUpdate is the most frequently called function (at least in the code) I've being working on, so getting it as fast as possible would be my preference, doing that many function calls has cost that I'd rather avoid paying in such a hot code path.

Here's an unrolled equivalent version to above:

const IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
function isImmutable(maybeImmutable) {
  return !!(maybeImmutable && maybeImmutable[IS_ITERABLE_SENTINEL]);
}

function isCursor(potential) {
    return potential && typeof potential.deref === 'function';
}

function unCursor(cursor) {
  if (!cursor || !cursor.deref) return cursor;
  return cursor.deref();
}

function isRebaseable(value) {
    return value && value[isRebaseableOnto];
}

function isComparable(value) {
    return value && typeof(value.equals) === 'function';
}

function aggregateCheck(disableRebase, prev, next) {
    if(prev === next)
        return true;

    if(isImmutable(prev) || isImmutable(next)) {
        return false;
    }

    if (isCursor(prev) && isCursor(next)
        && (unCursor(prev) === unCursor(next))) {
        return true;
    }

    if(isCursor(prev) || isCursor(next)) {
        return false;
    }

    if(!disableRebase && isRebaseable(expect) && expect[isRebaseableOnto](actual)) {
        return true;
    }

    if(!disableRebase && isRebaseable(actual)) {
        return false;
    }

    if((isComparable(expect) && expect.equals(actual))) {
        return true;
    }

    if(isComparable(actual)) {
        return false;
    }
}

function compare(nextProps, nextState) {
    return !isEqual(nextProps, this.props, aggregateCheck.bind(null, false)) ||
        !isEqual(nextState, this.state, aggregateCheck.bind(null, true));
}

May I know the reason why you feel uncomfortable doing it ? Without knowing reasons it seems awfully odd that you are more comfortable depending on internal implementation details like x['@@__IMMUTABLE_ITERABLE__@@'] than using public API of that data structure designed to do an equality check.

Also please note that immutable data structures have hashing built in which means that subsequent comparisons will be a lot faster than traversing data structure which is what _.isEqual does (although it's not the case in your implementation).

The other option was to bundle immutable as a heavy dependency, which isn't really feasible. Eventually this would be finished: immutable-js/immutable-js#317

It's preferable to just check prevMaybeImmutable === nextMaybeImmutable. If that's false, then just check if either are actually Immutable structures, and then bail the check chain (e.g. return false).

You'll notice that (in my revised proposal), for every "comparison type", if === is false, then checking if either prev or next is of the same type, then have shouldComponentUpdate return true.

My argument is that it's cheaper (in the long run) to bail out early and have shouldComponentUpdate return true, than to check internal values via .equals or some other deep equality checking mechanism. When I say that it's "cheaper", I'm saying that re-renders are assumed to be cheap.

Why bother doing deep equality checking at shouldComponentUpdate level, when this is inherently done via React components (i.e. shouldComponentUpdate of sub-components)?

Example:

component(function({settings, profile}) {

   return (
        <Row>
            // In React components, you've defined where sub-props go in sub-components
            // Just let sub-components' shouldComponentUpdate handle their own props.
            <Settings settings={settings} />
            <Profile profile={profile} />
        <Row>
   ) 
});

Also please note that immutable data structures have hashing built in which means that subsequent comparisons will be a lot faster than traversing data structure which is what _.isEqual does (although it's not the case in your implementation).

Okay, so I looked into what Immutable does with .equals, to see if it's worth using. I'm following code via: https://github.com/facebook/immutable-js/blob/d6086b32202fff584452570967530e6b64fd271b/src/IterableImpl.js#L282-L284

Interestingly, hashing isn't performed until .hashCode() is explicitly called.

https://github.com/facebook/immutable-js/blob/d6086b32202fff584452570967530e6b64fd271b/src/IterableImpl.js#L433-L435

Thus: assert(Immutable.Map({a: 42}).__hash === void 0)

This leads me to believe it's an expensive operation.

Even if it's computed, it isn't a guarantee that two Immutable structures are equivalent, and it'll still need to perform deep equality checking:
https://github.com/facebook/immutable-js/blob/d6086b32202fff584452570967530e6b64fd271b/src/utils/deepEqual.js#L22

When objects are different, it isn't any different than doing (i.e. you get the same results):

    if(prev === next)
        return true;

    if(isImmutable(prev) || isImmutable(next)) {
        return false;
    }

Ideally, this is what we want.

const map1 = Immutable.Map({a: 42});
// render component given map1

const map2 = Immutable.Map({a: 42});
// don't render component given map2

But practically, this is a corner case where it's not worth doing .equals or some deep equality check mechanism. Assuming re-renders are cheap (as it should), it's realistic to make shouldComponentUpdate computationally cheap to execute via reference checking; and only on object types that lodash.isEqual doesn't know how to compare.

IMO, wanting deep equality, just means, to me, that the developer wants a certain keyPath to be critically compared. This can be done via a custom shouldComponentUpdate in a mixin.

It's preferable to just check prevMaybeImmutable === nextMaybeImmutable. If that's false, then just check if either are actually Immutable structures, and then bail the check chain (e.g. return false).

I understand that you want to bail fast, that is why you check if one of the inputs isn't immutable, but again immutable.js does the same with their .equals check see ? https://github.com/facebook/immutable-js/blob/master/src/utils/deepEqual.js

So I get an impression that you do not trust them to bail fast when possible, but that is not the case.

Why bother doing deep equality checking at shouldComponentUpdate level, when this is inherently done via React components (i.e. shouldComponentUpdate of sub-components)?

Because each component in it's render will generate additional garbage and will trigger sub components even if there is no need for that. If course it will depend from case to case what will be faster in practice, but I'd argue that in larger & complex apps doing data comparison will be faster.

Okay, so I looked into what Immutable does with .equals, to see if it's worth using. I'm following code via: https://github.com/facebook/immutable-js/blob/d6086b32202fff584452570967530e6b64fd271b/src/IterableImpl.js#L282-L284

Interestingly, hashing isn't performed until .hashCode() is explicitly called.

That is absolutely correct, hashes are calculated lazily but they are not just triggered during comparison there are bunch of operations that would trigger hash calculation for example use of data structure as a key in a map would trigger it. So chances are hash is already cached by the time you run .equals.

Even if it's computed, it isn't a guarantee that two Immutable structures are equivalent, and it'll still need to perform deep equality checking:
https://github.com/facebook/immutable-js/blob/d6086b32202fff584452570967530e6b64fd271b/src/utils/deepEqual.js#L22

You are misunderstanding that code it checks if bails early of both structures have hashes but thy do not match.

But practically, this is a corner case where it's not worth doing .equals or some deep equality check mechanism. Assuming re-renders are cheap (as it should), it's realistic to make shouldComponentUpdate computationally cheap to execute via reference checking; and only on object types that lodash.isEqual doesn't know how to compare.

IMO, wanting deep equality, just means, to me, that the developer wants a certain keyPath to be critically compared. This can be done via a custom shouldComponentUpdate in a mixin.

I'm afraid you are making too many assumptions, doing comparison over immutable data structures is a lot cheaper than doing those on mutable ones. Also hashing is not expensive as you assumed in fact it is one of the factors why comparison of immutable data structures is cheaper than deep equality checks.

So I would not jump a gun here and say that immutable data structure comparison is faster than re-rendering as it really depends on the data & application structure but I'd argue that larger the size of application is more likely that data comparison will be cheaper same as story as with transducers vs arrays.

Also please keep in mind that during that app state transition will be like a -> b -> c -> d which means by the time you will compare b to c former will already have hash calculated so it will be calculating hash for just c in worst case. Now since components tend to be organised in sub-treessubcomponents will end up comparing subtrees of b and c and by that time hashes are already calculated for both so it's super cheap.

That is absolutely correct, hashes are calculated lazily but they are not just triggered during comparison there are bunch of operations that would trigger hash calculation for example use of data structure as a key in a map would trigger it. So chances are hash is already cached by the time you run .equals.

I cannot seem to indirectly trigger a hash creation.

const
_ = require('lodash'),
Immutable = require('immutable');

const map1 = Immutable.Map({a: 42});
const map2 = Immutable.Map({a: 42});

const map3 = map1.set(map1, 24);

console.log(map1.equals(map2)); // => true
console.log(map2.equals(map1)); // => true
console.log(map1.__hash); // => undefined
console.log(map2.__hash); // => undefined
console.log(map3.__hash); // => undefined

OTOH, if I force a hash creation, new immutable objects don't seem to "keep" and "update" the hash.

const
_ = require('lodash'),
Immutable = require('immutable');

const map1 = Immutable.Map({a: 42});
const map2 = Immutable.Map({a: 42});

map1.hashCode();

const map3 = map1.set(map1, 24);

console.log(map1.__hash); // => 545632000
console.log(map2.__hash); // => undefined
console.log(map3.__hash); // => undefined

We're now pretty much arguing whether or not to keep the following:

    if(isImmutable(prev) || isImmutable(next)) {
        return false;
    }

If this is dropped from the precedence chain, then we're relying .equals as the only means of duck typing check.


Also please keep in mind that during that app state transition will be like a -> b -> c ->d which means by the time you will compare b to c former will already have hash calculated so it will be calculating hash for just c in worst case. Now since components tend to be organised in sub-treessubcomponents will end up comparing subtrees of b and c and by that time hashes are already calculated for both so it's super cheap.

The nature of omniscient is that it encourages a top-down rendering. That is, it encourages you to push your entire app/global state through the root component.

Currently, this component re-renders when prev !== next && (isImmutable(prev) || isImmutable(next)). If, instead, we delegate completely to .equals, we should trust the user to ensure prev.__hash and next.__hash are defined to avoid regression in performance.

If they aren't defined, then at the root component, deep equality is performed on global state. Then at the second level, deep equality is performed on sub-stree state. And so on. But notice that some equality checks were already performed via an ancestor's shouldComponentUpdate.

This can be bad if a change only occurred at the leaf of the state! Which can run O(n^2).

When I say: run O(n^2). I meant that n is the # of elements in keyPath to changed node in the state tree.

You are misunderstanding that code it checks if bails early of both structures have hashes but thy do not match.

Ah. Yeah, you're right. I misread that line.


This seems to work. But hash doesn't seem to carry over to new Immutable object unless you explicitly call .hashCode(). Is this necessary?

const
Immutable = require('immutable');

const map1 = Immutable.Map({a: 42});
const map2 = Immutable.fromJS({
    a: map1
});

map2.hashCode();

const map3 = map2.set('b', 24);

console.log(map1.__hash); // => 545632000
console.log(map2.__hash); // => 287702776
console.log(map3.__hash); // => undefined

Updating a sub-tree doesn't preserve or update the hash.

const
Immutable = require('immutable');

const map1 = Immutable.Map({a: 42});
const map2 = Immutable.fromJS({
    a: map1
});

map2.hashCode();

const map3 = map2.set('b', 24);
const map4 = map3.setIn(['a', 'b'], 25);

console.log(map1.__hash); // => 545632000
console.log(map2.__hash); // => 287702776
console.log(map3.__hash); // => undefined

console.log(map3.get('a').__hash); // => 545632000
console.log(map4.get('a').__hash); // => undefined

You'd have to ensure you'd call .hashcode() in any and all Immutable object. Which is a huge chore.


I understand that you want to bail fast, that is why you check if one of the inputs isn't immutable, but again immutable.js does the same with their .equals check see ? https://github.com/facebook/immutable-js/blob/master/src/utils/deepEqual.js

Yes, but when assert(prev !== next && isImmutable(prev) && isImmutable(next)), .equals fallbacks to deep equality check. This is pointless if one uses omniscient for top-down rendering since at each component level, you usually would pass in a sub-tree.

If a change occurred at a leaf (worst case), then if keyPath is an array denoting the path to the change, Then if a component is n levels deep, then nodes via keyPath.slice(n) in given props element was already compared m = keyPath.length - n times through m ancestors.

The takeaway is that a parent component shouldn't be repeating the computations of their children's shouldComponentUpdate.

I'm not really convinced that hashes are cheap if you need to explicitly recalculate them at every state change. If anything, this would create technical debt which ruins the nice-ish API usage flow of the Immutable library.

I'm just reading through this now. It seems to me you're suggesting to move to .equals to make this:

Immutable.List.of(1, 2) === Immutable.List.of(1, 2) // => false

Return true. However. Those are not the same values, immutable wise, and I don't really see someone doing:

render(Component(Immutable.List.of(1, 2)))
// and later
render(Component(Immutable.List.of(1, 2)))

And if they do, it's most likely very rarely, and a the cost of re-render wouldn't be too high. I think we need to optimize for encouraged usage. So rather having a more optimized shouldComponentUpdate than allowing for some edge case usage. And reference checks will be cheaper than .equals in any case.

Here's the intended behaviour of shouldComponentUpdate:

  1. Do cheap checks (with maximum benefits) first (like the length)
  2. Optimize for usage with cursors as this is the encourage behaviour
  3. Order checks for what is considered cheapest.

There is definitely room for improvement in the current shouldComponentUpdate. Especially now when we use _.isEqual and the customizer transformation.


I think something like the Output construction might be handy, and actually might ease the use of "statics".

You are still misunderstanding my points here let me illustrate it:

var x = Immutable.fromJS({ a: { b: { c: {} } } })

x.hashCode() // => -20936679
x.get('a').__hash // =>  -640595291
x.getIn(['a', 'b']).__hash // => -459264562
x.getIn(['a', 'b', 'c']).__hash // => -15128758

var y = x.setIn(['a', 'b', 'd'], 'y')

x.equals(y) // => false

y.getIn(['a', 'b', 'c']).__hash // => -15128758

As you can notice hashes for part that have not updated are kept only part that did update do not have hashes calculated yet.

@dashed To get to the point your assumption is that equality check on immutable data structures is expensive, which is not the case, but more importantly what your code suggest does not makes it any better at it takes same shortcuts as equality checks in Immutable.js take as well so why are we arguing about it ?

I don't wanna continue arguing that comparison of immutable data structures is actually fast, instead I'd like to point to resources that explain their performance characteristics and how this is possible
http://hypirion.com/musings/understanding-persistent-vector-pt-1 (it's about clojure but immutable.js implements the same)
https://www.youtube.com/watch?v=I7IdS-PbEgI And here is a talk from reactconf explaining immutable.js itself

And if they do, it's most likely very rarely, and a the cost of re-render would be too much. I think we need to optimize for encouraged usage. So rather having a more optimized shouldComponentUpdate than allowing for some edge case usage. And reference checks will be cheaper than .equals in any case.

Please note that .equals does reference check at first and then many other checks to bail as fast as possible. If B is some sort of transformation of A (which would be very likely a case) immutable data structures will share most of the structure and will have only path to leafs updated so doing comparison between those will be really fast & in most cases will end up as reference checks.

Of course if you create equal data from scratch than comparison that will be slower, but as we agree that would be a rare case.

The fun fact is that _.isEqual on props actually needs to go and traverse complete structure unlike .equal on immutable data structures but that seems to be ok while later isn't :(

Also please keep in mind that if shouldComponentUpdate returns true that means that it will trigger cascading render (unless it's a leaf component) creating bunch of objects to represent virtual dom and (for GC to collect), likely some closures along and then finally perform diffing over virtual dom which is almost certainly gonna be slower as there will be no sharing between vdom returned by last call to render and current call.

To cut to the point what I am saying is .equal on immutable data structures most certainly will be faster than triggering render unless you pass giant structures with no sharing while not using almost any of it. All this also can be also checked with a profiler to draw conclusions based on numbers.

@Gozala I totally understand. But to take advantage of this performance characteristic would mean to always ensure to call .hashCode() at the root node.

const
_ = require('lodash'),
Immutable = require('immutable');

const x = Immutable.fromJS({ a: { b: { c: {} } } })

x.hashCode();
console.log(x.get('a').__hash); // => -640595291
console.log(x.getIn(['a', 'b']).__hash); // => -459264562
console.log(x.getIn(['a', 'b', 'c']).__hash); // => -15128758

const y = x.setIn(['a', 'b', 'd'], 'y')

x.equals(y) // => false


console.log(y.getIn(['a', 'b', 'c']).__hash); // => -15128758


console.log(y.get('a').__hash); // => undefined
console.log(y.getIn(['a', 'b']).__hash); // => undefined
console.log(y.getIn(['a', 'b', 'c']).__hash); // => -15128758

// have to do this every single time we update the immutable object
y.hashCode();

console.log(y.get('a').__hash); // => -335500329
console.log(y.getIn(['a', 'b']).__hash); // => -544040712
console.log(y.getIn(['a', 'b', 'c']).__hash); // => -15128758

This doesn't carry over very well for cursors. You'd be insane to have to ensure to call .hashCode() on every new Immutable object.


Also please keep in mind that if shouldComponentUpdate returns true that means that it will trigger cascading render (unless it's a leaf component) creating bunch of objects to represent virtual dom and (for GC to collect), likely some closures along and then finally perform diffing over virtual dom which is almost certainly gonna be slower as there will be no sharing between vdom returned by last call to render and current call.

Recall that the default shouldComponentUpdate behaviour on vanilla React is that it returns true. That's why I kept pushing for the assumption that re-renders are cheap. If an app isn't snappy when shouldComponentUpdate always return true, then you're probably doing something wrong; and you should probably optimize other areas before you optimize shouldComponentUpdate. This is why we can get away with cheap checks.

@Gozala I've hadn't have the time to look at the details of this yet, but I just thought I'd say what the intention of shouldComponentUpdate, but I think we all pretty much agree that it should exit early and have some sort of precedence list for what is the cheapest and most likely checks first.

The fun fact is that _.isEqual on props actually needs to go and traverse complete structure unlike .equal on immutable data structures but that seems to be ok while later isn't :(

_.isEqual would work as a generic solution (across cursors, immutable structures and object literals), and with customizer checking for immutable/cursors, we'd get the performance gains for them as well. So, the way I see it, _.isEqual, could probably use .equal internally in the customizer.

To cut to the point what I am saying is .equal on immutable data structures most certainly will be faster than triggering render unless you pass giant structures with no sharing while not using almost any of it. All this also can be also checked with a profiler to draw conclusions based on numbers.

I haven't looked at the implementation of .equal, yet, but from what you're saying, it seems like it should be.

I'll look more into this later.

Like mentioned on #78, and with regards to Immutable.js' equalswe need some hard numbers to draw conclusions on the impact of this. I think we're all for a faster and simpler should component update, so your investigations are engcouraged 👍

Please note that .equals does reference check at first and then many other checks to bail as fast as possible.

Well yes, but its intent is opposite of ours. Its trying to fint out if objects are equal - we're trying to find out if objects are unequal. If its two references are unequal, the values of the objects can still be equal, so it continues.. Here, on the other hand, comparing values is a waste of time. When the reference has changed, the data has aswell.

Well yes, but its intent is opposite of ours. Its trying to fint out if objects are equal - we're trying to find out if objects are unequal. If its two references are unequal, the values of the objects can still be equal, so it continues.. Here, on the other hand, comparing values is a waste of time. When the reference has changed, the data has changed

I think this is key. This perspective has everything to say for if it is optimized or not.

As mentioned previously, and after looking through the equals implementation, I think we should optimized for encourage usage. So having reference checks and not deep value checks on immutable data is prefered. Passed immutable structures can be really deep and have a lot of values and value checks can be costly - when hashes aren't generated.

After sleeping at it I think I came to following conclusions:

  1. In common case doing .equal vs bailing == comparison between immutable structures would not make a difference since immutable structures share as much as possible with structures that were transformed.
  2. In pathological case (where data structure is re-created without any sharing) but there are no matches, .equal are likely will cost more & likely cost will be proportional to the number of components involved, unless hashing is happening (it seems that regular comparison does not cause hashes to be generated which is unfortunate, otherwise it would actually have a chance to be faster that current checks).
  3. In pathological case (were data structure is re-created without any sharing) but there is lot of matches .equal is likely gonna be more efficient, as it would prevent re-renders on matching cases. If hashing was triggered by .equal checks savings would have being far greater. There are cases where use .equal would actually make a difference, for example we plan on store changes into indexed-db to allow kind of infinite undo, since switching back by reading from indexed-db won't have any sharing with structure being rendered .equal has potential to make a difference.
  4. Not having to check for data types like isCursor & isImmutable actually would cut down the operations need to be done to bail, since very first think .equal does for both types is type comparison & in no match they both bail. So overall simplification, no real coupling with any data type and potential faster bail by cutting down function invocation.

Overall in practice I do not believe choosing one option over the other will make any visible difference in common (non pathological) case. There for I don't think arguing about this any further is worth it.

Only unfortunate thing is that I really liked how .equals made rebasing of statics very natural.

Well yes, but its intent is opposite of ours. Its trying to fint out if objects are equal - we're trying to find out if objects are unequal. If its two references are unequal, the values of the objects can still be equal, so it continues..

Sure but they also try to bail as fast as they can as shooting for non-equality is a better strategy to complete sooner. In case they can't fail soon, that actually means there is equality in sub-structures, which also implies that's not time wasted as it likely will let us avoid some re-renders down the pipe.

Here, on the other hand, comparing values is a waste of time. When the reference has changed, the data has changed

I don't agree with this last part. If equality checks do take visibly longer that's implication of equality in sub-structures which can save us from re-rendering down the pipe.

I think it's also worth keeping in mind that re-render in case of React is actually cheaper and comparisons of arbitrary objects is a lot more expensive. Unlike react omniscient tries to make most if not everything in render which in practice is a lot more wasted allocations if resulting DOM did not differ. Also comparing arbitrary objects is a lot more expensive that immutable data structures.

@Gozala I think anyone using point (3) without considering/taking full advantage of the immutable structures would be insane. Though I agree that .equal can guard this.

Anyway I'll let you guys decide what to do here. I will also try to followup with Immutable.js folks to find out why hashes are not generated during equality checks & if there an option to try to reuse results of some equality checks in substructure comparisons later on.

Ok so here is one more example where not just letting immutable.js will cause re-render give current implementation of shouldComponentUpdate:

let point = Immutable.Map({x:0, y: 0});

point.set('x', 1) === point.set('x', 1) // => false

Of course as is this does not seems that bad, but assuming transformation happened n level deep in component hierarchy it would have saved at least n-1 calls to shouldComponentUpdate and n re-rendres.

That being said immutable.js has optimisation to handle point.set('x', point.get('x')) case and return same structure, but at least in my use I tend to have pipelines of transformations in which case this optimisation won't be able to catch it.

To clarify, you mean something like this?

let point = Immutable.Map({x:0, y: 0});

var current = point.set('x', 1);
var next = point.set('x', 1);

 assert(current !== next);

It seems to me that this would be an external synchronization issue.

It seems to me that this would be an external synchronization issue.

I don't understand what you mean.

@dashed to clarify immutable.js structures will handle case where you set leaf node to the value it had by returning same instance, but when you do something little more complicated where it's not just a simple update to field it won't try to assert previous and current will just return new structure even it's equal to previous one. If it's deep in the cursor it will cause bunch of re-renders that otherwise would have short circuited if .equals was used.

Feel this discussion is outdated and we've simplified the SCU some since this. AFAIK, we concluded that .equals and Immutable.is was not the proper thing to use for speed, but rather take other measures to optimise and avoid work in render-methods. Feel free to continue the discussion if you disagree.