Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Table] Eliminate unnecessary re-renders everywhere #998

Merged
merged 15 commits into from
Apr 18, 2017
Merged

Conversation

cmslewis
Copy link
Contributor

@cmslewis cmslewis commented Apr 17, 2017

Checklist

Redux of #988 that fixes some of the nefarious bugs that popped up.

  • Include tests

Changes proposed in this pull request:

Goal: Improve table perf by eliminating unnecessary re-renders, particularly when clicking+dragging.

  • Add @PureRender decorator throughout packages/table.
  • Add custom shouldComponentUpdate to GuideLayer to avoid unnecessary re-renders.
  • Add custom shouldComponentUpdate to RegionLayer to avoid unnecessary re-renders.
    • 🔥 Also, change RegionLayer props to accept pre-computed regionStyles instead of getRegionStyle function. Before, we were creating this styling function inline in table.tsx on each render, causing shallow comparison between this.props.getRegionStyle === nextProps.regionStyle to always return false in RegionLayer.shouldComponentUpdate. Ultimately, this led to tons of unnecessary re-renders of the region layer (e.g. as you were clicking + dragging). LMK if you want to know more.
  • Extend Utils.shallowCompareKeys (+ tests) to compare entire objects, not just subset of keys.
  • Add Utils.arraysEqual (+ tests) to compare arrays using an optional compare callback.

Reviewers should focus on:

  • Does perf improve noticeably enough?
  • Any weird bugs lingering with selected regions or guides? shouldComponentUpdate is tricky to get right.

@blueprint-bot
Copy link

Make shallowCompareKeys return false for two arrays

Preview: documentation | table
Coverage: core | datetime

@llorca
Copy link
Contributor

llorca commented Apr 17, 2017

The table with reordering feature feels super snappy, though it's only 5 rows.

We should enable reordering and other fancy features on the full page spreadsheet table, cause this is where the real stress test is.

if (objA[key] !== objB[key]) {
shallowCompareKeys(objA: any, objB: any, keys?: string[]) {
if (keys != null) {
return shallowCompareKeys(objA, objB, keys);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this not an immediate infinite loop every time?

can we please rename the internal function? might be legit to use _ prefix here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* will be compared using the custom `compare` function if one is provided.
*/
arraysEqual(arrA: any[], arrB: any[], compare?: (a: any, b: any) => boolean) {
if (bothUndefined(arrA, arrB) || bothNull(arrA, arrB)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think arraysEqual(null, undefined) should be true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* Returns true if the arrays are equal. Elements will be shallowly compared by default, or they
* will be compared using the custom `compare` function if one is provided.
*/
arraysEqual(arrA: any[], arrB: any[], compare?: (a: any, b: any) => boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about compare = (a, b) => a === b as a default arg here? then line 357 gets real simple

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet

}
return keys
.map((key) => objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && objA[key] === objB[key])
.every((isEqual) => isEqual);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keys.every((key) => objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && objA[key] === objB[key])

please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, fixed this elsewhere but missed this one

// array contents explicitly.
const verticalGuidesEqual = Utils.arraysEqual(this.props.verticalGuides, nextProps.verticalGuides);
const horizontalGuidesEqual = Utils.arraysEqual(this.props.horizontalGuides, nextProps.horizontalGuides);
return !verticalGuidesEqual || !horizontalGuidesEqual;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be a minor optimization here to inline both vars in the ||. then if the first one is true it won't compute the second one (i think that's how js works).

return !Utils.arraysEqual(this.props.verticalGuides, nextProps.verticalGuides) 
    || !Utils.arraysEqual(this.props.horizontalGuides, nextProps.horizontalGuides);

Copy link
Contributor Author

@cmslewis cmslewis Apr 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice. That's my understanding too - did this same thing in RegionLayer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i saw! sounds good

@@ -21,13 +21,26 @@ export interface IRegionLayerProps extends IProps {
regions?: IRegion[];

/**
* A callback interface for applying CSS styles to the regions.
* The array of CSS styles to apply to each region.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we know which styles belong to which region? just by index? worth noting in docs here i think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. LMK if the wording needs to be massaged though.

public shouldComponentUpdate(nextProps: IRegionLayerProps) {
// shallowly comparable props like "className" tend not to change in the default table
// implementation, so do that check last with hope that we return earlier and avoid it
// altogether.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -528,6 +528,11 @@ export class Regions {
return regionGroups;
}

public static regionsEqual(regionA: IRegion, regionB: IRegion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why move this? the previous location seems more logical to me: next to all the other comparison checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's public now, so it has to move up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh public gotcha ok

const { viewportRect } = this.state;
if (viewportRect == null) {
return {};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this viewportRect check seems to happen in every one of these functions. maybe it can be moved up a level to the function that calls these style*Region() functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Played with this. Looks like viewportRect is not considered in styleBodyRegion, so we can't fully generalize. Will leave as is for now.

runTest(false, [3], []);
runTest(false, [3, 1, 2], [3, 1]);
runTest(false, [{}], [{}]);
runTest(false, [{ x: 1 }], [{ x: 1 }]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes in this file generally feel like too much testing. most of these cases are equivalent: they're different instances of the same thing so of course they're not shallow equal. [] is the same as [3], from my perspective.

i would do this with like 1-3 cases in each describe but not worry about testing every possible permutation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@blueprint-bot
Copy link

Oops, undo describe.only

Preview: documentation | table
Coverage: core | datetime

Copy link
Contributor

@adidahiya adidahiya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clicking and dragging feels faster, but scrolling feels about the same

/**
* Partial shallow comparison between objects using the given list of keys.
*/
function _shallowCompareKeys(objA: any, objB: any, keys: string[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any way you can simplify this to all just one function?

shallowCompareKeys: function shallowCompareKeys(objA: any, objB: any, keys?: string[]) {
    ...

    // recurse
    shallowCompareKeys(...);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would literally be the second time I've actually had to use recursion in the wild, ha. Let me dust off my knowledge and give it a try.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recursion is a tree's best friend

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for this kid. https://www.youtube.com/watch?v=1TZCP6OqRlE

@adidahiya ended up pulling the duplicate validation checks into one place and greatly simplifying the helper function. Didn't do the recursive approach though. This look okay now?

Copy link
Contributor

@gscshoyru gscshoyru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, though agree with Adi that scrolling seems really really slow on the formats table. Wish we knew why...

@cmslewis
Copy link
Contributor Author

@gscshoyru I definitely see the symptoms in the code. Will spend today getting to the bottom of it. (If you're in a masochistic mood, you should try clicking a column header in that formats table with why-did-you-update enabled.)

@blueprint-bot
Copy link

Reduce duplicate logic in shallowCompareKeys

Preview: documentation | table
Coverage: core | datetime

Copy link
Contributor

@giladgray giladgray left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great refactor

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

Successfully merging this pull request may close these issues.

6 participants